/** 考慮到窩真的是一個很菜的選手,加上英語不太好文檔看的很吃力,部分概念可能理解不對,因此若是您發現錯誤,請必定要告訴窩,拯救一個辣雞(但很帥)的少年就靠您了!*/javascript
Babel 是一個 JavaScript 的編譯器。你可能知道 Babel 能夠將最新版的 ES 語法轉爲 ES5,不過不僅如此,它還可用於語法檢查,編譯,代碼高亮,代碼轉換,優化,壓縮等場景。
html
Babel7 爲了區分以前的版本,全部的包名都改爲了 @babel/... 格式。本文參考最新版文檔。java
<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
node
const presets = [
[
'@babel/env',
{
useBuiltIns: 'usage'
}
]
]
module.exports = { presets }複製代碼
也可使用 .babelrc
文件配置,二者好像沒什麼區別,不過 js
文件比 json
文件靈活,一些複雜的配置就只能使用 babel.config.js
了。react
{
"presets": [
[
"@babel/env",
{
"useBuiltIns": "usage"
}
]
]
}
複製代碼
其中 "useBuiltIns": "usage"
是預設插件組合 @babel/env
的選項,表示按需引入用到的 API,使用該選項要下載 @babel/polyfill
包。webpack
建立源文件 src/index.js
git
let f = x => x;
let p = Promise.resolve(1);複製代碼
而後在命令行運行命令 npx babel src/index.js
es6
能夠看到控制檯打印出的編譯後的代碼:github
"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
web
在 Webpack 中配置 babel-loader
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
]
}
複製代碼
更多使用方法可見 使用 Babel
使用 Babel 時通常會設置 presets
和 plugins
,也能夠同時設置。而 Presets
就是預設的一組 Babel 插件集合。
Babel 會先執行 plugins
再執行 presets
,其中 plugins
按指定順序執行,presets
逆序執行。
設置預設的插件集合,來配置 babel 能轉換的 ES 語法的級別,stage 表示語法提案的不一樣階段。如今所有不推薦使用了,請一概使用 @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> 複製代碼
{
"presets": ["es2015"],
"plugins": [],
"env": {
"development": {
"plugins": [...]
},
"production": {
"plugins": [...]
}
}
}
複製代碼
當前環境可使用 process.env.BABEL_ENV
來得到。 若是 BABEL_ENV
不可用,將會替換成 NODE_ENV
,而且若是後者也沒有設置,那麼缺省值是"development"
。
Babel 在配置了上面的 babel-preset-env
以後,只能轉換語法,而對於一些新的 API,如 Promise
,Map
等,並無實現,仍然須要引入。
引入 @babel/polyfill
(能夠經過 require("@babel/polyfill");
或 import "@babel/polyfill";
)會把這些 API 所有掛載到全局對象。缺點是會污染全局變量,同時若是隻用到其中部分的話,會形成多餘的引用。也能夠在 @babel/preset-env
裏經過設置 useBuiltIns
選項引入。
@babel/runtime
和 @babel/polyfill
解決相同的問題,不過 @babel/runtime
是手動按需引用的。 不一樣於 @babel/polyfill
的掛載全局對象, @babel/runtime
是以模塊化方式包含函數實現的包。
引入 babel-plugin-transform-runtime
包實現屢次引用相同 API 只加載一次。
注意:對於相似 "foobar".includes("foo")
的實例方法是不生效的,如需使用則仍要引用 @babel/polyfill
。
babel 的命令行工具,能夠在命令行使用 Babel 編譯文件,像前文演示的那樣。
@babel/register
模塊改寫 require
命令,爲它加上一個鉤子。此後,每當使用 require
加載 .js
、.jsx
、.es
和 .es6
後綴名的文件,就會先用 Babel 進行轉碼。默認會忽略 node_modules
。具體配置可見 @babel/register 。
@babel/node
提供一個同 node 同樣的命令行工具,不過它在運行代碼以前會根據 Babel 配置進行編譯。在 Babel7 中 @babel/node
不包含在 @babel/cli
中了。
babel 編譯器的核心。能夠經過直接調用 API 來對代碼、文件或 AST 進行轉換。
解析(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/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 類的實用程序庫
...
}
複製代碼
依次研究一下這些包.....
之前版本叫 Babylon ,是 Babel 的解析器。@babel/parser
支持 JSX
、Flow
和 TypeScript
語法。API 爲:
babelParser.parse(code, [options])
babelParser.parseExpression(code, [options])複製代碼
@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
模塊是一個用於 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
經過 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 能讓你編寫字符串形式且帶有佔位符的代碼來代替手動編碼。在計算機科學中,這種能力被稱爲準引用(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");複製代碼
關於訪問者模式,能夠參考文章:《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 的按需加載的問題。
正常經過 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");複製代碼