本文为稀土技能社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!
前语
2022了,咱们做的面向C端的产品(Web,小程序,其它跨端计划),触及JS产品的仍是避不开兼容性的话题(即便IE已官宣中止支撑)
但就现在看来这个中止保护仍是避免不了咱们做开发仍是要考虑兼容低端机,甚至IE11
针对js现在通常的手法都是经过东西对js进行语法降级至 ES5,同时引入对应的 polyfill(垫片)
东西首选仍是老牌 Babel,当然现在还有 SWC 这个冉冉升起的新星
经过一顿操作为项目装备 Babel 之后,为了保证产品不出现 ES5 之外的语法,通常都会搭配一个 Check 东西去检测产品是否契合要求
本文将论述市面上已有东西的完成原理
,功用比照
,终究完成增强型的es-check
,提供 CLI 和 Lib 两种运用办法
下面先别离介绍一下社区版的es-check和滴滴版的@mpxjs/es-check完成原理,终究再完成一个集大成者
es-check
先看一下其作用,下面是用于测验的代码
// test.js
var str = 'hello'
var str2 = 'world'
const varConst = 'const'
let varLet = 'let'
const arrFun = () => {
console.log('hello world');
}
npx es-check es5 testProject/**/*.js
能够看到其报错信息比较简略,只输出了代码中的第一个ES语法问题const
,然后对应的是行数和具体文件路径
咱们再把这个测验文件构建压缩混淆一下
(模仿build产品)
npx tsup __test__/testProject/js/index.js --sourcemap -d __test__/testProject/dist --minify
经过成果,能够看到,只说有解析问题,并未奉告是什么问题,然后有对应的队伍数
如果有sourcemap
那么咱们暂时是能够经过source-map这个库解析一下,以上面的报错为例
// npx esno source-map.ts
import sourceMap from 'source-map'
import fs from 'fs'
import path from 'path'
const file = path.join(__dirname, 'testProject/dist/index.js.map')
const lineNumber = 1
const columnNumber = 45
;(async () => {
const consumer = await new sourceMap.SourceMapConsumer(
fs.readFileSync(file, 'utf-8')
)
const sm = consumer.originalPositionFor({
column: columnNumber,
line: lineNumber
})
// 对应文件的源码
const content = consumer.sourceContentFor(sm.source!)
// 过错行的代码
const errCode = content?.split(/r?n/g)[sm.line! - 1]
console.log(errCode)
})()
履行成果如下,能够得到对应的过错代码
原理分析
打开源码能够看到完成非常简略,要害不过100行。能够总结为3过程
- 运用 fast-glob 获取方针文件
- 运用 acorn 解析源码生层AST,并捕获解析过错
- 判别是否存在解析过错,有就打印
acorn
是一个很常见的 js 解析库,能够用于AST的生成与CRUD操作,其包括1个 ecmaVersion
参数用于指定要解析的 ECMAScript
版别。es-check
正是利用了这个特性
import * as acorn from 'acorn'
try {
acorn.parse(`const a = 'hello'`, {
ecmaVersion: 5,
silent: true
// sourceType: 'module'
// allowHashBang:true
})
} catch (err) {
// The keyword 'const' is reserved (1:0)
console.log(err)
// err 除了继承惯例 Error 方针,包括 stack 和 message 等内容外,还包括如下信息
// {
// pos: 0,
// loc: Position { line: 1, column: 0 },
// raisedAt: 7
// }
}
下面是es-check
的精简完成,完好源码见 Github
// npx esno es-check.ts
import fg from 'fast-glob'
import path from 'path'
import * as acorn from 'acorn'
import fs from 'fs'
const testPattern = path.join(__dirname, 'testProject/**/*.js')
// 要查看的文件
const files = fg.sync(testPattern)
// acorn 解析装备
const acornOpts = {
ecmaVersion: 5,// 方针版别
silent: true
// sourceType: 'module'
// allowHashBang:true
}
// 过错
const errArr: any[] = []
// 遍历文件
files.forEach((file) => {
const code = fs.readFileSync(file, 'utf8')
try {
acorn.parse(code, acornOpts as any)
} catch (err: any) {
errArr.push({
err,
stack: err.stack,
file
})
}
})
// 打印过错信息
if (errArr.length > 0) {
console.error(
`ES-Check: there were ${errArr.length} ES version matching errors.`
)
errArr.forEach((o) => {
console.info(`
ES-Check Error:
----
erroring file: ${o.file}
error: ${o.err}
see the printed err.stack below for context
----n
${o.stack}
`)
})
process.exit(1)
}
console.info(`ES-Check: there were no ES version matching errors! `)
小结
- 只能检测源码中是否存在不契合对应ECMAScript版别的语法
- 只会反应出文件中第一个语法问题
- 过错信息只包括地点文件中的
队伍号
以及parser error
- 不支撑html
mpx-es-check
滴滴出品的 mpx (增强型跨端小程序结构)的配套东西 @mpxjs/es-check
咱们仍是用上面的例子先实测一下作用
# 1
npm i -g @mpxjs/es-check
# 2
mpx-es-check --ecma=6 testProject/**/*.js
能够看到其将过错信息输出到了1个log文件中
log日志信息如下,仍是很清晰的指出了有哪些过错并标明晰过错的具体位置,内置了source-map
解析。
下面来探求一下完成原理
原理分析
打开源码,从进口文件开端看,大体分为以下几步:
- 运用
glob
获取要检测方针文件 - 获取文件对应的
源码
和sourcemap
文件内容 - 运用@babel/parser解析生成AST
- 运用@babel/traverse遍历节点
- 将一切非ES5语法的节点规矩进行枚举,再遍历节点时,找出契合条件的节点
- 格局化输出信息
其中@babel/parser
与@babel/traverse
是babel
的中心构成部分。一个用于解析一个用于遍历
节点规矩示例如下,这个办法准确,就是费时吃力,需求将每个版别的特性都穷举出来
// 部分节点规矩
const partRule = {
// let and const
VariableDeclaration(node) {
if (node.kind === 'let' || node.kind === 'const') {
errArr.push({
node,
message: `Using ${node.kind} is not allowed`
})
}
},
// 箭头函数
ArrowFunctionExpression(node) {
errArr.push({
node,
message: 'Using ArrowFunction(箭头函数) is not allowed'
})
}
}
下面是遍历规矩与节点的逻辑
// 寄存一切节点
const nodeQueue = []
const code = fs.readFileSync(file, 'utf8')
// 生成AST
const ast = babelParser.parse(code, acornOpts)
// 遍历获取一切节点
babelTraverse(ast, {
enter(path) {
const { node } = path
nodeQueue.push({ node, path })
}
})
// 遍历每个节点,履行对应的规矩
nodeQueue.forEach(({ node, path }) => {
partRule[node.type]?.(node)
})
// 解析格局化过错
errArr.forEach((err) => {
// 省掉 sourcemap 解析过程
problems.push({
file,
message: err.message,
startLine: err.node.loc.start.line,
startColumn: err.node.loc.start.column
})
})
精简完成的运转成果如下,完好源码见Github
小结
- 检测输出的成果相对友好(比较理想的格局),内置了sourcemap解析逻辑
- 不支撑html
- 需求额外保护一套规矩(相对ECMAScript迭代频率来说,能够接受)
增强完成es-check
综上2个比照,从源码完成反应来看 es-check
的完成更简略,保护本钱也相对较低
@sugarat/es-check 也将基于es-check
做1个增强完成,弥补单文件屡次检测
,支撑HTML
、sourcemap解析
等才能
单文件屡次检测
现状:利用acorn.parse
直接对code
进行解析时分,将会直接抛出code
中的一处解析过错
,然后就结束了
那咱们只需求将code
拆成多个代码片段,那这个问题理论上就迎刃而解了
现在的问题就是怎么拆了?
咱们这直接简略暴力一点,对AST直接进行节点遍历,然后别离检测每个节点对应的代码是否合法
首要运用latest
版别生成这棵AST
const ast = acorn.parse(code, {
ecmaVersion: 'latest'
})
接下来运用acorn-walk进行遍历
import * as acornWalk from 'acorn-walk'
acornWalk.full(ast, (node, _state, _type) => {
// 节点对应的源码
const codeSnippet = code.slice(node.start, node.end)
try {
acorn.parse(codeSnippet, {
ecmaVersion,
})
} catch (error) {
// 在这儿输出过错片段和解析报错原因
console.log(codeSnippet)
console.log(error.message)
}
})
仍是以前面的测验代码为例,输出的过错信息如下
var str = 'hello'
var str2 = 'world'
const varConst = 'const'
let varLet = 'let'
const arrFun = () => {
console.log('hello world');
}
完好demo1代码
部分节点对应的片段可能不完好,会导致解析过错
用于测验的片段如下
const obj = {
'boolean': true,
}
这儿能够再parse
检测error
前再parse一次latest
用于排除语法过错,额外逻辑如下
let isValidCode = true
// 判别代码片段 是否合法
try {
acorn.parse(codeSnippet, {
ecmaVersion: 'latest'
})
} catch (_) {
isValidCode = false
}
// 不合法不处理
if (!isValidCode) {
return
}
完好demo2代码
此时输出的过错存在一些重复的情况,比方父节点包括子节点的问题代码
,这儿做一下过滤
const codeErrorList: any[] = []
acornWalk.full(ast, (node, _state, _type) => {
// 节点对应的源码
const codeSnippet = code.slice(node.start, node.end)
// 省掉重复代码。。。
try {
acorn.parse(codeSnippet, {
ecmaVersion: '5'
} as any)
} catch (error: any) {
// 与先存过错进行比较
const isRepeat = codeErrorList.find((e) => {
// 判别是否是包括关系
return e.start >= node.start && e.end <= node.end
})
if (!isRepeat) {
codeErrorList.push({
codeSnippet,
message: error.message,
start: node.start,
end: node.end
})
}
}
})
console.log(codeErrorList)
修正后成果如下
完好demo3代码
如有一些边界情况也是在 catch err
部分依据 message
做一下过滤即可
比方下代码
var { boolean:hello } = {}
完好demo4代码
做一下过滤,catch message
增加过滤逻辑
const filterMessage = [/^The keyword /]
if (filterMessage.find((r) => r.test(error.message))) {
return
}
调整后的报错信息就是解构赋值
的语法过错了
完好demo5代码
至此基本能完成了单文件的屡次es-check检测
,尽管不像mpx-es-check
那样用直白的言语直接说面是什么语法。但还有改进空间嘛,后面再单独写个文章做个东西检测方针代码用了哪些ES6+
特性。就不再这儿赘述了
sourcemap解析
这个首要针对检测资源是build产品
的一项优化,经过source-map
解析报错信息对应的源码
前面的代码咱们只获取了问题源码
的起止字符位置start
,end
经过source-map解析,首要要获取报错代码在资源中的队伍信息
这儿经过acorn.getLineInfo
办法可直接获取队伍信息
// 省掉了重复代码
const codeErrorList: any[] = []
acornWalk.full(ast, (node, _state, _type) => {
// 节点对应的源码
const codeSnippet = code.slice(node.start, node.end)
try {
acorn.parse(codeSnippet, {
ecmaVersion: '5'
} as any)
} catch (error) {
const locStart = acorn.getLineInfo(code, node.start)
const locEnd = acorn.getLineInfo(code, node.end)
codeErrorList.push({
loc: {
start: locStart,
end: locEnd
}
})
}
})
console.dir(codeErrorList, {
depth: 3
})
成果如下,完好demo1代码
有了队伍号,咱们就能够依据*.map
文件进行源码的解析
默认map
文件由原文件名加.map
后缀
function getSourcemapFileContent(file: string) {
const sourceMapFile = `${file}.map`
if (fs.existsSync(sourceMapFile)) {
return fs.readFileSync(sourceMapFile, 'utf-8')
}
return ''
}
解析map
文件直接运用 sourceMap.SourceMapConsumer
,返回的实例是1个Promise
,运用时需注意
function parseSourceMap(code: string) {
const consumer = new sourceMap.SourceMapConsumer(code)
return consumer
}
依据前面source-map
解析的例子,把这块逻辑放到checkCode
之后即可
const code = fs.readFileSync(file, 'utf-8')
// ps: checkCode 即为上一小节完成代码检测才能的封装
const codeErrorList = checkCode(code)
const sourceMapContent = getSourcemapFileContent(file)
if (sourceMapContent) {
const consumer = await parseSourceMap(sourceMapContent)
codeErrorList.forEach((v) => {
// 解析获取原文件信息
const smStart = consumer.originalPositionFor({
line: v.loc.start.line,
column: v.loc.start.column
})
const smEnd = consumer.originalPositionFor({
line: v.loc.end.line,
column: v.loc.end.column
})
// start对应源码地点行的代码
const sourceStartCode = consumer
.sourceContentFor(smStart.source!)
?.split(/r?n/g)[smStart.line! - 1]
const sourceEndCode = consumer
.sourceContentFor(smEnd.source!)
?.split(/r?n/g)[smEnd.line! - 1]
// 省掉 console 打印代码
})
}
完好demo2代码
这块就对齐了mpx-es-check
的source-map
解析才能
HTML支撑
这个就比较好办了,只需求将script
里的内容提取出来,调用上述的checkCode
办法,然后对成果进行一个队伍号的优化即可
这儿提取的办法很多,能够
正则匹配
- cheerio:像jQuery相同操作
- parse5:生成AST,递归遍历需求的节点
-
htmlparser2:生成AST,比较
parse5
愈加,解析战略愈加”包容“
小试比照了一下,终究发现是用parse5
更契合这个场景(编写代码更少)
import * as parse5 from 'parse5'
const htmlAST = parse5.parse(code, {
sourceCodeLocationInfo: true
})
下面是生成的AST示例: astexplorer.net/#/gist/0372…
经过nodeName
或许tagName
就能够区别节点类型,这儿简略写个遍历办法
节点能够经过childNodes
特点区别是否包括子节点
function traverse(ast: any, traverseSchema: Record<string, any>) {
traverseSchema?.[ast?.nodeName]?.(ast)
if (ast?.nodeName !== ast?.tagName) {
traverseSchema?.[ast?.tagName]?.(ast)
}
ast?.childNodes?.forEach((n) => {
traverse(n, traverseSchema)
})
}
这儿遍历一下demo代码生成的ast
traverse(htmlAST, {
script(node: any) {
const code = `${node.childNodes.map((n) => n.value)}`
const loc = node.sourceCodeLocation
if (code) {
console.log(code)
console.log(loc)
}
}
})
完好demo1代码
取得对应的源码后就能够调用之前的checkCode
办法,对过错行号做一个拼接即可得到过错信息
traverse(htmlAST, {
script(node: any) {
const code = `${node.childNodes.map((n) => n.value)}`
const loc = node.sourceCodeLocation
if (code) {
const errList = checkCode(code)
errList.forEach((err) => {
console.log(
'line:',
loc.startLine + err.loc.start.line - 1,
'column:',
err.loc.start.column
)
console.log(err.source)
console.log()
})
}
}
})
完好demo2代码
组建CLI才能
这儿就不再赘述CLI过程代码,中心的已在前面论述,这儿直接上终究成品的运用演示,参数同es-check
保持一致
npm i @sugarat/es-check -g
检测方针文件
escheck es5 testProject/**/*.js testProject/**/*.html
日志输出到文件
escheck es5 testProject/**/*.js testProject/**/*.html --out
终究比照
Name | JS | HTML | Friendly |
---|---|---|---|
es-check | ✅ | ❌ | ❌ |
@mpxjs/es-check | ✅ | ❌ | ✅ |
@sugarat/es-check | ✅ | ✅ | ✅ |
取了2者的优点相结合然后做了一定的增强
终究
当然这个东西可能存在bug,遗漏部分场景等情况,读者试用能够谈论区给反馈,或许库里直接提issues
有其它功用上的建议也可谈论区留言交流
完好源码移步=>Github
参阅
- es-check:社区出品
- mpx-es-check:滴滴出品 MPX 结构的配套东西