前端工程化之Babel详解和实战

浏览器的发展跟不上语言的发展,es6+虽然很普及了,但也不是所有浏览器都可以支持 es6+语法。babel 的诞生就源于此。

什么是 Babel

Babel 是一个广泛使用的 JavaScript 编译器,主要功能是将 ECMAScript 2015+版本的代码转换为向后兼容的 JavaScript 版本,以便在当前和旧版本的浏览器或环境中运行。它出现的原因是 JavaScript 语言快速发展,新的语言特性不断加入,但这些新特性并不总是被所有的浏览器或环境所支持。Babel 使得开发者可以使用最新的 JavaScript 特性,而不必担心兼容性问题。它通过转换新语法和实现新特性来实现这一点。此外,Babel 也支持 React 的 JSX 语法,使其在转换过程中能够转换为标准的 JavaScript 代码。

Babel 的出现主要是为了解决以下两个痛点:

  • ES 标准兼容性问题
  • 支持提前尝试 ES 的新特征

ES 标准兼容性问题

首先是兼容性问题,在 ES5 之前,JavaScript 就是充满缺陷的语言:历史包袱终、不支持模块化、作用域污染、不支持 class、原型链继承、令人迷惑的 this 等等。所以在长达十年的讨论、修补后,新发布了 ES6 规范,为 JavaScript 添加了一些语言特征,改善可维护性,提高开发体验。

但是软件版本的发布是非常谨慎的问题。当特征变动较大时,其开发工具、浏览器环境都未必能调整过来。ES6 改动幅度非常大,而市面上绝大多数浏览器还在使用 ES5,ES6 并没有得到浏览器的原生支持,也几乎没有什么人用 ES6。

于是 Babel 的原型 6To5 应运而生。Babel 是 JavaScript 的编译器,它运行你愉快地编写 ES6,然后写好了在编译成 ES5 的代码,这样就解决了浏览器不兼容 ES6 规范的问题。可以说,ES6 的推广离不开 6To5 的功劳。

随着时间的发展,6to5 扩展了其功能,开始支持更多的 ES 标准和实验性语言特性,最终更名为 Babel,以反映其作为一个更通用的 JavaScript 编译器的身份。

支持提前尝试 ES 的新特征

Babel 汲取了 ES5 以前提案的一些教训。在 ES5 以前,每次提案都没法及时验证合理性,如果移开式设计的不合理,则一代又一代下去会变成一坨屎山。换了一种思路:既然可以把新规范改为旧规范的代码,那么有没有一种办法,提前尝鲜新规范呢?

于是 Babel 添加了对新规范中特种的支持,尤其是还未正式发布的新规范。它的好处就是提前尝试新规范,供开发者在实践中验证新特种的合理性。如果在使用中觉得某个特种设计的好久保留,不好就撤回修正,这比一拍脑袋就做决策发布规范科学的多嘛。也就是熟,Babel 可以引导 JavaScript 规范往合理处发展。

Babel 的编译流程

Babel 的编译流程可以分为三步:

  1. parse:通过使用 @babel/parser 负责将 es6 代码进行语法分析和词法分析后转换成抽象语法树,也就是我们熟知的 AST。
  2. transform:使用 @babel/traverse 来遍历 AST 并进行节点的操作,用它提供的 API 来编写对 AST 的遍历和修改逻辑,由此来将一种 AST 转换为另一种 AST。
  3. generator:@babel/generator 负责通过 AST 树生成 ES5 代码。

其中第二步的转化是重中之重,babel 的插件机制也是在这一步发挥作用的,plugins 在这里进行操作,转化成新的 AST,再交给第三步的@babel/generator。 所以如果没有这些 plugins 进驻平台,那么 babel 这个"平台"是不具备任何能力的。

而这整个过程,由@babel/core 负责编译过程的控制和管理。它会调用其他模块来解析、转换和生成代码。

plugin 和 preset

在 Babel 的处理过程中,插件(Plugins)和预设(Presets)的执行顺序非常重要

  1. 插件首先执行:在转换代码时,先指定的插件会先执行。如果你在 Babel 配置中列出了多个插件,它们将按照列出的顺序依次执行。

  2. 预设之后执行:预设(一组插件的集合)在所有单独指定的插件之后执行。如果你同时使用了插件和预设,那么所有的插件将首先按照指定的顺序执行,然后才轮到预设中的插件。

  3. 预设的执行顺序是倒序:不同于插件,如果你在配置中列出了多个预设,它们将按照倒序执行。也就是说,列表中最后的预设将首先被应用,然后是倒数第二个,以此类推。

如下代码示例所示:

json 复制代码
{
  "plugins": ["moment", "memo"],
  "presets": ["a", "b", "c"]
}

在上面这些示例中,plugins 运行顺序:moment、memo,而 presets 运行顺序: c、b、然后 a

那么问题来了,为什么是插件先执行?

在 Babel 的处理过程中,插件先于预设执行的设计选择主要是为了提供更精细的控制和灵活性。插件通常用于实现特定的、细粒度的代码转换,而预设则是一组针对特定目标的插件集合。通过先执行插件,我们可以优先应用一些特殊的、高优先级的转换规则,这些规则可能对后续的转换过程有重要影响。此外,这种设计也允许开发者更灵活地组合和定制代码转换的行为,以适应不同的开发需求和环境要求。

插件(Plugins)

在 Babel 中,插件(plugins)是用来转换 JavaScript 代码的工具,每个插件实现了特定的代码转换规则。它的本质就是一个 JS 程序, 指示着 Babel 如何对代码进行转换.所以你也可以编写自己的插件来应用你想要的任何代码转换.

插件使用案例

首先我们来看看插件的一些使用案例,假设我们要将箭头函数转换为普通函数,要想使用,首先我们要执行如下依赖包:

bash 复制代码
pnpm add --save-dev @babel/plugin-transform-arrow-functions @babel/core

编写如下代码:

js 复制代码
import { transformSync } from "@babel/core";
import pluginTransformArrowFunctions from "@babel/plugin-transform-arrow-functions";

const code = `
var a = () => {};
var a = b => b;

const double = [1, 2, 3].map(num => num * 2);
console.log(double); // [2,4,6]

var bob = {
  _name: "Bob",
  _friends: ["Sally", "Tom"],
  printFriends() {
    this._friends.forEach(f => console.log(this._name + " knows " + f));
  },
};
console.log(bob.printFriends());
`;
const result = transformSync(code, {
  plugins: [pluginTransformArrowFunctions],
});

console.log(result.code);

最终代码输出结果如下图所示:

第二个案例就是我们来看看对象扩展运算法这个插件,它可以将 ... 的语法转化为浏览器更容易识别的代码,要想使用,首先我们要执行如下依赖包:

bash 复制代码
pnpm add --save-dev @babel/plugin-transform-arrow-functions @babel/core

安装完成之后我们要编写如下代码:

js 复制代码
import { transformSync } from "@babel/core";
import pluginTransformObjectRestSpread from "@babel/plugin-transform-object-rest-spread";
import { writeFileSync } from "fs";

const code = `
  const x = 222;
  const y = {
    a: 1,
    b: 2,
  };

  const z = { x, ...y };

  console.log(z)
`;
const result = transformSync(code, {
  plugins: [pluginTransformObjectRestSpread],
});

writeFileSync("output.js", result.code);

执行该文件之后我们会生成一个新的文件,该文件中包含这些代码,如下图所示:

js 复制代码
function ownKeys(e, r) {
  var t = Object.keys(e);
  if (Object.getOwnPropertySymbols) {
    var o = Object.getOwnPropertySymbols(e);
    r &&
      (o = o.filter(function (r) {
        return Object.getOwnPropertyDescriptor(e, r).enumerable;
      })),
      t.push.apply(t, o);
  }
  return t;
}
function _objectSpread(e) {
  for (var r = 1; r < arguments.length; r++) {
    var t = null != arguments[r] ? arguments[r] : {};
    r % 2
      ? ownKeys(Object(t), !0).forEach(function (r) {
          _defineProperty(e, r, t[r]);
        })
      : Object.getOwnPropertyDescriptors
      ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t))
      : ownKeys(Object(t)).forEach(function (r) {
          Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
        });
  }
  return e;
}
function _defineProperty(obj, key, value) {
  key = _toPropertyKey(key);
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true,
    });
  } else {
    obj[key] = value;
  }
  return obj;
}
function _toPropertyKey(t) {
  var i = _toPrimitive(t, "string");
  return "symbol" == typeof i ? i : String(i);
}
function _toPrimitive(t, r) {
  if ("object" != typeof t || !t) return t;
  var e = t[Symbol.toPrimitive];
  if (void 0 !== e) {
    var i = e.call(t, r || "default");
    if ("object" != typeof i) return i;
    throw new TypeError("@@toPrimitive must return a primitive value.");
  }
  return ("string" === r ? String : Number)(t);
}
const x = 222;
const y = {
  a: 1,
  b: 2,
};
const z = _objectSpread(
  {
    x,
  },
  y
);
console.log(z);

最后执行该代码,输出结果如下图所示,输出了我们想要的结果了:

预设(Presets)

在 Babel 中,"Presets" 是一组预先配置的插件集合,用于简化 Babel 配置过程。每个预设包含了多个插件,旨在满足特定的编译目标或需求。以下是几个最常用的几个预设包括:

  1. @babel/preset-env:这是最流行的预设之一,它根据目标环境(如不同版本的浏览器或 Node.js)自动选择所需的 Babel 插件和 polyfills。

  2. @babel/preset-react:用于转换 React 的 JSX 语法。

  3. @babel/preset-typescript:用于转换 TypeScript 代码为 JavaScript。

预设可以极大地简化 Babel 配置,因为你不需要手动指定每个插件。通过使用预设,开发者可以轻松地为项目配置 Babel,以适应特定的开发或生产需求。

例如,我们经常在 React 项目中编写的 jsx 代码就是被@babel/preset-react 编译过的,我们先来安装两个需要使用到的包:

bash 复制代码
pnpm add --save-dev @babel/core @babel/preset-react

在这个例子中我们将使用非常简单的代码来看看它具体会编译成什么样子的代码,如下代码所示:

js 复制代码
import { transformSync } from "@babel/core";
import presetReact from "@babel/preset-react";

const code = "<div>Hello, React!</div>";
const result = transformSync(code, {
  presets: [presetReact],
});

console.log(result.code);

使用 node 执行该文件,最终代码的输出结果如下图所示:

到这里我们就应该对 Babel 的功能有一定的了解了,接下来我们要开始摆脱这种方式,要使用配置的方式来对这些配置进行学习了。

配置

在前面的内容中,我们只是介绍了一些傻瓜式的 demo,这仅仅只适合拿来玩的,在实际的项目使用中,我们都是使用配置文件来对 babel 来进行使用的。

Babel 有两种并行的配置文件格式,可以一起使用,也可以独立使用:

  1. 项目范围配置:

    • babel.config.* 文件,具有以下扩展名:.json、.js、.cjs、.mjs、.cts,例如 babel.config.js
  2. 文件相对配置:

    • .babelrc.* 文件,具有以下扩展名:.json、.js、.cjs、.mjs、.cts,例如 .babelrc.js

更多详细的配置文件信息请参考 官方文档

现在我们只需要在项目的根目录下创建一个 babel.config.js 文件,并编写如下代码:

js 复制代码
module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        targets: {
          edge: "60",
          chrome: "60",
        },
        useBuiltIns: "usage",
        corejs: { version: 3, proposals: true },
        debug: true,
        modules: false,
        forceAllTransforms: false,
        exclude: ["transform-typeof-symbol"],
      },
    ],
  ],
};

在上面的这个 @babel/preset-env 预设中,它用于帮助开发者将最新的 JavaScript 代码转换为向后兼容的版本。这样做的主要目的是确保开发者编写的现代 JavaScript 代码能够在不支持最新特性的老旧浏览器上运行。

接下来我们就来看下我们剩下的配置项都代表什么意思:

  1. targets:指定要支持的特定浏览器版本,这里配置了 Edge 和 Chrome 的版本。

  2. useBuiltIns:配置为 "usage",意味着 Babel 会根据代码中实际使用的特性,自动引入必要的 polyfills。

  3. corejs:指定 core-js 库的版本,并启用对提案中的特性的支持。

  4. debug:当设置为 true 时,Babel 会输出转换过程中的调试信息。

  5. modules:设置为 false,意味着不会将 ES6 模块转换为其他模块类型(如 CommonJS)。

  6. forceAllTransforms:如果为 false,Babel 只会对不支持的特性进行转换。

  7. exclude:排除特定的转换插件,这里排除了 "transform-typeof-symbol" 插件。

core-js 是一个模块化的标准库,提供了许多 JavaScript 中缺失的功能,特别是对那些较新的 ECMAScript 特性的 polyfill 支持。它包括对诸如 Promise、Symbol、Object.assign 等 ES2015+ 特性的兼容实现。此外,它还包含了一些额外的、非标准的特性,如某些实验性提案的 polyfills。
Polyfills 是一种浏览器技术,用于在旧浏览器中添加对最新 Web 标准的支持。它们是 JavaScript 代码片段,可以模拟较新的 API,使得在不支持这些特性的旧浏览器中也能使用这些特性。

这个时候我们要在 package.json 文件中添加如下代码:

json 复制代码
{
  "scripts": {
    "build": "babel src -d dist"
  }
}

这个时候执行 pnpm run build 就可以根据你的 babel.config.js 文件的配置然后对 src 目录下的文件进行编译。

在这里我们在 src/index.js 目录下添加如下代码:

js 复制代码
const fn = () => 1; // ES6箭头函数, 返回值为1
const num = 3 ** 2; // ES7求幂运算符

const obj = {
  a: 1,
  b: 2,
};

const rest = { ...obj, c: 3 }; // es9扩展运算符

const foo = function (a, b, c) {
  // ES7参数支持尾部逗号
  console.log("a:", a);
  console.log("b:", b);
  console.log("c:", c);
};

+foo(1, 3, 4);

console.log(fn());
console.log(num);
console.log(rest);

当我们执行 pnpm run build 的时候,发现代码输出并没有发生改变:

这是因为在 edge 和 chrome 最新版本中,这些语法都支持,所以他就没有必要转换了,它只会为目标浏览器中没有功能加载转换插件。

那么我们试着将目标浏览器修改更低版本的试试看:

diff 复制代码
module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        targets: {
-          edge: "120",
-          chrome: "120",
+          edge: "60",
+          chrome: "60",
        },
        useBuiltIns: "usage",
        corejs: { version: 3, proposals: true },
        debug: true,
        modules: false,
        forceAllTransforms: false,
        exclude: ["transform-typeof-symbol"],
      },
    ],
  ],
};

再次执行 pnpm run build 你会发现最终的输出结果如下所示:

你会发现上面的代码中加入了一些 polyfill,要想运行这些代码的话,我们还需要安装 core-js:

bash 复制代码
pnpm add core-js

我们执行编译生成的代码来看看运行结果,如下图所示:

你会发现上面的代码输出结果正如我们预期的一样。这个就得意我们强大的 polyfill 功能了。

参考资料

总结

通过这篇文章相信大家已经对 babel 有一个大概的认识了。在之后的文章来对 babel 的一些实现原理和架构方面进行详细讲解。

相关推荐
嘉琪0013 分钟前
Day4 完整学习包(this 指向)——2026 0313
前端·javascript·学习
前端小菜鸟也有人起3 分钟前
Vue3父子组件通信方法总结
前端·javascript·vue.js
peachSoda76 分钟前
小程序图片加载优化方案
前端·微信小程序·小程序
Maimai1080811 分钟前
React Server Components 是什么?一文讲清 CSR、Server Components 与 Next.js 中的客户端/服务端组件
前端·javascript·css·react.js·前端框架·html·web3
肉肉不吃 肉21 分钟前
事件循环,宏任务,微任务
前端·javascript
z止于至善27 分钟前
Vue ECharts:Vue 生态下的 ECharts 可视化最佳实践
前端·vue.js·echarts·vue echarts
℘团子এ27 分钟前
什么是Docker
前端·docker·容器
Software攻城狮28 分钟前
【el-table 表格组件 删除标头分割线】
前端·vue.js·elementui