【面試準備·1】Babel

  /** 考慮到窩真的是一個很菜的選手,加上英語不太好文檔看的很吃力,部分概念可能理解不對,因此若是您發現錯誤,請必定要告訴窩,拯救一個辣雞(但很帥)的少年就靠您了!*/javascript

Babel 是一個 JavaScript 的編譯器。你可能知道 Babel 能夠將最新版的 ES 語法轉爲 ES5,不過不僅如此,它還可用於語法檢查,編譯,代碼高亮,代碼轉換,優化,壓縮等場景。
html

Babel7 爲了區分以前的版本,全部的包名都改爲了 @babel/... 格式。本文參考最新版文檔。java

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  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.jses6

能夠看到控制檯打印出的編譯後的代碼: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.jsweb

  • 構建工具的插件(webpack、Glup 等)

在 Webpack 中配置 babel-loader 

module: {
  rules: [
    { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
  ]
}
複製代碼

更多使用方法可見 使用 Babel

Babel 配置 presets 和 plugins

使用 Babel 時通常會設置 presetsplugins ,也能夠同時設置。而 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 。

舉一個同時配置 pluginspresets 的例子:

配置文件 .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,如 PromiseMap 等,並無實現,仍然須要引入。

引入 @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 支持 JSXFlowTypeScript 語法。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 對其進行先序遍歷,每一個節點都會被訪問兩次,能夠經過 enterexit 方法對兩次訪問節點進行操做。

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");複製代碼

參考資料

相關文章
相關標籤/搜索