前端基建: Babel 梳理

154 阅读4分钟

什么是 Babel

Babel 其实就是一个 JavaScript 的“编译器”,是一个使用 Lerna 构建的 Monorepo 风格的仓库,在其./packages目录下有 140 多个包。Babel 不仅仅是一个工具,更是一个工具链(toolchain),是前端基建中绝对重要的一环。

主要模块

因为前端语言特性和宿主(浏览器/Node.js 等)环境高速发展,但宿主环境对新语言特性的支持无法做到即时,而开发者又需要兼容各种宿主环境,因此语言特性的降级成为刚需,前端各种代码被编译为 JavaScript 的需求成为标配,这就是 Babel 的职责。Babel 在前端中占有举足轻重的历史地位,几乎所有的大型前端应用项目都离不开 Babel 的支持。

@babel/core

是 Babel 实现转换的核心,它可以根据配置,进行源码的编译转换:

var babel = require("@babel/core");

babel.transform(code, options, function(err, result) {
  result; // => { code, map, ast }
});

@babel/cli

是 Babel 提供的命令行,它可以在终端中通过命令行方式运行,编译文件或目录。@babel/cli 使用了 commander 库搭建基本的命令行开发。作为 @babel/cli 的关键依赖,@babel/core 提供了基础的编译能力。以编译文件为例,其关键部分源码如下:

import * as util from "./util";

const results = await Promise.all(
  _filenames.map(async function (filename: string): Promise<Object> {
    let sourceFilename = filename;
    if (cliOptions.outFile) {
      sourceFilename = path.relative(
        path.dirname(cliOptions.outFile),
        sourceFilename,
      );
    }
    // 获取文件名
    sourceFilename = slash(sourceFilename);
    try {
      return await util.compile(filename, {
        ...babelOptions,
        sourceFileName: sourceFilename,
        // 获取 sourceMaps 配置项
        sourceMaps:
          babelOptions.sourceMaps === "inline"
            ? true
            : babelOptions.sourceMaps,
      });
    } catch (err) {
      if (!cliOptions.watch) {
        throw err;
      }
      console.error(err);
      return null;
    }
  }),
);

@babel/standalone

这个包对于浏览器环境动态插入高级语言特性的脚本、在线自动解析编译非常有意义。Babel 官网也用到了这个包。它可以在非 Node.js 环境(比如浏览器环境)自动编译含有 text/babel 或 text/jsx 的 type 值的 script 标签,并进行编译,如下面代码:

<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
	const getMessage = () => "Hello World";
	document.getElementById('output').innerHTML = getMessage();
</script>

@babel/parser

是 Babel 用来对 JavaScript 语言解析的解析器,源码中的require("@babel/parser").parse() 方法可以返回给我们一个针对源码编译得到的 AST,这里的 AST 符合 Babel AST 格式。

@babel/traverse

有了 AST,我们还需要对 AST 完成修改,才能产出编译后的代码。这就需要对 AST 进行遍历,此时 就需要 @babel/traverse,大概用法如下:

traverse(ast, {
  enter(path) {
    if (path.isIdentifier({ name: "n" })) {
      path.node.name = "x";
    }
  }
});

遍历的同时,需要,@babel/types 对具体的 AST 节点进行修改。

@babel/generator

得到了编译后的 AST 之后,最后一步:使用 @babel/generator 对新的 AST 进行聚合并生成 JavaScript 代码:

const output = generate(
  ast,
  {
    /* options */
  },
  code
);

@babel/preset-env

在工程中运用时,我们很少直接操作 @babel/core、@babel/types 等,而应该对 @babel/preset-env 更加熟悉,毕竟 @babel/preset-env 是直接暴露给开发者在业务中运用的包能力。

在工程中,我们需要 Babel 做到的是编译降级,而这个编译降级一般通过 @babel/preset-env 来配置。@babel/preset-env 允许我们配置需要支持的目标环境(一般是浏览器范围或 Node.js 版本范围),利用 core-js 和 regenerator-runtime, 通过 targets 参数,按照 browserslist 规范筛选出适配环境所需的 polyfills(或 plugins),关键源码如下:

export default declare((api, opts) => {

  // 规范参数
  const {
    bugfixes,
    configPath,
    debug,
    exclude: optionsExclude,
    forceAllTransforms,
    ignoreBrowserslistConfig,
    include: optionsInclude,
    loose,
    modules,
    shippedProposals,
    spec,
    targets: optionsTargets,
    useBuiltIns,
    corejs: { version: corejs, proposals },
    browserslistEnv,
  } = normalizeOptions(opts);

  let hasUglifyTarget = false;

  // 获取对应 targets

  const targets = getTargets(
    (optionsTargets: InputTargets),
    { ignoreBrowserslistConfig, configPath, browserslistEnv },
  );

  const include = transformIncludesAndExcludes(optionsInclude);
  const exclude = transformIncludesAndExcludes(optionsExclude);
  const transformTargets = forceAllTransforms || hasUglifyTarget ? {} : targets;

  // 获取需要兼容的内容

  const compatData = getPluginList(shippedProposals, bugfixes);

  const modulesPluginNames = getModulesPluginNames({
    modules,
    transformations: moduleTransformations,
    shouldTransformESM: modules !== "auto" || !api.caller?.(supportsStaticESM),
    shouldTransformDynamicImport:
      modules !== "auto" || !api.caller?.(supportsDynamicImport),
    shouldTransformExportNamespaceFrom: !shouldSkipExportNamespaceFrom,
    shouldParseTopLevelAwait: !api.caller || api.caller(supportsTopLevelAwait),
  });

  // 获取目标 plugin 名称
  const pluginNames = filterItems(
    compatData,
    include.plugins,
    exclude.plugins,
    transformTargets,
    modulesPluginNames,
    getOptionSpecificExcludesFor({ loose }),
    pluginSyntaxMap,
  );

  removeUnnecessaryItems(pluginNames, overlappingPlugins);

  const polyfillPlugins = getPolyfillPlugins({
    useBuiltIns,
    corejs,
    polyfillTargets: targets,
    include: include.builtIns,
    exclude: exclude.builtIns,
    proposals,
    shippedProposals,
    regenerator: pluginNames.has("transform-regenerator"),
    debug,
  });
  const pluginUseBuiltIns = useBuiltIns !== false;

  // 根据 pluginNames,返回一个 plugins 配置列表
  const plugins = Array.from(pluginNames)
    .map(pluginName => {
      if (
        pluginName === "proposal-class-properties" ||
        pluginName === "proposal-private-methods" ||
        pluginName === "proposal-private-property-in-object"
      ) {
        return [
          getPlugin(pluginName),
          {
            loose: loose
              ? "#__internal__@babel/preset-env__prefer-true-but-false-is-ok-if-it-prevents-an-error"
              : "#__internal__@babel/preset-env__prefer-false-but-true-is-ok-if-it-prevents-an-error",
          },
        ];
      }
      return [
        getPlugin(pluginName),
        { spec, loose, useBuiltIns: pluginUseBuiltIns },
      ];
    })
    .concat(polyfillPlugins);
  return { plugins };
});

@babel/plugin

是 Babel 插件集合,他的细分内容命名规则如下:

@babel/plugin-syntax- 是 Babel 的语法插件。它的作用是扩展 @babel/parser 的一些能力,提供给工程使用。比如 @babel/plugin-syntax-top-level-await 插件,提供了使用 top level await 新特性的能力。

@babel/plugin-proposal- 用于编译转换在提议阶段的语言特性。

@babel/plugin-transform- 是 Babel 的转换插件。比如简单的 @babel/plugin-transform-react-display-name 插件,可以自动适配 React 组件 DisplayName。

babel-loader

babel-loader 是 Babel 结合 Webpack,融入整个基建环节的例子。在 Webpack 编译生命周期中,babel-loader 作为一个 Webpack loader,承担着文件编译的职责。

总结

Babel 生态和前端工程中的各个环节都是打通开放的。它可以以 babel-loader 的形式和 Webpack 协作,也可以以 @babel/eslint-parser 的方式和 ESLint 合作。现代化的前端工程是一环扣一环的,作为工程链上的任意一环,插件化能力、协作能力将是设计的重点和关键。