关于前端:一文读懂babel编译流程再也不怕面试官的刁难了

3次阅读

共计 12087 个字符,预计需要花费 31 分钟才能阅读完成。

前言

Babel 是一个弱小的 js 编译器。有了 Babel,咱们能够放肆的应用 js 的新个性,而不必思考浏览器兼容性问题。不仅如此,基于 babel 体系,咱们能够通过插件的办法批改一些语法,优化一些语法,甚至创立新的语法。

那么,如此弱小且灵便的个性是如何实现的?咱们从头开始,理解下 Babel 的编译流程。

流程

babel 生成配置

package.json

我的项目配置文件

"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,这些都是做什么的?

1、babel:

babel 官网对其做了十分明了的定义:

Babel 是一个工具链,次要用于在旧的浏览器或环境中将 ECMAScript 2015+ 代码转换为向后兼容版本的 JavaScript 代码:
转换语法
Polyfill 实现目标环境中短少的性能 (通过 @babel/polyfill)
源代码转换 (codemods)
更多!

咱们能够看到,babel 是一个蕴含语法转换等诸多性能的工具链,通过这个工具链的应用能够使低版本的浏览器兼容最新的 javascript 语法。

须要留神的是,babel 也是一个能够装置的包,并且在 webpack 1.x 配置中应用它来作为 loader 的简写。如:

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

然而这种形式在 webpack 2.x 当前不再反对并失去谬误提醒:

The node API forbabelhas been moved tobabel-core

此时删掉 babel 包,装置 babel-loader, 并制订 loader: ‘babel-loader’ 即可

2、@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);
  },
);

3、@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
    }]]
  }

三个要害参数:

1、targets:

Describes the environments you support/target for your project.

简略讲,该参数决定了咱们我的项目须要适配到的环境,比方能够申明适配到的浏览器版本,这样 babel 会依据浏览器的反对状况主动引入所须要的 polyfill。

2、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)

3、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 了。

4、@babel/polyfill:

@babel/preset-env 只是提供了语法转换的规定,然而它并不能补救浏览器缺失的一些新的性能,如一些内置的办法和对象,如 Promise,Array.from 等,此时就须要 polyfill 来做 js 得垫片,补救低版本浏览器缺失的这些新性能。

咱们须要留神的是,polyfill 的体积是很大的,如果咱们不做非凡阐明,它会把你指标浏览器中缺失的所有的 es6 的新的性能都做垫片解决。然而咱们没有用到的那局部性能的转换其实是无意义的,造成打包后的体积无谓的增大,所以通常,咱们会在 presets 的选项里,配置“useBuiltIns”: “usage”, 这样一方面只对应用的新性能做垫片,另一方面,也不须要咱们独自引入 import ‘@babel/polyfill‘ 了,它会在应用的中央主动注入。

5、babel-loader:

以上 @babel/core、@babel/preset-env、@babel/polyfill 其实都是在做 es6 的语法转换和补救缺失的性能,然而当咱们在应用 webpack 打包 js 时,webpack 并不知道应该怎么去调用这些规定去编译 js。这时就须要 babel-loader 了,它作为一个两头桥梁,通过调用 babel/core 中的 api 来通知 webpack 要如何解决 js。

6、@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 执行编译

流程

上面看一下 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};
}
正文完
 0