背景
由于出现过两次线上事故,是由于引入的第三方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);
}