友情链接
- 说一说从 URL 输入到页面呈现到底发生了什么?
- 平安金服面试官:从 new 一个 Vue 对象开始...
前言
谈到 babel
肯定大家都不会感觉陌生。
- 桌面端组件库 Element ,借助
babel-plugin-component
,我们可以只引入需要的组件,以达到减小项目体积的目的。 - 使用
babel-polyfill
,开发者可以立即使用 ES 规范中的最新特性。 - 有了插件:
transform-vue-jsx
、react
,我们在 vue 和 react 开发中可以直接使用 JSX 编写模板。
组件能按需引入到底是怎么实现的? Babel
的工作原理是怎样的呢?
带着疑问,我们尝试对其原理深入探索和理解。
Babel 编译的三个阶段
Babel
是一个 JavaScript
编译器。
和大多数其他语言的编译器相似,Babel
的编译过程可分为三个阶段:
- 解析
Parse
:将代码字符串解析成抽象语法树(AST
)。简单来说就是对JS
代码进行词法分析与语法分析。 - 转换
Transform
:对抽象语法树进行转换操作。这里操作主要是添加、更新及移除。 - 生成
Generate
: 根据变换后的抽象语法树再生成代码字符串。
解析 Parse
Babel
会把源代码抽象出来,变成 AST
。
可以看看 var answer = 6 * 7;
抽象之后的结果。
{
"type": "Program", // 根结点
"body": [
{
"type": "VariableDeclaration", // 变量声明
"declarations": [
{
"type": "VariableDeclarator", // 变量声明器
"id": {
"type": "Identifier",
"name": "answer"
},
"init": {
"type": "BinaryExpression", // 表达式
"operator": "*", // 操作符是 *
"left": {
"type": "Literal", // 字面量
"value": 6,
"raw": "6"
},
"right": {
"type": "Literal",
"value": 7,
"raw": "7"
}
}
}
],
"kind": "var"
}
],
"sourceType": "script"
}
Program
、 VariableDeclaration
、 VariableDeclarator
、 Identifier
、 BinaryExpression
、 Literal
均为节点类型。每个节点都是一个有意义的语法单元。这些节点通过携带的属性描述自己的作用。
其中的所有节点名词,均来源于 ECMA 规范 。
ATS 生成过程分为两个步骤:
- 分词:将代码字符串分割成语法单元数组
token
。 - 语法分析:分析语法单元之间的关联关系。
分词
JS
中的语法单元主要包括以下这么几种:
- 关键字:
const
、let
、var
等。 - 标识符:
if/else
、return
、function
等。 - 运算符:
+
、-
、*
、/
等。 - 数字
- 空格
- 注释
比如下面的代码生成的语法单元数组:
var answer = 6 * 7;
// Tokens
[
{
"type": "Keyword",
"value": "var"
},
{
"type": "Identifier",
"value": "answer"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "Numeric",
"value": "6"
},
{
"type": "Punctuator",
"value": "*"
},
{
"type": "Numeric",
"value": "7"
},
{
"type": "Punctuator",
"value": ";"
}
]
分词的大致思路:遍历字符串,通过各种方式(如:正则)匹配当前字符串片段对应的语法单元类型,然后生成数组 token
。
语法分析
先了解语法分析的两个概念:
- 语句:指一个具备边界的代码区域,相邻的两个语句之间从语法上来讲互不影响,即使调换顺序也不会产生语法错误。
- 表达式:指最终有个结果的一小段代码,它可以嵌入到另一个表达式,且包含在语句中。
语法分析就是识别语句和表达式,这是一个递归的过程(理解为深度优先遍历)。Babel
会在解析过程中设置一个暂存器,用来暂存当前读取到的语法单元,如果解析失败,就会返回之前的暂存点,再按照另一种方式进行解析,如果解析成功,则将暂存点销毁,不断重复以上操作,直到最后生成对应的语法树。
转换 Transform
Plugins
插件应用于 Babel
的转译过程。如果不使用任何插件,那么 Babel
会原样输出代码。
Presets
Babel
官方已经针对常用环境编写了一些 preset
:
- @babel/preset-env
- @babel/preset-flow
- @babel/preset-react
- @babel/preset-typescript
Preset
的路径:
如果 preset
在 npm
上,你可以输入 preset
的名称,Babel
将检查是否已经将其安装到 node_modules
目录下了
{
"presets": ["babel-preset-myPreset"]
}
你还可以指定指向 preset
的绝对或相对路径。
{
"presets": ["./myProject/myPreset"]
}
Preset
的排列顺序:
Preset
是逆序排列的(从后往前)。
{
"presets": [
"a",
"b",
"c"
]
}
将按如下顺序执行: c
、b
然后是 a
。
这主要是为了确保向后兼容,由于大多数用户将 es2015
放在 stage-0
之前。
生成 Generate
用 babel-generator
通过 AST
树生成 ES5
代码。
实现一个简单的按需打包功能
例如 ElementUI
中把 import { Button } from 'element-ui'
转成 import Button from 'element-ui/lib/button'
可以先对比下 AST
:
// import { Button } from 'element-ui'
{
"type": "Program",
"body": [
{
"type": "ImportDeclaration",
"specifiers": [
{
"type": "ImportSpecifier",
"local": {
"type": "Identifier",
"name": "Button"
},
"imported": {
"type": "Identifier",
"name": "Button"
}
}
],
"source": {
"type": "Literal",
"value": "element-ui",
"raw": "'element-ui'"
}
}
],
"sourceType": "module"
}
// import Button from 'element-ui/lib/button'
{
"type": "Program",
"body": [
{
"type": "ImportDeclaration",
"specifiers": [
{
"type": "ImportDefaultSpecifier",
"local": {
"type": "Identifier",
"name": "Button"
}
}
],
"source": {
"type": "Literal",
"value": "element-ui/lib/button",
"raw": "'element-ui/lib/button'"
}
}
],
"sourceType": "module"
}
可以发现, specifiers
的 type
和 source
的 value、raw
不同。
然后 ElementUI
官方文档中,babel-plugin-component
的配置如下:
// 如果 plugins 名称的前缀为 'babel-plugin-',你可以省略 'babel-plugin-' 部分
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
直接干:
import * as babel from '@babel/core'
const str = `import { Button } from 'element-ui'`
const { result } = babel.transform(str, {
plugins: [
function({types: t}) {
return {
visitor: {
ImportDeclaration(path, { opts }) {
const { node: { specifiers, source } } = path
// 比较 source 的 value 值 与配置文件中的库名称
if (source.value === opts.libraryName) {
const arr = specifiers.map(specifier => (
t.importDeclaration(
[t.ImportDefaultSpecifier(specifier.local)],
// 拼接详细路径
t.stringLiteral(`${source.value}/lib/${specifier.local.name}`)
)
))
path.replaceWithMultiple(arr)
}
}
}
}
}
]
})
console.log(result) // import Button from "element-ui/lib/Button";
完美!我们的第一个 Babel
插件完成了。
大家有没有对 Babel
有自己的理解了呢?
感谢
如果本文对你有帮助,就点个赞支持下吧!感谢阅读。