- Bable 的重要性
- Babel 编译流程
- 如何开发 Babel 插件
- 四则运算编译器
- 参考资料
- 总结
- 打包工具:webpack、rollup、
- 浏览器兼容性:Babel、
- 性能:tree-shaking、
- 类型检查:typescript、
- 代码规范:eslint
- 安全: 压缩混淆
- 友好提示: 代码高亮
- 文档:docz
- sourcemap
- Babel 简介
- Babel AST 基础知识
- Babel API
- 扩展 Babel Parse
- 扩展 Babel traverse
- 扩展 Bable generate
引言: babel 是一个 JS、TS 的编译器,它能把新语法写的代码转换成目标环境支持的语法的代码,并且对目标环境不支持的 api 自动 polyfill。
- parse:通过词法分析和语法分析,把源码转成抽象语法树(AST)✨
- transform:遍历 AST,调用各种 transform 插件对 AST 进行增删改 ✨
- generate:把转换后的 AST 打印成目标代码,并生成 sourcemap
备注:AST 可视化查看工具
我们的程序就是由声明语句和语句或者表达式等等构成。
- 常见的 AST 节点:
- Literal: 字面量,const name = '小米' ,分为:StringLiteral、NumericLiteral、BooleanLiteral、RegExpLiteral
- Identifier:标识符,变量名、属性名、参数名等
- Statement:语句,它是可以独立执行的单位,比如 break、continue、debugger、return,特点:语句末尾一般会加一个分号分隔
- Declaration:声明语句是一种特殊的语句,它执行的逻辑是在作用域内声明一个变量、函数、class、import、export 等。
- Expression:expression 是表达式,特点是执行完以后有返回值,这是和语句 (statement) 的区别。
- es modules:
- import: ImportSpicifier、ImportDefaultSpecifier、ImportNamespaceSpcifier。
- export: ExportNamedDeclaration、ExportDefaultDeclaration、ExportAllDeclaration。
- Program: 最外层程序节点
- Directive:指令节点
- Comment: 注释节点
疑问:🤔️
- 为什么 AST 是一棵树 🌲?为什么我们写的代码节点都在 AST 最末位的节点?
- AST 节点如何自定义?
- 到底有哪些节点?@babel/types
-
@babel/parser : 对源码进行 parse,可以通过 plugins、sourceType 等来指定 parse 语法, 转化为 AST。
-
@babel/traverse: 通过 visitor 函数对遍历到的 ast 进行处理,分为 enter 和 exit 两个阶段,具体操作 AST 使用 path 的 api,还可以通过 state 来在遍历过程中传递一些数据
-
@babel/generator: 打印 AST 成目标代码字符串,支持 comments、minified、sourceMaps 等选项。
- @babel/types: 用于创建、判断 AST 节点,提供了 xxx、isXxx、assertXxx 的 api // 重点
- @babel/template: 用于批量创建节点
- @babel/code-frame: 打印错误信息,是否高亮、展示啥错误信息。
- @babel/core:完成整个编译流程,从源码到目标代码,生成 sourcemap。分为同步和异步 API 方法。
备注: 如果大家对 Babel API 有更深入的了解,可以查看=>Github:babel-handbook.
AST 也是有标准的,JS parser 的 AST 大多是 estree 标准,从 SpiderMonkey 的 AST 标准扩展而来。babel 的整个编译流程都是围绕 AST 来的,这一节我们来学一下 AST。
熟悉了 AST,也就是知道转译器和 JS 引擎是怎么理解代码的,对深入掌握 Javascript 也有很大的好处。
webpack 、rollup 底层都是基于acorn扩展的,如果想看懂 acorn 源码,并且明天它为什么那样写,需要了解编译原理,如果需要自定义语句和 AST 节点。参考如下
traverse(AST, {
Identifier(path, state) {}, // enter
StringLiteral: {
enter(path, state) {}, // enter
exit(path, state) {}, // exit
},
"FunctionDeclaration|VariableDeclaration"(path, state) {},
});
-
visitor:对象的 value 是对象或者函数(设计模式:访问者模式=>当被操作的对象结构比较稳定,而操作对象的逻辑经常变化的时候,通过分离逻辑和对象结构,使得他们能独立扩展): 1. 如果 value 为函数,那么就相当于是 enter 时调用的函数。 2. 如果 value 为对象,则可以明确指定 enter 或者 exit 时的处理函数。
-
path: path 是遍历过程中的路径,会保留上下文信息,有很多属性和方法.
path {
// 属性:
node // 重点
parent
parentPath
scope: { // 重点
bindings // 重点: 作用域中保存的是声明的变量和对应的值,每一个声明叫做一个binding(绑定)。
references // 重点
block: | BlockStatement
| CatchClause
| DoWhileStatement
| ForInStatement
| ForStatement
| FunctionDeclaration
| FunctionExpression
| Program
| ObjectMethod
| SwitchStatement
| WhileStatement
| ArrowFunctionExpression
| ClassExpression
| ClassDeclaration
| ForOfStatement
| ClassMethod
| ClassPrivateMethod
| StaticBlock
| TSModuleBlock
parent // 重点
parentBlock
path // 重点
dump()
parentBlock()
getAllBindings()
getBinding(name)
hasBinding(name)
getOwnBinding(name)
parentHasBinding(name)
removeBinding(name)
moveBindingTo(name, scope)
generateUid(name)
}
hub
container // 重点
key
listKey
// 方法
get(key)
set(key, node)
inList()
getSibling(key)
getNextSibling()
getPrevSibling()
getAllPrevSiblings()
getAllNextSiblings()
isXxx(opts)
assertXxx(opts)
find(callback)
findParent(callback)
insertBefore(nodes)
insertAfter(nodes)
replaceWith(replacement)
replaceWithMultiple(nodes)
replaceWithSourceString(replacement)
remove()
traverse(visitor, state)
skip()
stop()
}
- state:共享数据, AST 节点之间传递数据
state {
file
opts // 我们写插件传入的参数
}
generate 是把 AST 打印成字符串,是一个从根节点递归打印的过程,主要讲下 sourcemap, 首先看下格式:
{
version : 3,
file: "out.js",
sourceRoot : "",
sources: ["foo.js", "bar.js"],
names: ["src", "maps", "are", "fun"],
mappings: "AAgBC,SAAQ,CAAEA"
}
重点看 mappping 部分
mappings: "AAAAA,BBBBB;;;;CCCCC,DDDDD";
每一个分号 ;
表示一行,多个空行就是多个 ;
,mapping 通过 ,
分割。
mapping 有五位, 每一位是通过 VLQ 编码:
第一位是目标代码中的列数
第二位是源码所在的文件名
第三位是源码对应的行数
第四位是源码对应的列数
第五位是源码对应的 names,不一定有
参考:https://www.npmjs.com/package/source-map
babel plugin 有两种格式:
1. 返回对象的函数.
1. 返回对象。区别: 直接返回一个对象,不用函数包裹,这种方式用于不需要处理参数的情况。
// 直接返回对象函数
export default function(api, options, dirname) {
return {
// 继承某个插件
inherits: parentPlugin,
// manipulateOptions 用于修改 options,
manipulateOptions(options, parserOptions) {
options.lq = '';
},
pre(file) {
this.cache = new Map();
},
visitor: {
StringLiteral(path, state) {
this.cache.set(path.node.value, 1);
}
},
post(file) {
console.log(this.cache);
}
};
}
// 直接返回对象
export default plugin = {
pre(state) {
this.cache = new Map();
},
visitor: {
StringLiteral(path, state) {
this.cache.set(path.node.value, 1);
}
},
post(state) {
console.log(this.cache);
}
};
遗留:
1. 为什么AST是一棵树🌲?为什么我们写的代码节点都在AST最末位的节点? 1. parse:通过**词法分析**和**语法分析**,把源码转成抽象语法树(**AST**)?
前面说的只是 Babel 大概的一个编译原理,接下来我们通过看下四则运算搞明白这二个问题。
-
掘金:掘金-zxg_神说要有光 主要参考
-
书籍 📚:浙江大学-编译原理
-
Github:the-super-tiny-compiler-cn
-
Github:babel-handbook
- 学习设计模式:访问者模式。
- babel 插件机制和 preset 机制,都是为了修改 AST 节点。
- 作用域的灵活运用。
- 代码如何压缩、高亮、混淆。
- Acorn 插件机制和扩展强大。