【check-es6】对构建后的产物进行es6语法检测

背景

由于出现过两次线上事故,是由于引入的第三方npm包,里面还有es6语法,而webpack打包会忽略nodemodules的编译,会导致项目在低版本浏览器无法解析es6语法,出现项目崩溃,移动端页面白屏等重大事故。

于是开发了 check-es6 脚手架,并应用到所有移动端项目中,至少检测出10次问题,避免了许多线上事故。

解决思路

为了从根本上避免这种事故再次发生,我们最终讨论结果,对构建的产物进行关键字const,let,=>等字段检查,校验项目build之后的产物是否存在低版本浏览器不支持的es6属性.

我们的主要目标是检查构建产物是否含有const,let,=>等关键字段,最开始写了一个正则,

const regEs6 = /\b(let|const)\s+\$?\w*?\b|=>/;

这个基本能针对含let,const,=>的字段做出检测,但是区分不了字符串,并且检测范围有限。

so,最终还是通过AST,将编译后的产物解析成ast语法树,进行判断区分。

技术方案

一、如何执行

当部署项目代码时,npm run build 结束后,希望执行一个 npm run check:es6,即可对项目构建的js进行检测。

然后问题来了,部署项目那么多目录要针对哪个目录进行检测呢,所以还需要提供一个文件目录。

比如我的项目构建后的目录是wukong-pay。 所以我的package.json代码如下:

"scripts": {
  "check:es6": "./node_modules/@xes/check-es6/index.js check wukong-pay",
},

那么为什么执行check wukong-pay就可以执行了呢?

自己实现的脚手架原理如下:

/** bin/main.js **/

const { program } = require('commander');
const packageJson = require('../package.json')
const cwd = process.cwd(); // 项目执行路径
const {  checkAllJs  } = require('../lib/common'); // 检测当前目录下的是所有js,参数接收一个路径以及文件夹名

program.version(packageJson.version)

program
  .command('check <dirname>')
  .description("the dirname will be checked")
  .alias('ck')
  .action(async (argv) => {
    if (argv) {
      const filePath = `${cwd}/${argv}`
      checkAllJs(filePath, argv)
    } else {
      throw Error('【dirname】 为必传参数');
    }
  });

program.parse();

二、如何遍历文件下的所有js

第一步里面传入了绝对路径,然后就是遍历路径下的所有文件夹下的所有js(./*.js ./**/*.js)进行处理。

node库应有尽有,使用了globby,对js文件的遍历获取所有js文件的路径。

获取到路径数组后,还需要提取当前路径的文件,可以使用 fs 模块提取。

使用如下:

const globby = require('globby');
const fs = require('fs');

const checkAllJs = async (filepath, projectname) => {
  const paths = await globby(filepath, {
    expandDirectories: {
      extensions: ['js']
    }
  });

  (async () => {
    await paths.forEach(async filename => {
      let jsContent = fs.readFileSync(filename, 'utf-8').toString(); // 获取js文件内容

      // 对文件代码进行 ast 处理,判断语法类型
      const msg = await traverseAst(jsContent);
      saveEs6Qes(msg, filename);
  })
}

三、如何解析文件

先简单介绍下要使用的babel工具,

  • @babel/parser:babel解析器。可以帮助我们分析内部的语法,包括 es6,返回一个 AST 抽象语法树。

  • @babel/traverse:babel遍历器。可以用来遍历更新@babel/parser生成的AST,对语法树中特定的节点进行操作。

  • @babel/type:babel类型检查器。该模块包含手动构建 AST 和检查 AST 节点类型的方法。

所以接下来要做的工作就是,对文件代码进行ast解析,遍历ast,判断是否含有es6相关的语法,如果有,存储相关文件路径,以便追踪来源。

// ast遍历节点
const traverseAst = (jsContent) => {
  return new Promise((resolve, reject) => {
    try {
      const ast = parse(jsContent, {
        sourceType: 'module'
      })
      let errMsg = [];
      traverse(ast, {
        enter(path) {
          let msg = '';
          if (path.node.kind === 'const' || path.node.kind === 'let') {
            msg = path.node.kind;
          } else if (types.isAssignmentPattern(path.node)) {
            msg = '函数的默认参数';
          } else if (types.isRestElement(path.node)) {
            msg = '扩展(...)运算符';
          } else if (types.isSpreadElement(path.node)) {
            msg = '扩展(...)运算符';
          } else if (types.isArrayPattern(path.node) || types.isObjectPattern(path.node)) {
            msg = '解构赋值';
          } else if (types.isTemplateLiteral(path.node)) {
            msg = '模板字符串';
          } else if (types.isForOfStatement(path.node)) {
            msg = 'for...of循环';
          } else if (types.isArrowFunctionExpression(path.node)) {
            msg = '箭头函数';
          } else if (types.isYieldExpression(path.node)) {
            msg = 'Generator函数';
          } else if (types.isObjectMethod(path.node) && path.node.kind !== 'get') {
            msg = '对象属性的简洁表示法';
          } else if (types.isObjectProperty(path.node) && (path.node.key && path.node.key.type === 'BinaryExpression') ) {
            msg = '对象属性名使用表达式';
          } else if (types.isExportNamedDeclaration(path.node)) {
            msg = '模块的export命令';
          } else if (types.isImportDeclaration(path.node)) {
            msg = '模板的import命令';
          } else if (types.isClassDeclaration(path.node)) {
            msg = '类(class)';
          } else if (types.isCallExpression(path.node) && path.node.callee && path.node.callee.type === 'Identifier' && (path.node.callee.name === '__proto__') ) {
            msg = path.node.callee.name;
          } else if (types.isRegExpLiteral(path.node) && (path.node.flags === 'u') ) {
            msg = '正则表达式的u修饰符(例如 var regexp = /\\u{20BB7}/u;)';
          } else if (types.isRegExpLiteral(path.node) && (path.node.flags === 'y') ) {
            msg = '正则表达式的y修饰符(例如 /b/y)';
          } else if (types.isSuper(path.node)) {
            msg = '对象方法使用super';
          } else if (path.node.object && path.node.object.name === 'Uint32Array' && path.node.property && (path.node.property.name === 'from' || path.node.property.name === 'of')) {
            msg = '类型化数组的静态方法(例如 Uint32Array.from(), Uint32Array.of())';
          }
          if (!(errMsg.includes(msg) || msg === '')) {
            errMsg.push(msg);
          }
        }
      })
      resolve(errMsg);
    } catch (e) {
      reject(e);
    }
  })
}

四、记录含有es6的文件路径并打印出来

// 记录es6语法错误
const saveEs6Qes = (msg, filepath) => {
  if (Array.isArray(msg) && msg.length > 0) {
    if (errMap.has(filepath)) {
      const pathValue = errMap.get(filepath);
      if (pathValue.includes(msg)) return;
      errMap.get(filepath).push(msg);
    } else {
      errMap.set(filepath, [msg]);
    }
  }
}


if (errMap.get(filename) && errMap.get(filename).length > 0) {
    hasEs6 = true;
    console.log(`-------------------------------------------    Failed   -----------------------------------------------  \n`)
    console.log(`${projectname}项目下 ${filename} 文件含有未编译语法${errMap.get(filename)},请检查相关文件!\n`);
    console.log(`-------------------------------------------------------------------------------------------------------  \n`)
}
console.log(`${filename}  es6语法校验通过! \n`);

五、当存在es6语法时,添加报警处理

使用自动化部署时,有时候存在部署很慢,但是我们又不知道慢是因为网络慢,还是程序报错,所以,需要添加一个报警通知。

当存在es6语法问题时,发送群报警通知,并中断部署构建程序。

if (!hasEs6) {
    console.log(`-------------------------------------------    Success   ----------------------------------------------  \n`)
    console.log(`恭喜当前目录下所有js文件es6语法校验通过 \(^o^)/\n`);
    console.log(`-------------------------------------------------------------------------------------------------------  \n`)
  } else {
    let errStr = '';
    for (let [key, value] of errMap.entries()) {
      errStr += `${key}文件含有文件含有未编译语法${value}\n`
    }
    await sendData({
      "msgtype": "text",
      "text": {
          // "content": `${projectname}项目下 ${filename} 文件含有未编译语法${errMap.get(filename)},请检查相关文件!`
        "content": `${projectname}项目下${errStr}`
      }
    })

    // eslint-disable-next-line no-unreachable
      process.exit(1);
  }

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦