/** 考虑到窝真的是一个很菜的选手,加上英语不太好文档看的很吃力,部分概念可能理解不对,所以如果您发现错误,请一定要告诉窝,拯救一个辣鸡(但很帅)的少年就靠您了!*/
Babel 是一个 JavaScript 的编译器。你可能知道 Babel 可以将最新版的 ES 语法转为 ES5,不过不只如此,它还可用于语法检查,编译,代码高亮,代码转换,优化,压缩等场景。
Babel7 为了区分之前的版本,所有的包名都改成了 @babel/... 格式。本文参考最新版文档。
Babel 的使用方式
- 单文件
<div id="output"></div>
<!-- 加载 Babel -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- 你的脚本代码 -->
<script type="text/babel">
// code...
</script>复制代码
- 命令行
npm install --save-dev @babel/core @babel/cli @babel/preset-env
npm install --save @babel/polyfill
复制代码
创建配置文件 babel.config.js
const presets = [
[
'@babel/env',
{
useBuiltIns: 'usage'
}
]
]
module.exports = { presets }复制代码
也可以使用 .babelrc
文件配置,两者好像没什么区别,不过 js
文件比 json
文件灵活,一些复杂的配置就只能使用 babel.config.js
了。
{
"presets": [
[
"@babel/env",
{
"useBuiltIns": "usage"
}
]
]
}
复制代码
其中 "useBuiltIns": "usage"
是预设插件组合 @babel/env
的选项,表示按需引入用到的 API,使用该选项要下载 @babel/polyfill
包。
创建源文件 src/index.js
let f = x => x;
let p = Promise.resolve(1);复制代码
然后在命令行运行命令 npx babel src/index.js
可以看到控制台打印出的编译后的代码:
"use strict";
require("core-js/modules/es6.promise");
var f = function f(x) {
return x;
};
var p = Promise.resolve(1);复制代码
也可以将编译结果保存到文件,运行命令 npx babel src/index.js --out-dir lib
可以将编译后的文件保存到 lib/index.js
- 构建工具的插件(webpack、Glup 等)
在 Webpack 中配置 babel-loader
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
]
}
复制代码
更多使用方法可见 使用 Babel
Babel 配置 presets 和 plugins
使用 Babel 时一般会设置 presets
和 plugins
,也可以同时设置。而 Presets
就是预设的一组 Babel 插件集合。
Babel 会先执行 plugins
再执行 presets
,其中 plugins
按指定顺序执行,presets
逆序执行。
babel-preset-es2015/es2016/es2017/latest & babel-preset-stage-x
设置预设的插件集合,来配置 babel 能转换的 ES 语法的级别,stage 表示语法提案的不同阶段。现在全部不推荐使用了,请一律使用 @babel/preset-env
。
@babel/preset-env
默认配置相当于 babel-preset-latest
,详细配置见 Env preset 。
举一个同时配置 plugins
和 presets
的例子:
配置文件 .babelrc
,可以写 react
语法和使用装饰器。装饰器还没有通过提案,浏览器一般也都不支持,需要使用 babel
进行转换。
{
"presets":[
"@babel/preset-react"
],
"plugins":[
[
"@babel/plugin-proposal-decorators",
{
"legacy":true
}
]
]
}
复制代码
然后写 index.js 文件
function createComponentWithHeader(WrappedComponent) {
class Component extends React.Component {
render() {
return (
<div>
<div>header</div>
<WrappedComponent />
</div>
);
}
}
return Component;
}
@createComponentWithHeader
class App extends React.Component {
render() {
return (
<div>hello react!</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('app')
);
复制代码
然后同上面一样进行编译,npx babel src/index.js --out-dir lib
就可以得到编译后文件了。
可以创建 index.html 打开页面查看效果。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
</head>
<body>
<div id="app"></div>
<script src="./lib/index.js"></script>
</body>
</html>
复制代码
基于环境配置 Babel
{
"presets": ["es2015"],
"plugins": [],
"env": {
"development": {
"plugins": [...]
},
"production": {
"plugins": [...]
}
}
}
复制代码
当前环境可以使用 process.env.BABEL_ENV
来获得。 如果 BABEL_ENV
不可用,将会替换成 NODE_ENV
,并且如果后者也没有设置,那么缺省值是"development"
。
Babel 相关工具
@babel/polyfill
Babel 在配置了上面的 babel-preset-env
之后,只能转换语法,而对于一些新的 API,如 Promise
,Map
等,并没有实现,仍然需要引入。
引入 @babel/polyfill
(可以通过 require("@babel/polyfill");
或 import "@babel/polyfill";
)会把这些 API 全部挂载到全局对象。缺点是会污染全局变量,同时如果只用到其中部分的话,会造成多余的引用。也可以在 @babel/preset-env
里通过设置 useBuiltIns
选项引入。
@babel/runtime & @babel/plugin-transform-runtime
@babel/runtime
和 @babel/polyfill
解决相同的问题,不过 @babel/runtime
是手动按需引用的。 不同于 @babel/polyfill
的挂载全局对象, @babel/runtime
是以模块化方式包含函数实现的包。
引入 babel-plugin-transform-runtime
包实现多次引用相同 API 只加载一次。
注意:对于类似 "foobar".includes("foo")
的实例方法是不生效的,如需使用则仍要引用 @babel/polyfill
。
@babel/cli
babel 的命令行工具,可以在命令行使用 Babel 编译文件,像前文演示的那样。
@babel/register
@babel/register
模块改写 require
命令,为它加上一个钩子。此后,每当使用 require
加载 .js
、.jsx
、.es
和 .es6
后缀名的文件,就会先用 Babel 进行转码。默认会忽略 node_modules
。具体配置可见 @babel/register 。
@babel/node
@babel/node
提供一个同 node 一样的命令行工具,不过它在运行代码之前会根据 Babel 配置进行编译。在 Babel7 中 @babel/node
不包含在 @babel/cli
中了。
@babel/core
babel 编译器的核心。可以通过直接调用 API 来对代码、文件或 AST 进行转换。
Babel 的处理阶段
解析(parse)
通过词法分析转为 token 流(可以理解为词法单元的数组),然后通过语法分析转为抽象语法树(Abstract Syntax Tree,AST)。
例如,下面的代码
n * n
复制代码
被转为转为 token 流:
[
{ type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
{ type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
{ type: { ... }, value: "n", start: 4, end: 5, loc: { ... } }
]复制代码
然后转为 AST。
{
"type":"BinaryExpression",
"start":0,
"end":5,
"left":{
"type":"Identifier",
"start":0,
"end":1,
"name":"n"
},
"operator":"*",
"right":{
"type":"Identifier",
"start":4,
"end":5,
"name":"n"
}
}复制代码
转换(transform)
Babel 将遍历 AST,插件就是作用于这个阶段,我们可以获取遍历 AST 过程中的一些信息并进行处理。
代码生成(generate)
通过处理后的 AST 生成可执行代码。
Babel 的核心模块
@babel/core
@babel/core
的编译器的核心模块,打开 package.json
可以看到其依赖包
"dependencies": {
"@babel/code-frame": "^7.0.0", // 生成指向源位置包含代码帧的错误
"@babel/generator": "^7.3.4", // Babel 的代码生成器 读取AST并将其转换为代码和源码映射
"@babel/helpers": "^7.2.0", // Babel 转换的帮助函数集合
"@babel/parser": "^7.3.4", // Babel 的解析器
"@babel/template": "^7.2.2", // 从一个字符串模板中生成 AST
"@babel/traverse": "^7.3.4", // 遍历AST 并且负责替换、移除和添加节点
"@babel/types": "^7.3.4", // 为 AST 节点提供的 lodash 类的实用程序库
...
}
复制代码
依次研究一下这些包.....
@babel/parser
以前版本叫 Babylon ,是 Babel 的解析器。@babel/parser
支持 JSX
、Flow
和 TypeScript
语法。API 为:
babelParser.parse(code, [options])
babelParser.parseExpression(code, [options])复制代码
@babel/traverse
@babel/traverse
用于维护 AST 的状态,并且负责替换、移除和添加节点。
遍历并修改 AST (将标识符 n 改为 x)
import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
const code = `function square(n) { return n * n; }`;
const ast = parser.parse(code);
traverse(ast, {
enter(path) {
if (path.isIdentifier({ name: "n" })) {
path.node.name = "x";
}
}
});
复制代码
@babel/types
@babel/types
模块是一个用于 AST 节点的 Lodash 式工具库,它包含了构造、验证以及变换 AST 节点的方法。
引入 import * as t from "babel-types";
判断是否为标识符 t.isIdentifier(node)
构造表达式(a*b) t.binaryExpression("*", t.identifier("a"), t.identifier("b"));
超多 API 见 babel-types ,编写插件需要参考这里。
@babel/generator
@babel/generator
通过 AST 生成代码,同时可以生成转换代码和源码的映射。
对于上面 @babel/traverse
生成的 AST 转换为代码:
import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
import generate from '@babel/generator';
const code = `function square(n) { return n * n;}`;
const ast = parser.parse(code);
traverse(ast, {
enter(path) {
if (path.isIdentifier({
name: "n"
})) {
path.node.name = "x";
}
}
});
const output = generate(ast, { /* options */ }, code);
/*
{ code: 'function square(x) {\n return x * x;\n}', map: null, rawMappings: null }
*/
复制代码
@babel/template
@babel/template 能让你编写字符串形式且带有占位符的代码来代替手动编码。在计算机科学中,这种能力被称为准引用(quasiquotes)。
import template from "@babel/template";
import generate from "@babel/generator";
import * as t from "@babel/types";
const buildRequire = template(`
var IMPORT_NAME = require(SOURCE);
`);
const ast = buildRequire({
IMPORT_NAME: t.identifier("myModule"),
SOURCE: t.stringLiteral("my-module"),
});
console.log(generate(ast).code);
// const myModule = require("my-module");复制代码
Babel 的插件编写
访问者模式
关于访问者模式,可以参考文章:《23种设计模式(9):访问者模式》
总结下就是有元素类和访问者两种类型,元素类有 accept
方法接受一个访问者对象并调用其访问方法,访问者提供访问方法,接受元素类提供的参数并进行操作。
好处是符合单一职责原则和扩展性良好。
使用于对象中存在着一些与本对象不相干(或者关系较弱)的操作,或一组对象中,存在着相似的操作,为了避免出现大量重复的代码,也可以将这些重复的操作封装到访问者中去。
缺点是元素类扩展困难。
访问者
写 Babel 插件就是定义一个访问者,每次进入一个节点的时候,我们是在访问一个节点。对于 AST,@babel/traverse
对其进行先序遍历,每个节点都会被访问两次,可以通过 enter
和 exit
方法对两次访问节点进行操作。
const MyVisitor = {
Identifier() {
console.log("Called!");
}
};
// 你也可以先创建一个访问者对象,并在稍后给它添加方法。
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}复制代码
Identifier() { ... }
相当于 Identifier { enter() { ... } }
通过属性名来指定该属性中的函数会访问哪些节点。也可以通过 |
分割访问多种类型的节点。如: "Idenfifier |MemberExpression"
。
路径
enter()
和 exit()
的参数是 path
,如果想获得当前节点,需要通过 path.node
获取。path 表示两个节点的连接对象,所以除了 node 表示当前节点外还有许多其他的属性,如 parent 获取父节点。
我们也可以遍历一个 traverse(ast, visitor);
也可以直接对路径进行遍历 path.traverse(visitor);
如果忽略当前节点的所有子孙节点,可以使用 path.skip()
如果想要结束遍历,可以使用 path.stop()
。
写一个简单的插件
我们接受 babel 作为参数,可以取 babel.types
作为参数 t
,并返回一个含有 visitor 属性的对象。
export default function({ types: t }) {
return {
visitor: {
// visitor contents
}
};
};复制代码
编写插件,src/visitor.js
,对于二元表达式,如果操作符为 ===
,则将操作符左边的标识符改为 sebmck 将右边的标识符改为 dork 。
export default function({ types: t }) {
return {
visitor: {
BinaryExpression(path) {
if (path.node.operator !== "===") {
return;
}
path.node.left = t.identifier("sebmck");
path.node.right = t.identifier("dork");
}
}
};
}
复制代码
然后在 src/index.js
使用插件
import { transform } from '@babel/core';
const result = transform("foo === bar;", {
plugins: [require("./visitor.js")]
});
console.log(result.code); // sebmck === dork;
复制代码
可以在 package.json
中设置脚本 然后通过 npm run build
执行。(babel 配置不用说了吧
"scripts": {
"build": "babel src/index.js src/visitor.js --out-dir lib && node lib/index.js"
}
复制代码
这样可以在控制台看到输出编译后的结果,sebmck === dork;
antd 的按需加载
看到有面试题是关于 antd 的按需加载的问题。
正常通过 import { Button } from 'antd';
引入组件时会加载整个组件库。如果通过 Babel 转成 import Button from 'antd/lib/button';
则可以只引入所需组件。
通过 AST Explorer 可以看到 import { Button, Table } from 'antd';
生成的 AST 为:
{
"type":"ImportDeclaration",
"start":0,
"end":37,
"specifiers":[
{
"type":"ImportSpecifier",
"start":9,
"end":15,
"imported":{
"type":"Identifier",
"start":9,
"end":15,
"name":"Button"
},
"local":{
"type":"Identifier",
"start":9,
"end":15,
"name":"Button"
}
},
{
"type":"ImportSpecifier",
"start":17,
"end":22,
"imported":{
"type":"Identifier",
"start":17,
"end":22,
"name":"Table"
},
"local":{
"type":"Identifier",
"start":17,
"end":22,
"name":"Table"
}
}
],
"source":{
"type":"Literal",
"start":30,
"end":36,
"value":"antd",
"raw":"'antd'"
}
}
复制代码
同时也要看下生成的 import Table from 'antd/lib/table';
的 AST
{
"type":"ImportDeclaration",
"start":36,
"end":71,
"specifiers":[
{
"type":"ImportDefaultSpecifier",
"start":43,
"end":48,
"local":{
"type":"Identifier",
"start":43,
"end":48,
"name":"Table"
}
}
],
"source":{
"type":"Literal",
"start":54,
"end":70,
"value":"antd/lib/table",
"raw":"'antd/lib/table'"
}
}
复制代码
对比两个 AST ,可以写出转换插件。
module.exports = function({ types: t }) {
return {
visitor: {
ImportDeclaration(path) {
let { specifiers, source } = path.node;
if (source.value === 'antd') {
// 如果库引入的是 'antd'
if (!t.isImportDefaultSpecifier(specifiers[0]) // 判断不是默认导入 import Default from 'antd';
&& !t.isImportNamespaceSpecifier(specifiers[0])) { // 也不是全部导入 import * as antd from 'antd';
let declarations = specifiers.map(specifier => {
let componentName = specifier.imported.name; // 引入的组件名
// 新生成的引入是默认引入
return t.ImportDeclaration([t.ImportDefaultSpecifier(specifier.local)], // 转换后的引入要与之前保持相同的名字
t.StringLiteral('antd/lib/' + componentName.toLowerCase()) // 修改引入库的名字
);
}); // 用转换后的语句替换之前的声明语句
path.replaceWithMultiple(declarations);
}
}
}
}
};
}
复制代码
当然 antd 的插件 babel-plugin-import 是有参数的,所以这里也简单的配置参数。
重写插件
module.exports = function({ types: t }) {
return {
visitor: {
ImportDeclaration(path, { opts }) { // opts 用户配置插件选项
let { specifiers, source } = path.node;
if (source.value === opts.libraryName) { // 如果库引入的是 opts.libraryName 就进行转换
if (!t.isImportDefaultSpecifier(specifiers[0]) // 判断不是默认导入 import Default from 'antd';
&& !t.isImportNamespaceSpecifier(specifiers[0])) { // 也不是全部导入 import * as antd from 'antd';
let declarations = [];
for (let specifier of specifiers) {
let componentName = specifier.imported.name; // 引入的组件名
declarations.push(t.ImportDeclaration( // 新生成的引入是默认引入
[t.ImportDefaultSpecifier(specifier.local)], // 转换后的引入要与之前保持相同的名字
t.StringLiteral(opts.customName(componentName)) // 修改引入库的名字
));
if (opts.styleName) {
declarations.push(t.ExpressionStatement( // 新增引入样式的节点
t.CallExpression(t.Identifier('require'),
[t.StringLiteral(opts.styleName(componentName))])
));
}
} // 用转换后的语句替换之前的声明语句
path.replaceWithMultiple(declarations);
}
}
}
}
};
}
复制代码
配置 babel.config.js
文件
const plugins = [
[
'./plugin.js',
{
"libraryName": "antd", // 转换的库名
"customName": name => `antd/lib/${name.toLowerCase()}`, // 引入组件声明的转换规则
"styleName": name => `antd/lib/${name.toLowerCase()}/style` // 引入组件的样式
}
]
]
module.exports = { plugins }
复制代码
源文件
import { Button as Btn, Table } from 'antd';复制代码
编译后的文件
import Btn from "antd/lib/button";
require("antd/lib/button/style");
import Table from "antd/lib/table";
require("antd/lib/table/style");复制代码