公司内部分享会的一个内容,其中借鉴了@zxg_神说要有光 大佬的小册《Babel 插件通关秘籍》中的一些内容,感兴趣的可以去看看小册,强烈推荐!
一、Babel 是啥?
Babel (发音[ˈbæbəl])最开始叫 6to5,顾名思义是 es6 转 es5,但是后来随着 es 标准的演进,有了 es7、es8 等, 6to5 的名字已经不合适了,所以改名为了 babel。
Babel 是一个广泛使用的 JavaScript 编译器,它可以将最新版本的 JavaScript 代码转换为向后兼容的代码,以便在不支持最新特性的旧浏览器和环境中运行。Babel 是一个开源项目,可以通过 npm 安装并在 Node.js 或浏览器中使用。
编译是啥?
编译是一种将高级语言转换成低级语言的技术,它需要进行词法分析、语法分析、语义分析等过程,最终生成抽象语法树(AST),并通过转译器或者编译器将AST转换成目标代码的字符串,解释执行或者生成机器码。
编译的难点在于如何保证语义的等价转换以及进行各种编译优化。此外,分词过程中需要使用有限状态机(DFA)来处理最小的单词格式,而组装过程中需要使用LL或LR算法,根据一两个单词或者组装结果来决定应该往下看几个单词,以此组装出正确的AST。
二、Babel的作用与使用场景
Babel的主要作用是将最新的ECMAScript标准的代码转换成浏览器可以识别的旧版本JavaScript。使用场景包括:
-
跨浏览器兼容性:让开发者能够使用最新的语法特性,而无需担心浏览器兼容性问题;
- react、vue等前端框架中将最新的ECMAScript标准的代码(ES6/ES7等)转换为浏览器可识别的ES5代码
-
语法扩展与代码转换:让开发者可以使用实验性的JavaScript特性,甚至可以创建自定义语法,开发一些转译工具
- 函数插桩(函数中自动插入一些代码,例如埋点代码)、自动国际化
- 开发类似Taro这种可转译多端的前端框架
-
优化代码:通过插件,Babel可以帮助优化代码,提高性能。
-
进行代码压缩、代码分离、代码优化(消除无用代码、重复代码,去除console打印)
-
Babel官方文档地址: Babel 是什么? · Babel 中文文档 | Babel中文网
三、Babel的核心原理
主要原理是将输入的 JavaScript 代码解析成一个 AST(抽象语法树)对象,然后对 AST 进行变换和操作,最后再将 AST 转换回 JavaScript 代码输出。
具体来说,Babel 的转换流程可以分为以下几个步骤:
-
解析(parse)
- 通过 parser 把源码转成抽象语法树(AST)
- 解析器将代码解析成一系列的语法节点,每个语法节点代表代码中的一个语法结构(如表达式、语句、函数等)。
-
转换(transform)
- 遍历 AST,调用各种 transform 插件对 AST 进行增删改
- 插件是一组函数,每个函数接收一个 AST 节点并返回一个新的 AST 节点。插件可以用来实现各种转换,例如将箭头函数转换为普通函数、将 ES6 模块转换为 CommonJS 模块等。
-
生成(generate)
-
把转换后的 AST 打印成目标代码,并生成 sourcemap。
-
代码生成器会遍历 AST 节点并输出相应的 JavaScript 代码。
-
在这个过程中,Babel 还会处理一些其他的任务,例如代码压缩、语法检查等。
扩展:AST 是个啥?
JavaScript AST(抽象语法树)是一种表示 JavaScript 代码结构的树状数据结构。它将 JavaScript 代码的每个语法单元(如表达式、语句、函数等)表示为一个节点,并使用树形结构将它们组织起来。
例如,以下是一个简单的 JavaScript 代码片段的 AST 表示:
function add(a, b) {return a + b;}function add(a, b) { return a + b; }function add(a, b) { return a + b; }
对应的 AST 如下所示:
Program└── FunctionDeclaration├── Identifier (name="add")├── FunctionExpression│ ├── Identifier (name="a")│ ├── Identifier (name="b")│ └── BinaryExpression│ ├── Identifier (name="a")│ ├── Identifier (name="b")│ └── Operator ("+")└── ReturnStatement└── BinaryExpression├── Identifier (name="a")├── Identifier (name="b")└── Operator ("+")Program └── FunctionDeclaration ├── Identifier (name="add") ├── FunctionExpression │ ├── Identifier (name="a") │ ├── Identifier (name="b") │ └── BinaryExpression │ ├── Identifier (name="a") │ ├── Identifier (name="b") │ └── Operator ("+") └── ReturnStatement └── BinaryExpression ├── Identifier (name="a") ├── Identifier (name="b") └── Operator ("+")Program └── FunctionDeclaration ├── Identifier (name="add") ├── FunctionExpression │ ├── Identifier (name="a") │ ├── Identifier (name="b") │ └── BinaryExpression │ ├── Identifier (name="a") │ ├── Identifier (name="b") │ └── Operator ("+") └── ReturnStatement └── BinaryExpression ├── Identifier (name="a") ├── Identifier (name="b") └── Operator ("+")
在这个 AST 中,顶层节点是Program
,表示整个 JavaScript 程序。
它有一个子节点FunctionDeclaration
,表示一个函数声明。
FunctionDeclaration
节点本身有三个子节点:函数名Identifier
节点、函数参数Identifier
节点和函数体FunctionExpression
节点。
函数体节点本身又有两个子节点:一个BinaryExpression
节点,表示函数体中的计算逻辑,以及一个ReturnStatement
节点,表示函数的返回语句。
重点工具: AST 在线可视化站点
AST(抽象语法树)格式有一些统一的规范。目前,JavaScript 社区普遍使用的是 ESTree 规范,该规范定义了 JavaScript AST 的基本结构和节点类型,是 JavaScript AST 格式的事实标准。
四、核心功能与在项目中的用法
以下是 Babel 的一些核心功能和用法演示:
- 安装 Babel
要安装 Babel,需要在命令行中运行以下命令:
npm install --save-dev @babel/core @babel/clinpm install --save-dev @babel/core @babel/clinpm install --save-dev @babel/core @babel/cli
这将安装 Babel 核心库和 CLI 命令行工具。安装完成后,可以使用 Babel 转换 JavaScript 代码。
- 配置 Babel
Babel 需要一个配置文件来确定希望转换哪些代码,以及如何进行转换。可以使用.babelrc
文件或在package.json
文件中添加 Babel 配置。以下是一个简单的.babelrc
文件:
{"presets": ["@babel/preset-env"]}{ "presets": [ "@babel/preset-env" ] }{ "presets": [ "@babel/preset-env" ] }
这个配置告诉 Babel 使用@babel/preset-env
预设来转换代码。这个预设可以根据目标浏览器和环境自动确定要使用的转换插件。预设可以理解成插件的集合。
- 转换代码
一旦设置了 Babel 配置文件,可以使用babel
命令行工具将的 JavaScript 代码转换为向后兼容的代码。例如,要转换一个名为index.js
的文件,可以运行以下命令:
npx babel index.js --out-file index-compiled.jsnpx babel index.js --out-file index-compiled.jsnpx babel index.js --out-file index-compiled.js
这将使用 Babel 将index.js
转换为向后兼容的代码,并将结果输出到index-compiled.js
文件中。
- 使用 Babel 插件
Babel 还提供了许多插件,可以使用它们来执行转换。例如,如果想使用 ES6 的箭头函数语法,可以使用@babel/plugin-transform-arrow-functions
插件。要安装此插件,可以运行以下命令:
npm install --save-dev @babel/plugin-transform-arrow-functionsnpm install --save-dev @babel/plugin-transform-arrow-functionsnpm install --save-dev @babel/plugin-transform-arrow-functions
然后,可以将插件添加到的 Babel 配置文件中:
{"plugins": ["@babel/plugin-transform-arrow-functions"]}{ "plugins": [ "@babel/plugin-transform-arrow-functions" ] }{ "plugins": [ "@babel/plugin-transform-arrow-functions" ] }
这将告诉 Babel 在转换代码时使用箭头函数插件。
总的来说,Babel 是一个非常有用的工具,可以帮助在现代 JavaScript 特性和向后兼容性之间找到平衡。它提供了许多插件和预设,使可以轻松地自定义的代码转换。
五、Babel 开发插件
- 安装 Babel 开发环境
在开始开发 Babel 插件之前,需要安装 Babel 的开发环境。可以通过运行以下命令来安装 Babel 的开发环境:
npm install --save-dev @babel/core @babel/cli @babel/parser @babel/traverse @babel/typesnpm install --save-dev @babel/core @babel/cli @babel/parser @babel/traverse @babel/typesnpm install --save-dev @babel/core @babel/cli @babel/parser @babel/traverse @babel/types
这将安装 Babel 的核心库和一些辅助库,以便可以处理 JavaScript AST(抽象语法树)并生成转换代码。
- 创建一个新的 Babel 插件
创建一个新的 Babel 插件很简单。可以创建一个新的 JavaScript 文件,并导出一个函数,该函数将被 Babel 调用并传递 AST 节点和一些选项。例如:
module.exports = function consolePlugin() {return {visitor: {// 插件的转换逻辑}};};module.exports = function consolePlugin() { return { visitor: { // 插件的转换逻辑 } }; };module.exports = function consolePlugin() { return { visitor: { // 插件的转换逻辑 } }; };
在这个例子中,我们导出一个函数,该函数返回一个对象,该对象具有一个名为visitor
的属性。该属性是一个对象,包含了一组访问器函数,这些函数将在处理 AST 时被调用。
- 实现插件的转换逻辑
在的插件中,需要实现访问器函数来处理 AST 节点并进行转换。访问器函数将接收以下参数:
path
:表示 AST 节点的路径,可以用来访问节点的属性和子节点。state
:表示插件的状态,可以用来存储和共享数据。
例如,以下是一个访问器函数,它将将所有console.jr()
语句替换console.log('自定义前缀/代码位置信息')
:
const types = require("@babel/types")const consolePlugin = ({ prefix = "jr:", showFilename = false } = {}) => {const t = typesreturn {visitor: {CallExpression(path,state) {const { callee, arguments: args } = path.node// 判断调用的是否是 console.jr 方法if (callee.type === "MemberExpression" &&callee.object.name === "console" &&callee.property.name === "jr") {// 构造新的 console.log 方法调用const newCallee = t.memberExpression(t.identifier("console"), t.identifier("log"))// 构造位置信息const loc = path.node.locconst line = loc.start.lineconst column = loc.start.columnconst filename = loc.filename ? loc.filename + ":" : ""// 添加自定义标识和位置信息const message = `${prefix} ${showFilename ? filename + line + ":" + column : ""}`const newArgs = [t.stringLiteral(message), ...args]// 替换原始的调用path.replaceWith(t.callExpression(newCallee, newArgs))// 记录已经转换过的位置if (state.opts && state.opts.transpiledPositions) {state.opts.transpiledPositions.push({filename,line,column,})}}},},}}module.exports = consolePluginconst types = require("@babel/types") const consolePlugin = ({ prefix = "jr:", showFilename = false } = {}) => { const t = types return { visitor: { CallExpression(path,state) { const { callee, arguments: args } = path.node // 判断调用的是否是 console.jr 方法 if ( callee.type === "MemberExpression" && callee.object.name === "console" && callee.property.name === "jr" ) { // 构造新的 console.log 方法调用 const newCallee = t.memberExpression(t.identifier("console"), t.identifier("log")) // 构造位置信息 const loc = path.node.loc const line = loc.start.line const column = loc.start.column const filename = loc.filename ? loc.filename + ":" : "" // 添加自定义标识和位置信息 const message = `${prefix} ${showFilename ? filename + line + ":" + column : ""}` const newArgs = [t.stringLiteral(message), ...args] // 替换原始的调用 path.replaceWith(t.callExpression(newCallee, newArgs)) // 记录已经转换过的位置 if (state.opts && state.opts.transpiledPositions) { state.opts.transpiledPositions.push({ filename, line, column, }) } } }, }, } } module.exports = consolePluginconst types = require("@babel/types") const consolePlugin = ({ prefix = "jr:", showFilename = false } = {}) => { const t = types return { visitor: { CallExpression(path,state) { const { callee, arguments: args } = path.node // 判断调用的是否是 console.jr 方法 if ( callee.type === "MemberExpression" && callee.object.name === "console" && callee.property.name === "jr" ) { // 构造新的 console.log 方法调用 const newCallee = t.memberExpression(t.identifier("console"), t.identifier("log")) // 构造位置信息 const loc = path.node.loc const line = loc.start.line const column = loc.start.column const filename = loc.filename ? loc.filename + ":" : "" // 添加自定义标识和位置信息 const message = `${prefix} ${showFilename ? filename + line + ":" + column : ""}` const newArgs = [t.stringLiteral(message), ...args] // 替换原始的调用 path.replaceWith(t.callExpression(newCallee, newArgs)) // 记录已经转换过的位置 if (state.opts && state.opts.transpiledPositions) { state.opts.transpiledPositions.push({ filename, line, column, }) } } }, }, } } module.exports = consolePlugin
在这个例子中,我们实现了一个名为CallExpression
的访问器函数,遍历 JavaScript 代码中的所有函数调用,当遇到 console.jr
方法时,就会将其转换为 console.log
方法,并在输出信息中添加自定义标识和位置信息。自定义标识由 prefix
参数指定,位置信息由 showFilename
参数控制是否显示文件名。
- 注册插件并进行测试
一旦实现了插件的转换逻辑,需要将其注册到 Babel 中。可以使用 Babel 的plugin
方法来注册插件。例如:
// 将源代码转换成新的代码const { code } = transformSync(sourceCode, {plugins: [consolePlugin],parserOpts: {sourceType: "unambiguous",},});// 将源代码转换成新的代码 const { code } = transformSync(sourceCode, { plugins: [consolePlugin], parserOpts: { sourceType: "unambiguous", }, });// 将源代码转换成新的代码 const { code } = transformSync(sourceCode, { plugins: [consolePlugin], parserOpts: { sourceType: "unambiguous", }, });
在这个例子中,我们将consolePlugin
插件注册到 Babel 中。然后,可以使用 Babel 来转换的 JavaScript 代码并测试的插件是否按预期工作。
总的来说,开发 Babel 插件是一个有趣且有用的任务,可以帮助扩展 Babel 并根据的需要自定义代码转换。
扩展
Vue 模版编译中的 AST 与 Babel 中的 AST 的区别
Vue 的 AST(抽象语法树)和 Babel 的 AST 在某些方面相似,但也有一些不同之处。
在 Vue 中,AST 是由 Vue 的模板编译器生成的,用于表示模板中的各种节点和语法。Vue 的 AST 包含了模板中的元素、属性、指令、事件等信息,以及它们的嵌套关系和父子关系。Vue 的 AST 还包含了一些特定的节点类型,例如 v-if、v-for、v-bind 等,这些节点类型是 Vue 特有的。
而在 Babel 中,AST 是由 Babel 编译器生成的,用于表示 JavaScript 代码中的各种节点和语法。Babel 的 AST 包含了 JavaScript 代码中的变量、函数、表达式、语句等信息,以及它们的嵌套关系和父子关系。Babel 的 AST 还包含了一些特定的节点类型,例如箭头函数、类声明、模板字符串等,这些节点类型是 ES6 和 ES7 的新语法。
此外,Babel 的 AST 还支持插件扩展和自定义,开发者可以编写自己的插件来扩展 Babel 的 AST 功能。Vue 的 AST 则没有这样的扩展机制。
总的来说,Vue 的 AST 和 Babel 的 AST 在某些方面相似,它们都是用于表示代码的抽象语法树。但在细节上有很大的不同,因为它们面向的是不同的语言和应用场景。
jsx 解析的 js 对象树和 vue 模版编译的 AST 的区别
JSX 解析的 JavaScript 对象树和 Vue 模板编译的 AST 在某些方面相似,但也有一些不同之处。
在 React 中,JSX 代码会被解析成一个 JavaScript 对象树,这个对象树通常称为虚拟 DOM(Virtual DOM)。虚拟 DOM 对象包含了 UI 元素的类型、属性、子元素等信息,以及它们的嵌套关系和父子关系。虚拟 DOM 对象通常由 React.createElement 函数创建。
而在 Vue 中,模板会被解析成 AST(抽象语法树),这个 AST 包含了模板中的元素、属性、指令、事件等信息,以及它们的嵌套关系和父子关系。Vue 的 AST 还包含了一些特定的节点类型,例如 v-if、v-for、v-bind 等,这些节点类型是 Vue 特有的。
从表面上看,虚拟 DOM 对象和 AST 都是用于表示 UI 元素的 JavaScript 对象树,它们都包含了元素的属性、子元素等信息,以及它们的嵌套关系和父子关系。但在细节上有很大的不同,因为它们面向的是不同的框架和应用场景。
虚拟 DOM 对象通常是 React 应用的核心,它是 React 通过比较新旧虚拟 DOM 对象来实现高效渲染的关键。而 Vue 的 AST 则是 Vue 编译器的核心,它是 Vue 将模板转换成渲染函数的关键。
总的来说,JSX 解析的 JavaScript 对象树和 Vue 模板编译的 AST 在某些方面相似,但在细节上还是有很大的不同。它们是不同框架和应用场景下的抽象语法树表示方式,主要用于实现框架的核心功能,例如高效渲染、组件化等。