一文讀懂babel編譯流程,不再怕面試官的刁難了

前言

Babel 是一個強大的 js 編譯器。有了 Babel, 咱們能夠放肆的使用 js 的新特性,而不用考慮瀏覽器兼容性問題。不只如此,基於 babel 體系,咱們能夠經過插件的方法修改一些語法,優化一些語法,甚至建立新的語法。javascript

那麼,如此強大且靈活的特性是如何實現的?咱們從頭開始,瞭解下 Babel 的編譯流程。html

流程

babel流程 (1)

babel生成配置

image-20210425145412812

package.json

項目配置文件java

"devDependencies": {
    "@babel/cli": "7.10.5",
    "@babel/core": "7.11.1",
    "@babel/plugin-proposal-class-properties": "7.10.4",
    "@babel/plugin-proposal-decorators": "7.10.5",
    "@babel/plugin-proposal-do-expressions": "7.10.4",
    "@babel/plugin-proposal-object-rest-spread": "7.11.0",
    "@babel/plugin-syntax-dynamic-import": "7.8.3",
    "@babel/plugin-transform-react-jsx": "7.12.17",
    "@babel/plugin-transform-runtime": "7.11.0",
    "@babel/preset-env": "7.11.0",
    "@babel/preset-react": "7.12.13",
    "@babel/preset-typescript": "7.12.17",
      .......
}

咱們常接觸到的有babelbabel-loader@babel/core@babel/preset-env@babel/polyfill、以及@babel/plugin-transform-runtime,這些都是作什麼的?node

一、babel:

babel官網對其作了很是明瞭的定義:react

Babel 是一個工具鏈,主要用於在舊的瀏覽器或環境中將 ECMAScript 2015+ 代碼轉換爲向後兼容版本的 JavaScript 代碼:
轉換語法
Polyfill 實現目標環境中缺乏的功能 (經過 @babel/polyfill)
源代碼轉換 (codemods)
更多!

咱們能夠看到,babel是一個包含語法轉換等諸多功能的工具鏈,經過這個工具鏈的使用可使低版本的瀏覽器兼容最新的javascript語法。webpack

須要注意的是,babel也是一個能夠安裝的包,而且在 webpack 1.x 配置中使用它來做爲 loader 的簡寫 。如:git

{
  test: /\.js$/,
  loader: 'babel',
}

可是這種方式在webpack 2.x之後再也不支持並獲得錯誤提示:es6

The node API forbabelhas been moved tobabel-coregithub

此時刪掉 babel包,安裝babel-loader, 並制定loader: 'babel-loader'便可web

二、@babel/core:

@babel/core 是整個 babel 的核心,它負責調度 babel 的各個組件來進行代碼編譯,是整個行爲的組織者和調度者。

transform 方法會調用 transformFileRunner 進行文件編譯,首先就是 loadConfig 方法生成完整的配置。而後讀取文件中的代碼,根據這個配置進行編譯。

const transformFileRunner = gensync<[string, ?InputOptions], FileResult | null>(
  function* (filename, opts) {
    const options = { ...opts, filename };

    const config: ResolvedConfig | null = yield* loadConfig(options);
    if (config === null) return null;

    const code = yield* fs.readFile(filename, "utf8");
    return yield* run(config, code);
  },
);

三、@babel/preset-env:

這是一個預設的插件集合,包含了一組相關的插件,Bable中是經過各類插件來指導如何進行代碼轉換。該插件包含全部es6轉化爲es5的翻譯規則

babel官網對此進行的以下說明:

Transformations come in the form of plugins, which are small JavaScript programs that instruct Babel on how to carry out transformations to the code. You can even write your own plugins to apply any transformations you want to your code. To transform ES2015+ syntax into ES5 we can rely on official plugins like @babel/plugin-transform-arrow-functions

大體即es6到es5的語法轉換是以插件的形式實現的,能夠是本身的插件也能夠是官方提供的插件如箭頭函數轉換插件@babel/plugin-transform-arrow-functions。

由此咱們能夠看出,咱們須要轉換哪些新的語法,均可以將相關的插件一一列出,可是這其實很是複雜,由於咱們每每須要根據兼容的瀏覽器的不一樣版原本肯定須要引入哪些插件,爲了解決這個問題,babel給咱們提供了一個預設插件組,即@babel/preset-env,能夠根據選項參數來靈活地決定提供哪些插件

{
    "presets":["es2015","react","stage-1"],
    "plugins": [["transform-runtime"],["import", {
        "libraryName": "cheui-react",
        "libraryDirectory": "lib/components",
        "camel2DashComponentName": true // default: true
    }]]
  }

三個關鍵參數:

一、targets:

Describes the environments you support/target for your project.

簡單講,該參數決定了咱們項目須要適配到的環境,好比能夠申明適配到的瀏覽器版本,這樣 babel 會根據瀏覽器的支持狀況自動引入所須要的 polyfill。

二、useBuiltIns:

"usage" | "entry" | false, defaults to false

This option configures how @babel/preset-env handles polyfills.

這個參數決定了 preset-env 如何處理 polyfills。

false`: 這種方式下,不會引入 polyfills,你須要人爲在入口文件處`import '@babel/polyfill';

但如上這種方式在 @babel@7.4 以後被廢棄了,取而代之的是在入口文件處自行 import 以下代碼

import 'core-js/stable';
import 'regenerator-runtime/runtime';
// your code

不推薦採用 false,這樣會把全部的 polyfills 所有打入,形成包體積龐大

usage:

咱們在項目的入口文件處不須要 import 對應的 polyfills 相關庫。 babel 會根據用戶代碼的使用狀況,並根據 targets 自行注入相關 polyfills。

entry:

咱們在項目的入口文件處 import 對應的 polyfills 相關庫,例如

import 'core-js/stable';
import 'regenerator-runtime/runtime';
// your code

此時 babel 會根據當前 targets 描述,把須要的全部的 polyfills 所有引入到你的入口文件(注意是所有,無論你是否有用到高級的 API)

三、corejs:

String or { version: string, proposals: boolean }, defaults to "2.0".

corejs

注意 corejs 並非特殊概念,而是瀏覽器的 polyfill 都由它來管了。

舉個例子

javascript const one = Symbol('one');

==Babel==>

"use strict";

require("core-js/modules/es.symbol.js");

require("core-js/modules/es.symbol.description.js");

require("core-js/modules/es.object.to-string.js");

var one = Symbol('one');

這裏或許有人可能不太清楚,2 和 3 有啥區別,能夠看看官方的文檔 core-js@3, babel and a look into the future

簡單講 corejs-2 不會維護了,全部瀏覽器新 feature 的 polyfill 都會維護在 corejs-3 上。

總結下:用 corejs-3,開啓 proposals: true,proposals 爲真那樣咱們就可使用 proposals 階段的 API 了。

四、@babel/polyfill:

@babel/preset-env只是提供了語法轉換的規則,可是它並不能彌補瀏覽器缺失的一些新的功能,如一些內置的方法和對象,如Promise,Array.from等,此時就須要polyfill來作js得墊片,彌補低版本瀏覽器缺失的這些新功能。

咱們須要注意的是,polyfill的體積是很大的,若是咱們不作特殊說明,它會把你目標瀏覽器中缺失的全部的es6的新的功能都作墊片處理。可是咱們沒有用到的那部分功能的轉換實際上是無心義的,形成打包後的體積無謂的增大,因此一般,咱們會在presets的選項裏,配置"useBuiltIns": "usage",這樣一方面只對使用的新功能作墊片,另外一方面,也不須要咱們單獨引入import '@babel/polyfill'了,它會在使用的地方自動注入。

五、babel-loader:

以上@babel/core、@babel/preset-env 、@babel/polyfill其實都是在作es6的語法轉換和彌補缺失的功能,可是當咱們在使用webpack打包js時,webpack並不知道應該怎麼去調用這些規則去編譯js。這時就須要babel-loader了,它做爲一箇中間橋樑,經過調用babel/core中的api來告訴webpack要如何處理js。

六、@babel/plugin-transform-runtime:

polyfill的墊片是在全局變量上掛載目標瀏覽器缺失的功能,所以在開發類庫,第三方模塊或者組件庫時,就不能再使用babel-polyfill了,不然可能會形成全局污染,此時應該使用transform-runtime。transform-runtime的轉換是非侵入性的,也就是它不會污染你的原有的方法。遇到須要轉換的方法它會另起一個名字,不然會直接影響使用庫的業務代碼,

.babelrc

若是咱們什麼都不配置的話,打包後的文件不會有任何變化,須要在 babelrc 文件中對 babel 作以下配置。而後打包。咱們後續會分析該配置做用的機制。

{
    "presets": ["@babel/preset-env"]
}

@babel/cli 解析命令行,可是僅有命令行中的參數的話,babel 是沒法進行編譯工做的,還缺乏一些關鍵性的參數,也就是配置在 .babelrc 文件中的插件信息。

@babel/core 在執行 transformFile 操做以前,第一步就是讀取 .babelrc 文件中的配置。

流程是這樣的,babel 首先會判斷命令行中有沒有指定配置文件(-config-file),有就解析,沒有的話 babel 會在當前根目錄下尋找默認的配置文件。默認文件名稱定義以下。優先級從上到下。

babel-main\packages\babel-core\src\config\files\configuration.js

const RELATIVE_CONFIG_FILENAMES = [
  ".babelrc",
  ".babelrc.js",
  ".babelrc.cjs",
  ".babelrc.mjs",
  ".babelrc.json",
];

.babelrc 文件中,咱們常常配置的是 plugins 和 presets,plugin 是 babel 中真正幹活的,代碼的轉化全靠它,可是隨着 plugin 的增多,如何管理好這些 plugin 也是一個挑戰。因而,babel 將一些 plugin 放在一塊兒,稱之爲 preset。

對於 babelrc 中的 plugins 和 presets,babel 將每一項都轉化爲一個 ConfigItem。presets 是一個 ConfigItem 數組,plugins 也是一個 ConfigItem 數組。

假設有以下的 .babelrc 文件,會生成這樣的 json 配置。

{
    "presets": ["@babel/preset-env"],
    "plugins": ["@babel/plugin-proposal-class-properties"]
}
plugins: [
     ConfigItem {
      value: [Function],
      options: undefined,
      dirname: 'babel\\babel-demo',
      name: undefined,
      file: {
        request: '@babel/plugin-proposal-class-properties',
        resolved: 'babel\\babel-demo\\node_modules\\@babel\\plugin-proposal-class-properties\\lib\\index.js'
      }
    }
  ],
  presets: [
    ConfigItem {
      value: [Function],
      options: undefined,
      dirname: 'babel\\babel-demo',
      name: undefined,
      file: {
        request: '@babel/preset-env',
        resolved: 'babel\\babel-demo\\node_modules\\@babel\\preset-env\\lib\\index.js'
      }
    }
  ]

對於 plugins,babel 會依序加載其中的內容,解析出插件中定義的 pre,visitor 等對象。因爲 presets 中會包含對個 plugin,甚至會包括新的 preset,因此 babel 須要解析 preset 的內容,將其中包含的 plugin 解析出來。以 @babel/preset-env 爲例,babel 會將其中的 40 個 plugin 解析到,以後會從新解析 presets 中的插件。

這裏有一個頗有意思的點,就是對於解析出的插件列表,處理的方式是使用 unshift 插入到一個列表的頭部。

if (plugins.length > 0) {
  pass.unshift(...plugins);
}

這實際上是由於 presets 加載順序和通常理解不同 ,好比 presets 寫成 ["es2015", "stage-0"],因爲 stage-x 是 Javascript 語法的一些提案,那這部分可能依賴了ES6 的語法,解析的時候須要先將新的語法解析成 ES6,在把 ES6 解析成 ES5。這也就是使用 unshift 的緣由。新的 preset 中的插件會被優先執行。

固然,無論 presets 的順序是怎樣的,咱們定義的 plugins 中的插件永遠是最高優先級。緣由是 plugins 中的插件是在 presets 處理完畢後使用 unshift 插入對列頭部。

最終生成的配置包含 options 和 passes 兩塊,大部分狀況下,options 中的 presets 是個空數組,plugins 中存放着插件集合,passes 中的內容和 options.plugins 是一致的。

{
  options: {
    babelrc: false,
    caller: {name: "@babel/cli"},
    cloneInputAst: true,
    configFile: false,
    envName: "development",
    filename: "babel-demo\src\index.js",
    plugins: Array(41),
    presets: []
  }
  passes: [Array(41)]
}

babel執行編譯

流程

image-20210425145517250

下面看一下run的主要代碼

export function* run(
  config: ResolvedConfig,
  code: string,
  ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<FileResult> {

  const file = yield* normalizeFile(
    config.passes,
    normalizeOptions(config),
    code,
    ast,
  );

  const opts = file.opts;
  try {
    yield* transformFile(file, config.passes);
  } catch (e) {
    ...
  }

  let outputCode, outputMap;
  try {
    if (opts.code !== false) {
      ({ outputCode, outputMap } = generateCode(config.passes, file));
    }
  } catch (e) {
    ...
  }

  return {
    metadata: file.metadata,
    options: opts,
    ast: opts.ast === true ? file.ast : null,
    code: outputCode === undefined ? null : outputCode,
    map: outputMap === undefined ? null : outputMap,
    sourceType: file.ast.program.sourceType,
  };
}
  1. 首先是執行 normalizeFile 方法,該方法的做用就是將 code 轉化爲抽象語法樹(AST);
  2. 接着執行 transformFile 方法,該方法入參有咱們的插件列表,這一步作的就是根據插件修改 AST 的內容;
  3. 最後執行 generateCode 方法,將修改後的 AST 轉換成代碼。

整個編譯過程仍是挺清晰的,簡單來講就是解析(parse),轉換(transform),生成(generate)。咱們詳細看下每一個過程。

解析(parse)

瞭解解析過程以前,要先了解抽象語法樹(AST),它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。不一樣的語言生成 AST 規則不一樣,在 JS 中,AST 就是一個用於描述代碼的 JSON 串。

舉例簡單的例子,對於一個簡單的常量申明,生成的 AST 代碼是這樣的。

const a = 1
{
  "type": "Program",
  "start": 0,
  "end": 11,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 11,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 11,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 7,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 10,
            "end": 11,
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}

回到 normalizeFile 方法,該方法中調用了 parser 方法。

export default function* normalizeFile(
  pluginPasses: PluginPasses,
  options: Object,
  code: string,
  ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<File> {
  ...
  ast = yield* parser(pluginPasses, options, code);
  ...
}

parser 會遍歷全部的插件,看哪一個插件中定義了 parserOverride 方法。爲了方便理解,咱們先跳過這部分,先看 parse 方法,parse 方法是 @babel/parser 提供的一個方法,用於將 JS 代碼裝化爲 AST。

正常狀況下, @babel/parser 中的規則是能夠很好的完成 AST 轉換的,但若是咱們須要自定義語法,或者是修改/擴展這些規則的時候,@babel/parser 就不夠用了。babel 想了個方法,就是你能夠本身寫一個 parser,而後經過插件的方式,指定這個 parser 做爲 babel 的編譯器。

import { parse } from "@babel/parser";

export default function* parser(
  pluginPasses: PluginPasses,
  { parserOpts, highlightCode = true, filename = "unknown" }: Object,
  code: string,
): Handler<ParseResult> {
  try {
    const results = [];
    for (const plugins of pluginPasses) {
      for (const plugin of plugins) {
        const { parserOverride } = plugin;
        if (parserOverride) {
          const ast = parserOverride(code, parserOpts, parse);

          if (ast !== undefined) results.push(ast);
        }
      }
    }

    if (results.length === 0) {

      return parse(code, parserOpts);

    } else if (results.length === 1) {
      yield* []; // If we want to allow async parsers

      ...

      return results[0];
    }
    throw new Error("More than one plugin attempted to override parsing.");
  } catch (err) {
    ...
  }
}

如今回過頭來看前面的循環就很好理解了,遍歷插件,插件中若是定義了 parserOverride 方法,就認爲用戶指定了自定義的編譯器。從代碼中得知,插件定義的編譯器最多隻能是一個,不然 babel 會不知道執行哪一個編譯器。

以下是一個自定義編譯器插件的例子。

const parse = require("custom-fork-of-babel-parser-on-npm-here");

module.exports = {
  plugins: [{
    parserOverride(code, opts) {
      return parse(code, opts);
    },
  }]
}

JS 轉換爲 AST 的過程依賴於 @babel/parser,用戶已能夠經過插件的方式本身寫一個 parser 來覆蓋默認的。@babel/parser 的過程仍是挺複雜的,後續咱們單獨分析它,這裏只要知道它是將 JS 代碼轉換成 AST 就能夠了。

轉換(transform)

AST 須要根據插件內容作一些變換,咱們先大概的看下一個插件長什麼樣子。以下所示,Babel 插件返回一個 function ,入參爲 babel 對象,返回 Object。其中 pre, post 分別在進入/離開 AST 的時候觸發,因此通常分別用來作初始化/刪除對象的操做。visitor(訪問者)定義了用於在一個樹狀結構中獲取具體節點的方法。

module.exports = (babel) => {
  return {
    pre(path) {
      this.runtimeData = {}
    },
    visitor: {},
    post(path) {
      delete this.runtimeData
    }
  }
}

理解了插件的結構以後,再看 transformFile 方法就比較簡單了。首先 babel 爲插件集合增長了一個 loadBlockHoistPlugin 的插件,用於排序的,無需深究。而後就是執行插件的 pre 方法,等待全部插件的 pre 方法都執行完畢後,執行 visitor 中的方法(並非簡單的執行方法,而是根據訪問者模式在遇到相應的節點或屬性的時候執行,具體規則見Babel 插件手冊),爲了優化,babel 將多個 visitor 合併成一個,使用 traverse 遍歷 AST 節點,在遍歷過程當中執行插件。最後執行插件的 post 方法。

import traverse from "@babel/traverse";

function* transformFile(file: File, pluginPasses: PluginPasses): Handler<void> {
  for (const pluginPairs of pluginPasses) {
    const passPairs = [];
    const passes = [];
    const visitors = [];

    for (const plugin of pluginPairs.concat([loadBlockHoistPlugin()])) {
      const pass = new PluginPass(file, plugin.key, plugin.options);

      passPairs.push([plugin, pass]);
      passes.push(pass);
      visitors.push(plugin.visitor);
    }

    for (const [plugin, pass] of passPairs) {
      const fn = plugin.pre;
      if (fn) {
        const result = fn.call(pass, file);

        yield* [];
        ...
      }
    }

    // merge all plugin visitors into a single visitor
    const visitor = traverse.visitors.merge(
      visitors,
      passes,
      file.opts.wrapPluginVisitorMethod,
    );

    traverse(file.ast, visitor, file.scope);

    for (const [plugin, pass] of passPairs) {
      const fn = plugin.post;
      if (fn) {
        const result = fn.call(pass, file);

        yield* [];
        ...
      }
    }
  }
}

該階段的核心是插件,插件使用 visitor 訪問者模式定義了遇到特定的節點後如何進行操做。babel 將對AST 樹的遍歷和對節點的增刪改等方法放在了 @babel/traverse 包中。

生成(generate)

AST 轉換完畢後,須要將 AST 從新生成 code。

@babel/generator 提供了默認的 generate 方法,若是須要定製的話,能夠經過插件的 generatorOverride 方法自定義一個。這個方法和第一個階段的 parserOverride 是相對應的。生成目標代碼後,還會同時生成 sourceMap 相關的代碼。

import generate from "@babel/generator";

export default function generateCode(
  pluginPasses: PluginPasses,
  file: File,
): {
  outputCode: string,
  outputMap: SourceMap | null,
} {
  const { opts, ast, code, inputMap } = file;

  const results = [];
  for (const plugins of pluginPasses) {
    for (const plugin of plugins) {
      const { generatorOverride } = plugin;
      if (generatorOverride) {
        const result = generatorOverride(
          ast,
          opts.generatorOpts,
          code,
          generate,
        );

        if (result !== undefined) results.push(result);
      }
    }
  }

  let result;
  if (results.length === 0) {
    result = generate(ast, opts.generatorOpts, code);
  } else if (results.length === 1) {
    result = results[0];
    ...
  } else {
    throw new Error("More than one plugin attempted to override codegen.");
  }

  let { code: outputCode, map: outputMap } = result;

  if (outputMap && inputMap) {
    outputMap = mergeSourceMap(inputMap.toObject(), outputMap);
  }

  if (opts.sourceMaps === "inline" || opts.sourceMaps === "both") {
    outputCode += "\n" + convertSourceMap.fromObject(outputMap).toComment();
  }

  if (opts.sourceMaps === "inline") {
    outputMap = null;
  }

  return { outputCode, outputMap };
}
相關文章
相關標籤/搜索