/** 考慮到窩真的是一個很菜的選手,加上英語不太好文件看的很吃力,部分概念可能理解不對,所以如果您發現錯誤,請一定要告訴窩,拯救一個辣雞(但很帥)的少年就靠您了!*/
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");複製程式碼