一文读懂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 };
}
相关文章
相关标签/搜索