Babel
可以干什么:
- 语法转换
- 通过
Polyfill
方式在目标环境中添加缺失的功能 - 源码转换(
codemods
)
项目中常用的Babel配置
首先,webpack 中 loader 的本质就是一个函数,接受我们的源代码作为入参同时返回新的内容。
我们日常使用的 babel 相关配置主要涉及以下三个相关插件:
-
babel-loader:本质就是一个函数,我们匹配到对应的文件交给 babel-loader
-
babel-core:babel-loader 是识别匹配文件和接受对应参数的函数。 babel 在编译代码过程中核心的库就是 @babel/core 这个库。他可以将我们的代码进行词法分析--语法分析--语义分析过程从而生成AST抽象语法树,从而对于"这棵树"的操作之后再通过编译称为新的代码。
-
babel-preset-env:告诉 babel 需要以为什么样的规则进行代码转义。
Preset
和 Plugin
Preset
就是一些 Plugin
组成的合集,可以将 Preset
理解成一些 Plugin
整合成的一个包。
例如,@babel/preset-env
是一个非常常用的 Preset
,它根据目标环境和配置的浏览器列表,自动选择和加载所需的插件,使得开发者无需手动选择和配置大量的插件。
babel-preset-env
将高版本 JavaScript 代码进行转译,根据内置的规则转译成为低版本的JavaScript 代码。
仅仅针对语法阶段的转译 ,比如转译箭头函数,const/let
语法。针对一些Api
或者Es 6
内置模块的 polyfill
,preset-env
是无法进行转译的。
常见的 Plugin 其实大多数都集成在了 babel-preset-env 中。
@babel/preset-env
不会包含任何低于 Stage 3 的 JavaScript 语法提案 。如果需要兼容低于 Stage 3
阶段的语法则需要额外引入对应的 Plugin
进行兼容。
ECMAScript(简称 ES)是 JavaScript 的标准化版本,它的新特性通常会经过一个由 TC39(ECMA 技术委员会)管理的提案过程,分为五个阶段,分别是 Stage 0 到 Stage 4。这些阶段代表了新特性从最初的提议到最终成为标准的过程。 具体各个阶段的含义如下:
- Stage 0 - Strawman(草案):此阶段是为了提出初步想法而设立的,通常由个人或小组提出,而不是由 TC39 委员会批准的提案。这些提案可能只是一个想法的草图,并不具有实际应用性。
- Stage 1 - Proposal(提案):在这个阶段,提案已经详细说明了设计、语法和语义,并且经过了初步讨论。该阶段要求提供详细的说明文档,以及实现和用户反馈等。
- Stage 2 - Draft(草案):在这个阶段,提案已经完整地描述了其语法和语义,并且有了初步的实现。这意味着提案已经成为一个可供实际使用的草案。
- Stage 3 - Candidate(候选):在这个阶段,提案已经基本完成,所有方面都已经得到了充分的讨论和评审,并且已经有了至少一个实现。这意味着提案已经足够成熟,可以供开发者使用和提供反馈。
- Stage 4 - Finished(完成):在这个阶段,提案已经通过了所有的评审流程,并且已经被加入到了 ECMAScript 的标准中。这意味着该特性已经成为了 JavaScript 的一部分,并且可以在任何支持该版本标准的环境中使用。
因此,当说一个特性处于 Stage 3 时,意味着它已经相对成熟,具有可用性,并且很可能会最终成为 ECMAScript 标准的一部分。
babel-preset-react
将 React 中的 jsx 进行转译。
babel-preset-typescript
对于 TypeScript
代码,有两种方式去编译 TypeScript 代码成为JavaScript代码。
- 使用 tsc 命令,结合 cli 命令行参数方式或者 tsconfig 配置文件进行编译 ts 代码。
- 使用 babel ,通过 babel-preset-typescript 代码进行编译 ts 代码。
Babel 的 polyfill
首先我们来理清楚这三个概念:
- 最新 ES 语法,比如:箭头函数,
let/const
。 - 最新 ES Api ,比如 Promise
- 最新 ES 实例/静态方法,比如
String.prototype.include
babel-prest-env 仅仅只会转化最新的 es 语法 ,并不会转化对应的 Api 和实例方法,比如说 ES 6 中的 Array.from
静态方法。 babel 是不会转译这个方法的,如果想在低版本浏览器中识别并且运行 Array.from
方法达到我们的预期就需要额外引入 polyfill
进行在 Array 上添加实现这个方法。
整体而言就是,语法层面的转化 preset-env 完全可以胜任。但是一些内置方法模块,仅仅通过 preset-env 的语法转化是无法进行识别转化的 ,所以就需要一系列类似"垫片"的工具进行补充实现这部分内容的低版本代码实现。这就是所谓的 polyfill 的作用。
针对于 polyfill 方法的内容,babel 涉及如下:
-
@babel/polyfill:(从 Babel 7.4.0 版本开始,这个软件包已经不建议使用了,建议直接包含
core-js/stable
。)通过往全局对象上添加属性以及直接修改内置对象的Prototype上添加方法实现 polyfill 。比如说我们需要支持String.prototype.include
,在引入 babelPolyfill 这个包之后,它会在全局 String 的原型对象上添加 include 方法从而支持我们的Js Api。这种方式本质上是往全局对象/内置对象上挂载属性,所以这种方式难免会造成全局污染。useBuiltIns 决定了如何在 preset-env 中使用 @babel/polyfill。
js{ "presets": [ ["@babel/preset-env", { "useBuiltIns": false }] ] }
他有三个可选值:
- false:默认值。它表示仅仅会转化最新的ES语法,并不会转化任何Api和方法。
- entry :需要我们在项目入口文件中手动引入一次core-js:
import "core-js/stable";
,它会根据我们配置的浏览器兼容性列表(browserList)然后全量引入不兼容的polyfill 。也就是说比如我们代码中仅仅使用了Array.from这个方法。但是polyfill并不仅仅会引入Array.from,同时也会引入Promise、Array.prototype.include等其他并未使用到的方法。这就会造成包中引入的体积太大了。 - usage :会根据配置的浏览器兼容,以及代码中 使用到的Api 进行引入polyfill按需添加 。如果我们存在很多个模块,那么无疑会多出很多冗余代码(import语法)。
-
@babel/runtime:@babel/polyfill是存在污染全局变量的副作用,在实现polyfill时Babel还提供了另外一种方式去让我们实现这功能,那就是@babel/runtime。
-
简单来讲,@babel/runtime更像是一种按需加载的解决方案,比如哪里需要使用到Promise,@babel/runtime就会在他的文件顶部添加
import promise from 'babel-runtime/core-js/promise'
。 -
preset-env的useBuintIns配置项,我们的polyfill是preset-env帮我们智能引入。而babel-runtime则会将引入方式由智能完全交由我们自己,我们需要什么自己引入什么。
存在如下问题:
- babel-runtime无法做到智能化分析,需要我们手动引入。
- babel-runtime编译过程中会重复生成冗余代码(一些工具函数)。
-
-
@babel/plugin-transform-runtime:和run-time配合使用,解决上述我们提到的run-time存在的问题而提出的插件。
- @babel/plugin-transform-runtime插件会智能化的分析我们的项目中所使用到需要转译的js代码,从而实现模块化从babel-runtime中引入所需的polyfill实现。
- @babel/plugin-transform-runtime插件提供了一个helpers参数。开启后可以将上边提到编译阶段重复的工具函数代码转化称为require语句。此时,这些工具函数就不会重复的出现在使用中的模块中了。
两者的选择:
- 如果项目对打包体积要求很高,或者想要避免全局污染,推荐使用 @babel/plugin-transform-runtime + @babel/runtime。
- 如果想要快速方便地启用所有可能需要的 polyfills,并且不需要担心打包体积,可以使用 useBuiltIns + @babel/polyfill。