文章代码的源码仓库javascript
AST(Abstract Syntax Tree)既抽象语法树,或称语法树,简单来讲就是代码语法结构的一种抽象表示。好比 var answer = 6 * 7;
会被解析为这么一棵树 html
那么代码怎样才能解析成这一棵 AST, AST在前端领域通常又能够干吗?前端
ast是由编译器解析生成的,简单的编译器能够由如下几部分组成:java
tokens
咱们前端构建中很经常使用的babel就是这种原理node
babel 初始阶段并无作任何事,基本上等于 const babel = code=> code; 先 tokenizer, parser 解析代码,再 transformer 的时候,彻底不改动原来的 astjquery
对编译器原理有兴趣的,能够看我之前写的小demo,500行简单易懂 min-compiler,看完会有个总体概念。webpack
而生成的AST咱们能够用来作什么?git
AST你都拿到了,剩下的事情就是对这棵树作你想要的操做,好比代码转换(babel),代码压缩等。github
这里我用他来处理webpack的alias泛滥问题。web
webpack alias 在不少状况下能够提供便利,可是若是项目参加的人太多,又没有什么约束,你们贪图方便什么都加到alias....就会变成这样子
咱们先来整理一下思路
咱们这里的把alias改成其余值,指的是这种状况
目录结构:
- src
- components
- btn
alias: {
btn: path.resolve(basepath, 'src/components/btn'),
btn: path.resolve(basepath, 'src/components'),
}
原来的引入 import Btn from 'btn';
改成 import Btn from 'components/btn';
复制代码
这里咱们用 esprima 来作代码分析生成ast,用 estraverse 来转换代码,用 escodegen 生成代码。直接上代码
const aliasConfig = { /* webpack alias 配置*/}
function translateAlias(filePath) {
// 解析ast
const codeStr = fs.readFileSync(filePath).toString();
const ast = esprima.parseModule(codeStr);
// 转换ast
estraverse.traverse(ast, {
// 对于每一个node节点都会进入这个函数
enter(node, parent) {
// 判断是不是咱们的目标文件
const isAliasDec = isRequireDeclaration(node, parent);
if (isAliasDec) {
// 替换掉alias => newAlias
const newVal = getModulePath(node.value, filePath);
node.value = newVal;
}
},
});
// 从新生成代码
const newCodeStr = escodegen.generate(ast);
fs.writeFileSync(filePath, newCodeStr, {});
}
// 工具函数: 判断是不是 require
function isRequireDeclaration(node, parent) {
const { type, value } = node;
const { callee } = parent || {};
// 类型一致 && 该key在aliasKey中 && 是 require引入的
return (
type === 'Literal' &&
aliasKey.includes(value) &&
!allowAliasKey.includes(value) &&
isRequest(callee)
);
}
// 工具函数:获取路径
function getModulePath(aliasKey, filePath) {
const firstDir = /\w*/.exec(aliasKey)[0];
const modulePath = aliasKey.replace(firstDir, aliasConfig[firstDir]);
const aliasPath = aliasKey.replace(firstDir, aliasMap[firstDir]);
if (!aliasConfig[firstDir] || !aliasMap[firstDir] || allowAliasKey.includes(firstDir)) return false;
// 获取引入的模块与当前模块相对路径,判断是否太长,是就返回alias,不然就返回相对路径就完事了
const relativePath = path.relative(filePath, modulePath);
const relativeTime = relativePath.split('../').length - 1;
return (relativeTime < MAX_RELATIVE)? relativePath: aliasPath;
}
translateAlias(filePath);
复制代码
这显然是不行的,先不说格式的问题,一个文件连换行和注释都没有,那他就是没有灵魂的js~
看了下这是由于 esprima
在解析的时候,遇到空行和注释会直接跳过不解析生成AST,因此会致使后面生成的代码没有空行和注释。
咱们平时项目上用的最多的转换代码的工具就是babel,那么咱们也能够把 esTool
那一套换成 babel
生态,用babel来帮咱们作这些转换。
原理和思路基本上是同样的,用 babylon
解析,babel-traverse
转换,再用babel-generator
生成代码。 生成以后,先不写进去,而是用 prettier
格式化一遍再重写到本地,以保持和原来的风格一致。
function translateAlias(filePath) {
console.log(`开始处理第${i++}个: ${filePath}`)
const code = fs.readFileSync(filePath).toString();
// 获取ast
const ast = babylon.parse(code, {
sourceType: 'module',
plugins: ['jsx', 'objectRestSpread']
});
traverse(ast, {
enter(path) {
// 转换 CommonJs 的状况
translateRequireModulePath(path, filePath);
// 转换 ESM 的状况
translateImportModulePath(path, filePath);
}
});
const newCode = generate(ast, {});
// 从新用项目的prettier配置格式化多一次再写入
const prettierCode = prettier.format(newCode.code, prettierConfig);
fs.writeFileSync(filePath, prettierCode);
console.log(`处理结束${filePath}`)
}
复制代码
到此减小webpack-alias的功能处理完成,最后总结一下
glob
读取全部要转的js文件babylon
将js文件解析成ASTbabel-traverse
处理AST,判断若是是 require('xxx')
或者import xxx from 'xxx'
替换掉这些路径babel-generator
将新生成的AST转化为代码prettier
格式化新生成的代码,保持与原项目风格一致最后写的时候参考到的连接,大部分是类库的文档 迷你编译器