webpack入门之js处理(babel、babel polyfill)
简介
我们都知道,babel
是用来编译js
的,就是把高版本的js
编译成低版本的js
,以便浏览器识别。但是对于babel
更深入点可能就不是很清楚了,所以笔者今天再来简单总结下
看完本文你将学到:
- 知道babel的核心包
- 怎么配置和使用babel
- babel polyfill概念以及使用
- babel结合webpack的使用
Babel 是什么?
Babel
是一个 JavaScript
编译器。
Babel
是一个工具链,主要用于将采用 ECMAScript 2015+
语法编写的代码转换为向后兼容的 JavaScript
语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
下面列出的是 Babel
能为你做的事情:
- 语法转换:高级语言特性的降级
- polyfill:通过 Polyfill 方式在目标环境中添加缺失的特性
- 源码转换:我们可以将 jsx、vue 代码转换为浏览器可识别的 JS 代码。
babel 的编译流程
babel 是 source to source 的转换,整体编译流程分为三步:
- parse:通过 parser 把源码转成抽象语法树(AST)
- transform:遍历 AST,调用各种 transform 插件对 AST 进行增删改
- generate:把转换后的 AST 打印成目标代码,并生成 sourcemap
简单总结一下就是:为了让计算机理解代码需要先对源码字符串进行 parse,生成 AST,把对代码的修改转为对 AST 的增删改,转换完 AST 之后再打印成目标代码字符串。
核心库 @babel/core
@babel/core
是babel
最核心的一个编译库,他可以将我们的代码进行词法分析--语法分析--语义分析过程从而生成AST
抽象语法树,从而对于“这棵树”的操作之后再通过编译成为新的代码。
CLI命令行工具 @babel/cli
@babel/cli
是babel
提供的命令行工具,它主要是提供 babel
这个命令。
安装了@babel/cli
后我们就可以使用babel
命令来编译js
文件了。将src
目录下的js
编译到lib
目录下。
./node_modules/.bin/babel src --out-dir lib
普通编译
为了更方便的操作我们创建一个项目
mkdir babeltest
然后创建package.json
cd babeltest
npm init
然后来安装下babel
的两个包
npm install --save-dev @babel/core @babel/cli
然后创建需要编译的源文件,在src
目录下创建index1.js
// src/index1.js
const fn = () => 1; // ES6箭头函数, 返回值为1
console.log(fn());
在package.json
定义编译脚本
"scripts": {
"index1": "babel src/index1.js --out-dir dist",
},
运行脚本进行编译
我们运行npm run index1
,就会执行babel
的编译,会把index1.js
进行编译。我们来看看编译后的效果。
啊,啥都没变,编译前后的代码是完全一样的,这是咋回事?
因为 Babel
虽然开箱即用,但是什么动作也不做,如果想要 Babel
做一些实际的工作,就需要为其添加插件(plugin
)或者预设(preset
)。
好吧,我们先来说说插件
使用插件进行编译
还是上面的例子,我们来使用帮助我们进行编译。
因为我们的源代码使用了es6
的箭头函数,所以我们安装一个转换箭头函数的插件@babel/plugin-transform-arrow-functions
npm install --save-dev @babel/plugin-transform-arrow-functions
插件虽然安装好了,但是要怎么使用呢?这就需要用到babel
的配置文件啦!我们创建一个babel.config.json
文件(需要 v7.8.0
或更高版本),并在plugins
里面配置好我们安装的插件就可以啦。
// babel.config.json
{
"plugins": ["@babel/plugin-transform-arrow-functions"]
}
我们运行npm run index1
,再次进行编译,我们来看看编译后的效果。
箭头函数被转换成普通函数啦,达到我们预期的效果啦。
我们再来添加一个es6
的新特性,解构赋值
我们运行npm run index1
,再次进行编译,我们来看看编译后的效果。
可以发现,由于我们只安装了转换箭头函数的插件,所以它只转换了箭头函数,对于解构这个新特性并没有进行编译。
天啊,ES
的新语法这么多,不会要我们一个一个去安装插件吧,那何时才能配置完呀?
关于插件,我们可以在插件列表查看所有的babel
插件。
其实babel
早就为我们考虑到了,预设(preset
)能完美解决这个问题。
那预设又是什么呢?
使用预设进行编译
简单理解,预设就是一组插件,相当于你只要安装了我这么一个预设,就能享受到我这个预设里面所有的插件。
官方 Preset
有如下几个
- @babel/preset-env,将高版本js编译成低版本js
- @babel/preset-flow,对使用了flow的js代码编译成js文件
- @babel/preset-react,编译react的jsx文件
- @babel/preset-typescript,将ts文件编译成js文件
下面我们使用@babel/preset-env
这个预设来进行编译。
@babel/preset-env
主要作用是对我们所使用的并且目标浏览器中缺失的功能进行代码转换和加载 polyfill
,在不进行任何配置的情况下,@babel/preset-env
所包含的插件将支持所有最新的JS特性(ES2015,ES2016等,不包含 stage 阶段),将其转换成ES5
代码。
首先我们安装@babel/preset-env
这个预设
npm install --save-dev @babel/preset-env
然后在babel.config.json
进行配置
// babel.config.json
{
"presets": ["@babel/preset-env"]
}
我们运行npm run index1
,再次进行编译,我们来看看编译后的效果。
可以看到,我们的解构语法也被转换好了。
需要说明的是,@babel/preset-env
会根据你配置的目标环境,生成插件列表来编译。对于基于浏览器或 Electron
的项目,官方推荐使用 .browserslistrc
文件来指定目标环境。默认情况下,如果你没有在 Babel
配置文件中(如babel.config.json
)设置 targets
或 ignoreBrowserslistConfig
,@babel/preset-env
会使用 browserslist
配置源。
.browserslistrc
默认值是 > 0.5%, last 2 versions, Firefox ESR, not dead。
如果你不是要兼容所有的浏览器和环境,推荐你指定目标环境,这样你的编译代码能够保持最小。
所以我们配置下目标环境,只需要兼容最近的两个Chrome
版本。
// babel.config.json
{
"targets": "last 2 Chrome versions"
}
我们运行npm run index1
,再次进行编译,我们来看看编译后的效果。
可以发现,源码和编译后的代码居然是一样的。为什么呢?因为最近的两个Chrome
版本它是原生支持箭头函数和解构赋值的所以根本就不需要进行编译成低版本的js
代码。
所以对于目标环境的配置是非常重要的。配置的好能大大减小我们代码的体积。
插件和预设的执行顺序
-
插件在预设前运行。
-
插件顺序从前往后排列。
-
预设顺序是从后往前(颠倒的)。
例如:
{
"plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
先执行 transform-decorators-legacy
,在执行 transform-class-properties
。
重要的时,preset
的顺序是 颠倒的。如下设置:
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
将按如下顺序执行: 首先是 @babel/preset-react
,然后是 @babel/preset-env
。
插件和预设的参数
插件和预设都可以接受参数,参数由插件名和参数对象组成一个数组,可以在配置文件中设置。
如果不指定参数,下面这几种形式都是一样的:
{
"plugins": ["pluginA", ["pluginA"], ["pluginA", {}]]
}
要指定参数,请传递一个以参数名作为键(key)的对象。
{
"plugins": [
[
"transform-async-to-module-method",
{
"module": "bluebird",
"method": "coroutine"
}
]
]
}
预设的设置参数的方式和插件完全相同:
{
"presets": [
[
"env",
{
"loose": true,
"modules": false
}
]
]
}
babel的配置文件
babel
的配置文件支持很多种格式。
babel.config.json
官方建议使用 babel.config.json
格式的配置文件。
{ "presets": [...], "plugins": [...] }
babel.config.js
module.exports = function (api) {
api.cache(true);
const presets = [ ... ];
const plugins = [ ... ];
return {
presets,
plugins
};
}
.babelrc.json
{ "presets": [...], "plugins": [...] }
.babelrc.js
const presets = [];
const plugins = [];
module.exports = { presets, plugins };
.babelrc
{
"presets": [],
"plugins": []
}
还可以放到package.json
{
"name": "my-package",
"version": "1.0.0",
"babel": { "presets": [ ... ], "plugins": [ ... ], }
}
@babel/polyfill
@babel/polyfill 模块包含 core-js 和一个自定义的 regenerator runtime 来模拟完整的 ES2015+ 环境。(不包含第4阶段前的提议)。
这里的第4阶段前的提议不包括是什么意思呢?
这就需要了解一个新语法的诞生过程了。我们知道,ES
每年都会更新,那这些新特性是怎么推出来的呢?
其实新语法的诞生包含五个过程。它不是一蹴而就而是一步一步诞生出来的。
- Stage 0 - 设想(Strawman):只是一个想法,可能有 Babel插件。
- Stage 1 - 建议(Proposal):这是值得跟进的。
- Stage 2 - 草案(Draft):初始规范。
- Stage 3 - 候选(Candidate):完成规范并在浏览器上初步实现。
- Stage 4 - 完成(Finished):将添加到下一个年度版本发布中。
所以,只有当到了Stage 4
才是确定要新增的新特性,所以@babel/polyfill
才会支持。
说了这么多@babel/polyfill
到底是个啥?我还是不太明白。
其实,说直白点,@babel/polyfill
就是一个垫片。因为语法转换只是将高版本的语法转换成低版本的,但是新的内置函数、实例方法无法转换。这时,就需要使用 polyfill
上场了,顾名思义,polyfill
的中文意思是垫片,所谓垫片就是垫平不同浏览器或者不同环境下的差异,让新的内置函数、实例方法等在低版本浏览器中也可以使用。
比如说我们需要支持String.prototype.include
,在引入babelPolyfill
这个包之后,它会在全局String
的原型对象上添加include
方法从而支持我们的Js Api
。
我们说到这种方式本质上是往全局对象/内置对象上挂载属性,所以这种方式难免会造成全局污染。
下面笔者演示下@babel/polyfill
的使用。
笔者创建了一个index2.js
文件,里面使用了新的includes
方法
这里我们只使用了@babel/preset-env
预设来进行代码的编译
// babel.config.json
{
"presets": ["@babel/preset-env"]
}
我们编译看看编译后的代码
发现居然编译后的代码和源代码基本上一样,这在低版本浏览器显然是运行不了的,因为低版本浏览器肯定是不支持新特性includes
方法。
所以就需要使用到@babel/polyfill
啦
首先,安装 @babel/polyfill
依赖:
npm install --save @babel/polyfill
我们需要将完整的 polyfill
在代码之前加载,修改我们的 src/index2.js
,在最开始引入@babel/polyfill
然后我们再次编译
可以看到,编译后的代码就是把@babel/polyfill
全部引入了。这样固然是不会再报错了,不过,很多时候,我们未必需要完整的 @babel/polyfill
,这会导致我们最终构建出的包的体积增大,@babel/polyfill
的包大小为99K
。
我们更期望的是,如果我使用了某个新特性,再引入对应的 polyfill
,避免引入无用的代码。
配置@babel/preset-env实现按需引入
@babel/preset-env
是支持配置polyfill
的,并且支持按需和全量引入。
在babel-preset-env
中存在一个useBuiltIns
参数,这个参数决定了如何在preset-env
中使用@babel/polyfill
。
false
当我们使用preset-env
传入useBuiltIns
参数时候,默认为false
。它表示仅仅会转化最新的ES
语法,并不会转化任何Api
和方法。
entry
当传入entry
时,需要我们在项目入口文件中手动引入一次core-js
,它会根据我们配置的浏览器兼容性列表(browserList
)然后全量引入不兼容的polyfill
。
如果是Babel7.4.0
之前,我们需要在入口文件引入@babel/polyfill
// core-js 2.0中是使用"@babel/polyfill"
import "@babel/polyfill";
const arr1 = [1, 2, 3, 4, 5, 6, 7, 8];
const result1 = arr1.includes(8);
console.log(result1);
如果是Babel7.4.0
之后,我们需要在入口文件引入core-js/stable
和regenerator-runtime/runtime
// core-js3.0版本中变化成为了下面两个包
import "core-js/stable";
import "regenerator-runtime/runtime";
const arr1 = [1, 2, 3, 4, 5, 6, 7, 8];
const result1 = arr1.includes(8);
console.log(result1);
这种方式就跟我们前面说的使用@babel/polyfill
差不多了,不管用没用到都引入,肯定会加大构建后包的体积。
usage
当我们配置useBuintIns:usage
时,会根据配置的浏览器兼容,以及代码中 使用到的Api
进行引入polyfill
按需添加。
当使用usage
时,我们不需要额外在项目入口中引入polyfill
了,它会根据我们项目中使用到的自动进行按需引入。
所以,如果我们想实现按需引入,我们肯定要配置成usage
。
// babel.config.json
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3"
}
]
]
}
这里我们使用的是corejs: 3
。首先说一下使用 core-js@3
的原因,core-js@2
分支中已经不会再添加新特性,新特性都会添加到 core-js@3
。例如你使用了 Array.prototype.flat()
,如果你使用的是 core-js@2
,那么其不包含此新特性。为了可以使用更多的新特性,建议大家使用 core-js@3
。
注意,core-js@3
需要我们手动安装。不会默认安装。
npm install --save core-js@3
我们来看看编译后的效果
只引入了我们需要的core-js/modules/es.array.includes.js
,这样就达到了按需引入的目的。
我们再来看看另外一个例子,假设有一个class
需要编译。
class People {}
我们使用 corejs3
和usage
的方式进行编译。
// babel.config.json
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3"
}
]
]
}
我们来看看编译后的结果
可以发现,它会给我们定义一个_classCallCheck
辅助函数。如果我们编译的源码里面有很多class
,那岂不是就会定义很多个这样的函数。这显然是不符合我们要求的。
usage
按需引入优缺点分析
通过前面使用usage
按需引入的两个例子,我们可以发现虽然preset-env
能支持按需引入,但是会有两个问题。
第一,就是引入的代码是全局的,容易污染环境。
第二,就是辅助函数在每个文件都会定义,比如上面的class
编译,就会单独定义_classCallCheck
辅助函数。
要解决这两个问题也很简单,那就是使用@babel/plugin-transform-runtime
插件。
@babel/plugin-transform-runtime
@babel/plugin-transform-runtime
的功能就很强大了,它能搭配不同的runtime
来起到不同的作用。
我们首先来安装一下
npm i @babel/plugin-transform-runtime -D
我们先来看看搭配@babel/runtime
的效果。
搭配@babel/runtime
@babel/runtime
工具包提供很多内联辅助函数。
我们首先安装@babel/runtime
npm install --save @babel/runtime
然后修改我们的babel.config.js
。并将@babel/plugin-transform-runtime
作为插件使用。
{
"presets": [
[
"@babel/preset-env"
]
],
"plugins": ["@babel/plugin-transform-runtime"]
}
我们再来编译我们前面的class
class People {}
可以看到,这次辅助函数classCallCheck
并不是定义的,而是从@babel/runtime
包中引入的。
这样,通过从@babel/runtime
包导入辅助函数的方式,能大大减少我们构建后代码的体积。这也就解决了我们上面的第二个问题。
由于@babel/runtime
包只有辅助函数,并没有polyfill
,所以当我们的源代码需要用到polyfill
的时候就会有问题。
比如上面的includes
的例子。
const arr1 = [1, 2, 3, 4, 5, 6, 7, 8];
const result1 = arr1.includes(8);
console.log(result1);
我们编译来看看
发现编译后的代码居然和源码一样,这在低版本浏览器显然是运行不了的,因为低版本浏览器肯定是不支持新特性includes
方法。
这又怎么解决呢?那就得搭配@babel/runtime-corejs3
使用了。
搭配@babel/runtime-corejs3
@babel/runtime-corejs3
这个又是什么呢?
@babel/runtime-corejs3
工具包提供内联辅助函数,并且还提供非全局的 core-js3
版本的 API
。
怎么理解呢?其实就是除了上面的@babel/runtime
外还提供了core-js3
版本的 polyfill
。所以有了这个工具包,我们就既能将辅助函数提取出来,还能自动按需引入polyfill
。
我们来测试下,
首先还是安装
npm install --save @babel/runtime-corejs3
然后修改我们的babel.config.js
。并将@babel/plugin-transform-runtime
作为插件使用,并配置corejs3
。
{
"presets": [
[
"@babel/preset-env"
]
],
"plugins": [["@babel/plugin-transform-runtime", { "corejs": "3" }]]
}
我们再来编译前面class
和includes
的例子。可以看到,辅助函数classCallCheck
是从@babel/runtime-corejs3/helpers
中引入的,并且includes
也是从@babel/runtime-corejs3/core-js-stable
引入的。
而且我们还发现,使用@babel/plugin-transform-runtime
还会自动重命名,比如上面的includes
会被重命名为_includes
。重命名后的好处就是 polyfill
不污染全局。
这也就解决了我们上面使用@babel/preset-env
的两个问题了。
对比总结
我们知道,@babel/polyfill
、@babel/preset-env
和 @babel/plugin-transform-runtime
都能用来引入polyfill
的。那到底该怎么选择呢?
@babel/polyfill
不用多说了,肯定不是首选,因为它全局引入,并且还会污染环境。
@babel/preset-env
的 usage
方案其实就是按需引入@babel/polyfill
,所以它不会全局引入,但是它直接引入的polyfill
会污染全局环境,并且每次还会创建多余的辅助函数,会增大构建后代码体积。
@babel/plugin-transform-runtime
插件搭配@babel/runtime
或者@babel/runtime-corejs3
优势就是
- 抽离重复注入的
helper
代码,减少构建后包的体积。 - 每次引入
polyfill
都会定义别名,所以不会污染全局。
缺点就是
- 由于每次引入
polyfill
都会定义别名,所以会导致多个文件出现重复代码。
好了说了这么多,那到底该怎么选择
写类库的时候用runtime,系统项目还是用polyfill。写库使用 runtime 最安全,如果我们使用了 includes,但是我们的依赖库 B 也定义了这个函数,这时我们全局引入 polyfill 就会出问题:覆盖掉了依赖库 B 的 includes。如果用 runtime 就安全了,会默认创建一个沙盒,这种情况 Promise 尤其明显,很多库会依赖于 bluebird 或者其他的 Promise 实现,一般写库的时候不应该提供任何的 polyfill 方案,而是在使用手册中说明用到了哪些新特性,让使用者自己去 polyfill。
话说的已经很明白了,该用哪种形式是看项目类型了,不过通常对于一般业务项目来说,还是plugin-transform-runtime
处理工具函数,babel-polyfill
处理兼容。也就是说使用@babel/preset-env
配置usage
来按需引入polyfill
,并配置plugin-transform-runtime
来抽取公共方法减少代码整体体积。
项目开发最佳配置如下:
{
"presets": [
[
// 编译js并按需提供polyfill
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3"
}
]
],
// 提取辅助函数
"plugins": ["@babel/plugin-transform-runtime"]
}
在webpack中的应用
前面讲的是使用babel-cli
来编译js
,但在实际项目开发过程中都不会直接使用babel-cli
来编译js
,一般会结合webpack
等一些构建工具来使用。下面笔者来说说使用webpack
编译js
的流程。
创建项目
首先我们创建一个文件夹,然后初始化package.json
文件。
// 创建webpacktest文件夹
mkdir webpacktest
// 进入webpacktest文件夹
cd webpacktest
// 创建package.json
npm init
创建源文件
在根目录下创建src
目录,并创建index.js
文件。
const say = () => {
console.log("hello world");
};
say();
安装webpack 和 webpack-cli
我们本地安装webpack 和 webpack-cli
npm i webpack webpack-cli -D
安装babel相关包
使用webpack
构建的话我们就不需要再安装@babel/cli
了,我们另外单独安装babel-loader
就可以了。
npm i @babel/core @babel/preset-env @babel/plugin-transform-runtime babel-loader -D
配置webpack.config.js
然后我们在根目录下创建webpack.config.js
文件,并做如下配置
module.exports = {
mode: "development",
module: {
rules: [
// js和jsx 配置
{
test: /\.jsx?$/,
use: ["babel-loader"],
},
],
},
};
用babel-loader
来处理js
和jsx
文件。
配置babel.config.json
使用@babel/preset-env
来编译js
,并添加polyfill
。使用@babel/plugin-transform-runtime
来抽离公共的辅助编译方法,减少构建后包的体积。
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3"
}
]
],
"plugins": ["@babel/plugin-transform-runtime"]
}
按需配置 browserslistrc
.browserslistrc
默认值是 > 0.5%, last 2 versions, Firefox ESR, not dead。
babel
的编译如果没配置 targets
或 ignoreBrowserslistConfig
,@babel/preset-env
会使用 browserslist
配置源。也就是会用上面的默认配置。
如果需要兼容特定浏览器,只需要按需修改.browserslistrc
就可以了。
我们这里创建一个.browserslistrc
文件,并配置
> 0.5%
last 2 versions
Firefox ESR
编译
在package.josn
里面配置webpack
编译脚本。
"scripts": {
"webpack1": "webpack"
}
在命令行运行 npm run webpack
就可以看到编译后的js
文件了。箭头函数被转换成普通的函数了。
我们再来试试polyfill
在index.js
文件中使用includes
语法
// index.js
const arr = [1, 2, 3];
console.log(arr.includes(4));
再次编译,额,居然报错了。为啥?
因为我们的polyfill
是从corejs
引入的,这里使用到了corejs3
,所以还需要单独安装
npm install --save core-js@3 -D
安装好后,我们再次编译,成功啦!
前面说啦,除了在@babel/preset-env
里面配置polyfill
,还能在@babel/plugin-transform-runtime
中配置。
这里我们再使用这种方式试试,我们修改下babel.config.json
文件
{
"presets": [
[
"@babel/preset-env"
]
],
"plugins": [["@babel/plugin-transform-runtime", {"corejs": "3"}]]
}
我们来编译试试,发现也报错了,原因是这种配置方式的polyfill
是从runtime-corejs
引入的,所以我们也需要再单独安装。
npm i @babel/runtime-corejs3 -D
安装好后,我们再次编译,成功啦!
参考文档
系列文章
webpack入门之css处理(css预处理器和css后置处理器)
webpack入门之js处理(babel、babel polyfill)
webpack入门之ts处理(ts-loadr和babel-loader的选择)
webpack入门之开发环境(mode、dev-server、devtool)
webpack入门之提升开发效率的几个配置(ProvidePlugin、DefinePlugin、resolve、externals)
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!
转载自:https://juejin.cn/post/7126465727178997791