webpack配置babel篇

  • 2020 年 11 月 12 日
  • 笔记

babel-polyfill & babel-runtime & babel-preset-env

babel-core

babel-core 的作用是把 js 代码分析成 ast ,方便各个插件分析语法进行相应的处理。(ast=抽象语法 # 树)

babel-polyfill

babel-polyfill 是为了模拟一个完整的ES2015+环境,旨在用于应用程序而不是库/工具。并且使用babel-node时,这个polyfill会自动加载。这里要注意的是babel-polyfill是一次性引入你的项目中的,并且同项目代码一起编译到生产环境。而且会污染全局变量。像Map,Array.prototype.find这些就存在于全局空间中。

babel-runtime

babel-runtime不会污染全局空间和内置对象原型。事实上babel-runtime是一个模块,你可以把它作为依赖来达成ES2015的支持。

比如环境不支持Promise,你可以在项目中加入

require(‘babel-runtime/core-js/promise’)

来获取Promise。

这样我们就弥补了babel-polyfill的缺点,达到了按需加载的效果。但是在实际项目开发过程中,我们往往会写很多新的es6 api,每次都要手动引入相应的包比较麻烦,维护起来也不方便,每个文件重复引入也造成代码的臃肿。

要解决这个问题,就要用到 babel-plugin-transform-runtime,它会分析我们的 ast 中,是否有引用 babel-rumtime 中的垫片(通过映射关系),如果有,就会在当前模块顶部插入我们需要的垫片。

接下来我们尝试一下,先安装babel-runtime和babel-plugin-transform-runtime

npm install --save babel-runtime
npm install --save-dev babel-plugin-transform-runtime

由于 babel-runtime只是集中了polyfill的library,对应需要的 polyfill 都是要引入项目中,并跟项目代码一起打包的,所以就要加入到生产环境依赖中

下面在.babelrc中加入以下配置

{
  "plugins": ["transform-runtime"]
}

执行打包命令,打包出来的bundle.js的大小为63K,比完整引入polyfill小了好多。但是事物都有两面性,babel-runtime有个缺点,它不模拟实例方法,即内置对象原型上的方法,所以类似Array.prototype.find,你通过babel-runtime是无法使用的,这只能通过 babel-polyfill 来转码,因为 babel-polyfill 是直接在原型链上增加方法。这就悲催了,难道还是要完整引入babel-polyfill?其实还有一个解决的办法,就是用babel-preset-env

babel-runtime的相关配置

core-js

core-js 是用于 JavaScript 的组合式标准化库,它包含 es5 (e.g: object.freeze), es6的 promise,symbols, collections, iterators, typed arrays, es7+提案等等的 polyfills 实现。也就是说,它几乎包含了所有 JavaScript 最新标准的垫片。不过为什么它不把 generator 也实现了… 😁

// 比如,只不过需要单个引用
require('core-js/array/reduce');
require('core-js/object/values');

regenerator

它是来自于 facebook 的一个库,链接。主要就是实现了 generator/yeild, async/await。

所以 babel-runtime 是单纯的实现了 core-js 和 regenerator 引入和导出,比如这里是 filter 函数的定义,做了一个中转并处理了 esModule 的兼容。

module.exports = { "default": require("core-js/library/fn/array/filter"), __esModule: true };

helpers

它把每个 helper 都单独放到一个文件夹里。这样,配合 transform-runtime 使用的时候,需要用 helper 转化的时候,就从 babel-runtime 中直接引用了。

var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator');

var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2);

文件结构:

 

文件结构

 

使用

可以单独引入require('babel-runtime/core-js/object/values');

不过这些模块都做了 esModule 的兼容处理,也就是上面引入的模块是{ "default": require("core-js/library/fn/array/filter"), __esModule: true }这样的,要使用还得加上 .default。所以我们期待的是,最好能有帮我们自动处理的插件,babel-plugin-transform-runtime就是用来做这个的。这个我们放到 plugin 去讲。

babel-preset-env

1、babel-preset-env 能根据当前的运行环境,自动确定你需要的 plugins 和 polyfills。通过各个 es标准 feature 在不同浏览器以及 node 版本的支持情况,再去维护一个 feature 跟 plugins 之间的映射关系,最终确定需要的 plugins。
2、单独使用时,只能转化部分语法箭头函数,let,const之类的,不能转化内置对象,实例方法等。如果进一步需要转换内置对象、实例方法,那就得用polyfill。

“presets”: [ [“env”, { “targets”: { “chrome”: 52, “browsers”: [“last 2 versions”, “safari 7”] }, “modules”: false, “useBuiltIns”: “usage”, “debug”: false }] ] }
 
其中的useBuiltIns就是是否开启自动支持 polyfill,它能自动给每个文件添加其需要的poly-fill。
重要的参数 “useBuiltIns”,他是控制 @babel/preset-env 使用何种方式帮我们导入 polyfill 的核心

entry

这是一种入口导入方式, 只要我们在打包配置入口 或者 文件入口写入 import "core-js" 这样一串代码, babel 就会替我们根据当前你所配置的目标浏览器(browserslist)来引入所需要的polyfill 。

useage

这个就比较神奇了, useBuiltIns = useage 时,会参考目标浏览器(browserslist) 和 代码中所使用到的特性来按需加入 polyfill

当然, 使用 useBuiltIns = useage, 还需要填写另一个参数 corejs 的版本号,

core-js 支持两个版本, 2 或 3, 很多新特性已经不会加入到 2 里面了, 比如: flat 等等最新的方法, 2 这个版本里面都是没有的, 所以建议大家用3

.babelrc

 
{
  "presets": [
    ["@babel/preset-env", 
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}
这种方式打包体积不大,但是如果我们排除node_modules/目录,遇上没有经过转译的第三方包,就检测不到第三方包内部的 ‘hello‘.includes(‘h‘)这种句法,这时候我们就会遇到bug,
但是不排除的话又耗费性能。

false

剩下最后一个 useBuiltIns = false , 那就简单了, 这也是默认值 , 使用这个值时不引入 polyfill。

options.targets

用来配置需要支持的的环境,不仅支持浏览器,还支持node。

如果没有配置targets选项,就会读取项目中的browserslist配置项。

options.loose

默认值是false,如果preset-env中包含的plugin支持loose的设置,那么可以通过这个字段来做统一的设置。

options.modules

"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默认值是auto

用来转换ES6的模块语法。如果使用false,将不会对文件的模块语法进行转化。

如果要使用webpack中的一些新特性,比如tree shaking 和 sideEffects,就需要设置为false,对ES6的模块文件不做转化,因为这些特性只对ES6的模块有效。

options.useBuiltIns

"usage" | "entry" | false,默认值是false

这个配置项主要是用来处理@babel/polyfill。

  • 设置为false时,会把@babel/polyfill这个包引进来,忽略targets配置项和项目里的browserslist配置
  • 设置为entry时,在整个项目里,只需要引入一次@babel/polyfill,它会根据targets和browserslist,然后只加载目标环境不支持的api文件
  • 设置为usage时,babel会在目标环境的基础上,只加载项目里用的那些不支持的api的文件,做到按需加载

其他的一些配置项可以看 官方文档

babel-runtime和plugin-transform-runtime区别

babel-runtime这种方式会借助 helper function 来实现特性的兼容,
并且利用 @babel/plugin-transform-runtime 插件还能以沙箱垫片的方式防止污染全局, 并抽离公共的 helper function , 以节省代码的冗余

也就是说 @babel/runtime 是一个核心, 一种实现方式, 而 @babel/plugin-transform-runtime 就是一个管家, 负责更好的重复使用 @babel/runtime

@babel/plugin-transform-runtime 插件也有一个 corejs 参数需要填写

版本2 不支持内置对象 , 但自从Babel 7.4.0 之后,拥有了 @babel/runtime-corejs3 , 我们可以放心使用 corejs: 3 对实例方法做支持

当前的 .babelrc

 
{
  "presets": [
    ["@babel/preset-env"]
  ],
  "plugins": [
    [ 
      "@babel/plugin-transform-runtime", {
        "corejs": 3
      }
    ]
  ]
}

babel-preset-env和plugin-transform-runtime区别

使用 @babel/plugin-transform-runtime 编译后的代码和之前的 @babel/preset-env 编译结果不一样,
它使用了帮助函数, 并且赋予了别名 , 抽出为公共方法, 实现复用。 比如它用了 _Promise 代替了 new Promise , 从而避免了创建全局对象

preset-env 和 plugin-transform-runtime同时使用

useage 和 @babel/runtime 同时使用的情况下比较智能, 并没有引入重复的 polyfill

个人分析原因应该是: babel 的 plugin 比 prset 要先执行, 所以preset-env 得到了 @babel/runtime 使用帮助函数包装后的代码,而 useage 又是检测代码使用哪些新特性来判断的, 所以它拿到手的只是一堆 帮助函数, 自然没有效果了

.babelrc

{
  "presets": [
    ["@babel/preset-env", 
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ],
  "plugins": [
    [ 
      "@babel/plugin-transform-runtime", {
        "corejs": 3
      }
    ]
  ]
}

entry 和 @babel/runtime

跟 useage 的情况不一样, entry 模式下, 在经过 @babel/runtime 处理后不但有了各种帮助函数还引入了许多polyfill, 这就会导致打包体积无情的增大

个人分析: entry 模式下遭遇到入口的 import "core-js" 及就立即替换为当前目标浏览器下所需的所有 polyfill, 所以也就跟 @babel/runtime 互不冲突了, 导致了重复引入代码的问题, 所以这两种方式千万不要一起使用, 二选一即可

{
  "presets": [
    ["@babel/preset-env", 
      {
        "useBuiltIns": "entry"
      }
    ]
  ],
  "plugins": [
    [ 
      "@babel/plugin-transform-runtime", {
        "corejs": 3
      }
    ]
  ]
}

@babel/plugin-transform-runtime默认情况下安装@babel/runtime这个库,即corejs为false时使用;
当corejs设置为2时,需要安装使用@babel/runtime-corejs2。
runtime和runtime-corejs2这两个库唯一的区别是,corejs2这个库增加了对core-js这个库的依赖,
而core-js是用来对ES6各个语法polyfill的库,所以在corejs为false的情况下,只能做语法的转换,并不能polyfill任何api。

总结

  1. @babel/preset-env 拥有根据 useBuiltIns 参数的多种polyfill实现,优点是覆盖面比较全(entry), 缺点是会污染全局, 推荐在业务项目中使用

    • entry 的覆盖面积全, 但是打包体积自然就大,
    • useage 可以按需引入 polyfill, 打包体积就小, 但如果打包忽略node_modules 时如果第三方包未转译则会出现兼容问题
  2. @babel/runtime 在 babel 7.4 之后大放异彩, 利用 corejs 3 也实现了各种内置对象的支持, 并且依靠 @babel/plugin-transform-runtime 的能力,沙箱垫片和代码复用, 避免帮助函数重复 inject 过多的问题, 该方式的优点是不会污染全局, 适合在类库开发中使用

    上面 1, 2 两种方式取其一即可, 同时使用没有意义, 还可能造成重复的 polyfill 文件

对比以上三种方案,我们得出以下结论

方案 打包后大小 优点 缺点
babel-polyfill 259K 完整模拟ES2015+环境 体积过大;污染全局对象和内置的对象原型
babel-runtime 63K 按需引入,打包体积小 不模拟实例方法
babel-preset-env(开启useBuiltIns) 194K 按需引入,可配置性高

方案没有绝对的优劣,在开发过程中还是要根据实际情况灵活运用。

 

几个包的包含关系

   babel-polyfill仅仅是引用core-js、regenerator-runtime这两个npm包。

   @babel/runtime包含两个文件夹:helpers(定义了一些处理新的语法关键字的帮助函数)、regenerator(仅仅是引用regenerator-runtime这个npm包)。

   @babel/runtime-corejs2包含三个文件夹:core-js(引用core-js这个npm包)、helpers(定义了一些处理新的语法关键字的帮助函数)、regenerator(仅仅是引用regenerator-runtime这个npm包)。

   可以看出,@babel/runtime-corejs2≈@babel/runtime + babel-polyfill:

   @babel/runtime只能处理语法关键字,而@babel/runtime-corejs2还能处理新的全局变量(例如,Promise)、新的原生方法(例如,String.padStart );

   使用了@babel/runtime-corejs2,就无需再使用@babel/runtime了。