利用ast和jscodeshift統計項目文件中接口重複使用率

第一階段:
由於有的接口最多隻有兩層 /xx/xx,所以查找出所有接口截取最後兩層進行統計
grep -Er 'ur[il]\s*\=|url:|action\=' ./src | awk -F "[\'\`]" '{print $2}'|  grep -Eo "(\/[^\/]+){2}$" |sort | uniq -c | sort -r
這樣的話有幾個問題:
1、yy/xx/xx 和 xx/xx/xx會被統計成一個
2、/xx/xx的GET請求 和 /xx/xx的POST或者其他請求也會統計成一個

第二階段:
爲了獲得更準確一點的統計,先通過jscodeshift對項目裏各種寫法的請求進行規整組成完整url
判斷了項目中的各種請求寫法
jscodeshift庫: https://github.com/facebook/jscodeshift
ast語法樹在線解析:https://astexplorer.net/

    const glob = require("glob");
    const fs = require('fs');
    const j = require('jscodeshift');
    const util = require('util');
    const fetch = require('node-fetch');
    const execFile = util.promisify(require('child_process').execFile);

    // 解析環境變量url
    const getSystemEnvs = (fileSource) => {
        const source = j(fileSource);
        let envsObj = {};
        source.find(j.Property, { key: { name: 'defineInProcessEnv' } }).find(j.Property).forEach(f => {
            let key = f.node.key.name;
            let value = '';
            let valueMatch = f.node.value.arguments[0];
            if (valueMatch.type === 'LogicalExpression') {
                value = valueMatch.right.value;
            } else if (valueMatch.type === 'Literal') {
                value = valueMatch.value;
            } else {
                value = '';
            }
            envsObj = Object.assign({}, envsObj, { [key]: value });
        })
        return envsObj;
    }
    // 規整url接口
    const transformApis = (fileSource, envsMap) => {
        const source = j(fileSource);
        let globalBaseUrl = ''; // 全局的前綴
        // 獲取頁面裏定義的全局baseUrl或baseUri前綴變量
        const parseGlobalBaseUrl = source.find(j.VariableDeclaration).find(j.VariableDeclarator, {
            id: {
                type: 'Identifier',
            },
            init: {
                type: 'Literal'
            }
        }).filter((f) => /baseUrl|baseUri|hostStart/i.test(f.node.id.name));
        if (parseGlobalBaseUrl.length) {
            globalBaseUrl = parseGlobalBaseUrl.get().node.init.value;
        }
        // 判斷是否有方法封裝的
        // const baseUrl = '/wms/internal/xxx/';
        // const sendPostRequestApi = (argObj) => sendPostRequest(argObj, baseUrl);
        const hasFnReWrite = source.find(j.VariableDeclaration).find(j.VariableDeclarator, (f) => f.id.name === 'sendPostRequestApi' && f.init?.body?.callee?.name === 'sendPostRequest' && f.init?.body?.arguments[1]?.name === 'baseUrl').length;
        source.find(j.VariableDeclaration).find(j.VariableDeclarator, {
            init: {
                type: 'ArrowFunctionExpression'
            }
        })
          .forEach((b) => {
              let method = ''; // 請求方法
              let preUrl = ''; // 原來的url地址
              let prefix = ''; // url前綴
              // 如果是調用表達式類型
              if (b.node?.init?.body?.type === 'CallExpression') {
                  // 解析請求調用方法類型 排除tms的
                  const parseMethod = b.node?.init?.body?.callee?.name?.match(/Internal|Post|Get(?=Request)|Put|sendRequest/ig);
                  if (parseMethod) {
                      method = parseMethod[0];
                      // 如果是sendInternalPostRequest internalSendPostRequest 取全局baseUrl|baseUri 前綴是/wms/internal/front
                      if (parseMethod[0] === 'Internal') {
                          prefix = globalBaseUrl || '/wms/internal/front';
                          method = 'POST';
                      } else if (parseMethod[0] === 'sendRequest') {
                          // 如果是sendRequest類型 前綴都是wms/qc
                          prefix = '/wms/qc';
                          method = 'POST';
                      }
                  } else {
                      return false;
                  }
                  // 解析環境變量 有可能process.env.WWS_XXX、''、/wms/xxx/xxx、baseUrl、baseUri、或者沒有
                  // 判斷請求方法調用的第二個參數類型 有可能沒有傳參
                  // console.info('--', b.node.init.body.arguments[1])
                  const secondArgumentsType = b.node.init.body.arguments[1] ? b.node.init.body.arguments[1].type : '';
                  if (secondArgumentsType) {
                      if (secondArgumentsType === 'MemberExpression' || secondArgumentsType === 'ConditionalExpression') {
                          // process.env.XXX_XXX或者MOCK_SWITCH ? MOCK_URL + MOCK_HOST +process.env.XXX_XXX : process.env.XXX_XXX類型
                          const parseEnvs = j(b).find(j.MemberExpression, (d) => d.property.name !== 'env');
                          prefix = parseEnvs.length ? `${parseEnvs.get().node.property.name}` : '';
                          prefix = envsMap[prefix];
                      } else if (secondArgumentsType === 'Literal') {
                          // 字符串類型 ''、/wms/xxx/xxx
                          prefix = b.node.init.body.arguments[1].value;
                      } else if (secondArgumentsType === 'Identifier') {
                          // 變量類型 param、baseUrl、baseUri hostStart
                          const varName = b.node.init.body.arguments[1].name;
                          if (/baseUrl|baseUri|hostStart/i.test(varName)) {
                              prefix = globalBaseUrl;
                          } else if (varName === 'param') {
                              // 這種寫法的經查詢都是Internal類型的前綴
                              prefix = '/wms/internal/front';
                          }
                      }
                  } else {
                      // 沒有傳路徑變量的 默認是/wms/front 如果有方法封裝的話則爲全局的url前綴
                      prefix = (hasFnReWrite ? globalBaseUrl : prefix) || '/wms/front';
                  }
                  // 獲取原url和參數裏baseUrl
                  const firstArgumentsType = b.node.init.body.arguments[0] ? b.node.init.body.arguments[0].type : '';
                  if (firstArgumentsType === 'ObjectExpression') {
                      j(b).find(j.Property, { value: { type: 'Literal' } }).forEach((c) => {
                          if (c.node.key.name === 'url') {
                              preUrl = c.node.value.value;
                          } else if (c.node.key.name === 'baseUrl') {
                              prefix = c.node.value.value;
                          }
                      });
                      j(b).find(j.Property, { key: { name: 'url' } }).forEach((e) => {
                          e.node.value.value = `${prefix}${preUrl}_${method.toUpperCase()}`;
                      });
                  } else if (firstArgumentsType === 'Literal') {
                      // 這種寫法的經查詢都是Internal類型的前綴
                      preUrl = b.node.init.body.arguments[0].value || '';
                      // 構造成一個{url:'xxx'}對象節點後面好匹配
                      j(b).find('Literal').replaceWith(j.objectExpression([
                          j.property(
                            'init',
                            j.identifier('url'),
                            j.literal(`${prefix}${preUrl}_${method.toUpperCase()}`),
                          ),
                      ]));
                  }
              } else if (b.node?.init?.body?.type === 'BlockStatement') {
                  // 如果是個代碼塊
                  const findHasUrlOrUri = j(b).find(j.VariableDeclarator, (j) => /url|uri/i.test(j.id.name));
                  findHasUrlOrUri.find(j.TemplateLiteral).forEach((f) => {
                      // 如果url uri是一個模板字符串說明路徑可能含有環境變量或者其他路徑變量
                      const parseEnvs = j(f).find(j.MemberExpression, (d) => d.property.name !== 'env')
                      // 沒有環境變量的尾部路徑
                      let urlTail;
                      const parseUrlTail = j(f).find(j.TemplateElement, (k) => {
                          urlTail = k.value.cooked;
                          return urlTail !== '';
                      });
                      if (parseEnvs.length) {
                          if (urlTail) {
                              findHasUrlOrUri.find(j.TemplateLiteral).replaceWith(j.identifier(`'${envsMap[parseEnvs.get().node.property.name]}${urlTail}_POST'`));
                          }
                      }
                      j(f).find(j.Identifier, ((m) => {
                          if (/baseUrl|baseUri/i.test(m.name) && urlTail) {
                              findHasUrlOrUri.find(j.TemplateLiteral).replaceWith(j.identifier(`'${globalBaseUrl}${urlTail}_POST'`));
                          }
                      }));
                      // 如果沒有變量只是因爲用了反引號
                      findHasUrlOrUri.find(j.TemplateLiteral).replaceWith(j.identifier(`'${urlTail}_POST'`));
                  });
                  // 如果方法塊裏的url|uri是字符串只是在後面加個請求類型
                  findHasUrlOrUri.find(j.Literal).forEach((f) => {
                      f.node.value = `${f.node.value}_POST`;
                  });
              }
          });
        // 操作記錄的url規整
        source.find(j.JSXAttribute, {name: {name: 'url'}}).forEach(f => {
            if (f.node.value.type === 'Literal') {
                f.node.value.value = `${f.node.value.value}_POST`
            }
        })
        // upload請求url規整
        source.find(j.JSXAttribute, {name: {name: 'action'}}).forEach(f => {
            if (f.node?.value?.type === 'Literal') {
                j(f.node).find(j.Literal).replaceWith(j.literal(`${f.node.value.value}_POST`))
            }
            if (f.node?.value?.type === 'JSXExpressionContainer') {
                const jsxExpressionContainer = j(f).find(j.JSXExpressionContainer)
                if (f.node?.value?.expression?.name === 'uploadUrl') {
                    jsxExpressionContainer && jsxExpressionContainer.replaceWith(j.literal(`${globalBaseUrl}_POST`))
                }
                if (j(f).find(j.TemplateElement).length > 1) {
                    const parseEnvs = j(f).find(j.MemberExpression, (d) => d.property.name !== 'env')
                    let prefix = '';
                    let urlTail = '';
                    if (parseEnvs.length) {
                        prefix = parseEnvs.get().node.property.name
                        prefix = envsMap[prefix];
                    }
                    const parseUrlTail = j(f).find(j.TemplateElement, (k) => {
                        urlTail = k.value.cooked
                        return urlTail !== '';
                    })
                    jsxExpressionContainer && jsxExpressionContainer.replaceWith(j.literal(`${prefix}${urlTail}_POST`))
                }
            }
        })
        return source.toSource({
            quote: 'single',
        });
    };
    const projectDir = process.argv.slice(2)[0];
    const project = projectDir.match(/\/([^\/]+)$/)[1];
    const legorcSource = fs.readFileSync(`${projectDir}/.legorc.ts`);
    const envsMap = getSystemEnvs(legorcSource.toString()) || {};
    // 遍歷文件規整url接口
    glob(`${projectDir}/src/**/*.js{,x}`, function (er, files) {
        for (let i = 0; i < files.length; i += 1) {
            const uri = files[i];
            const fileSource = fs.readFileSync(uri);
            const newData = transformApis(fileSource.toString(), envsMap);
            if (newData) {
                fs.writeFileSync(uri, newData);
            }
        }
        // 規整完成調用shell進行統計和羣消息通知
        execFile('bash', ['./api_analyse.sh', `${projectDir}/src`], (error, stdout, stderr) => {
            if (error) {
                return console.error(error);
            }
            const content = `# ${project}項目中接口重複使用>=5次的統計
* 時間:${new Date().toLocaleString()}
* ${stdout}`;
            // console.info(content)
            
        })
    })

通過下面的sh會調用上面的js進行url規整,當上面的js運行完畢會調用下面的sh進行統計

#! /bin/bash
sys_arr=('/tmp/xxx' '/tmp/yyy');
if [ -z $1 ]
  then
    for s in "${sys_arr[@]}"; do
     if [ -d $s ];then
       cd $s;
       git reset --hard;
       git checkout master;
       git pull;
       cd /www;
       node 'api_analyse.js' $s;
     fi;
    done;
  else
    grep -Er 'ur[il]\s*\=|url:|action\=' "$1" | awk -F [\'\`\"] '{print $2}' | grep -E '.+_(POST|GET|PUT)' |sort | uniq -c | sort -r | awk '$1>=5{print $0}'
fi;

  

結果預覽:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章