likes
comments
collection
share

webpack入门之js处理(babel、babel polyfill)

作者站长头像
站长
· 阅读数 50

简介

我们都知道,babel是用来编译js的,就是把高版本的js编译成低版本的js,以便浏览器识别。但是对于babel更深入点可能就不是很清楚了,所以笔者今天再来简单总结下

看完本文你将学到:

  1. 知道babel的核心包
  2. 怎么配置和使用babel
  3. babel polyfill概念以及使用
  4. 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

webpack入门之js处理(babel、babel polyfill)

简单总结一下就是:为了让计算机理解代码需要先对源码字符串进行 parse,生成 AST,把对代码的修改转为对 AST 的增删改,转换完 AST 之后再打印成目标代码字符串。

核心库 @babel/core

@babel/corebabel最核心的一个编译库,他可以将我们的代码进行词法分析--语法分析--语义分析过程从而生成AST抽象语法树,从而对于“这棵树”的操作之后再通过编译成为新的代码。

CLI命令行工具 @babel/cli

@babel/clibabel 提供的命令行工具,它主要是提供 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进行编译。我们来看看编译后的效果。

webpack入门之js处理(babel、babel polyfill)

啊,啥都没变,编译前后的代码是完全一样的,这是咋回事?

因为 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,再次进行编译,我们来看看编译后的效果。

webpack入门之js处理(babel、babel polyfill)

箭头函数被转换成普通函数啦,达到我们预期的效果啦。

我们再来添加一个es6的新特性,解构赋值

webpack入门之js处理(babel、babel polyfill)

我们运行npm run index1,再次进行编译,我们来看看编译后的效果。

webpack入门之js处理(babel、babel polyfill)

可以发现,由于我们只安装了转换箭头函数的插件,所以它只转换了箭头函数,对于解构这个新特性并没有进行编译。

天啊,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,再次进行编译,我们来看看编译后的效果。

webpack入门之js处理(babel、babel polyfill)

可以看到,我们的解构语法也被转换好了。

需要说明的是,@babel/preset-env 会根据你配置的目标环境,生成插件列表来编译。对于基于浏览器或 Electron 的项目,官方推荐使用 .browserslistrc 文件来指定目标环境。默认情况下,如果你没有在 Babel 配置文件中(如babel.config.json)设置 targetsignoreBrowserslistConfig@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,再次进行编译,我们来看看编译后的效果。

webpack入门之js处理(babel、babel polyfill)

可以发现,源码和编译后的代码居然是一样的。为什么呢?因为最近的两个Chrome版本它是原生支持箭头函数和解构赋值的所以根本就不需要进行编译成低版本的js代码。

所以对于目标环境的配置是非常重要的。配置的好能大大减小我们代码的体积。

插件和预设的执行顺序

  1. 插件在预设前运行。

  2. 插件顺序从前往后排列。

  3. 预设顺序是从后往前(颠倒的)。

例如:

{
  "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方法

webpack入门之js处理(babel、babel polyfill)

这里我们只使用了@babel/preset-env预设来进行代码的编译

// babel.config.json

{
  "presets": ["@babel/preset-env"]
}

我们编译看看编译后的代码

webpack入门之js处理(babel、babel polyfill)

发现居然编译后的代码和源代码基本上一样,这在低版本浏览器显然是运行不了的,因为低版本浏览器肯定是不支持新特性includes方法。

所以就需要使用到@babel/polyfill

首先,安装 @babel/polyfill 依赖:

npm install --save @babel/polyfill

我们需要将完整的 polyfill 在代码之前加载,修改我们的 src/index2.js,在最开始引入@babel/polyfill

webpack入门之js处理(babel、babel polyfill)

然后我们再次编译

webpack入门之js处理(babel、babel polyfill)

可以看到,编译后的代码就是把@babel/polyfill全部引入了。这样固然是不会再报错了,不过,很多时候,我们未必需要完整的 @babel/polyfill,这会导致我们最终构建出的包的体积增大,@babel/polyfill的包大小为99K

webpack入门之js处理(babel、babel polyfill)

我们更期望的是,如果我使用了某个新特性,再引入对应的 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/stableregenerator-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

我们来看看编译后的效果

webpack入门之js处理(babel、babel polyfill)

只引入了我们需要的core-js/modules/es.array.includes.js,这样就达到了按需引入的目的。

我们再来看看另外一个例子,假设有一个class需要编译。

class People {}

我们使用 corejs3usage的方式进行编译。

// babel.config.json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": "3"
      }
    ]
  ]
}

我们来看看编译后的结果

webpack入门之js处理(babel、babel polyfill)

可以发现,它会给我们定义一个_classCallCheck辅助函数。如果我们编译的源码里面有很多class,那岂不是就会定义很多个这样的函数。这显然是不符合我们要求的。

usage按需引入优缺点分析

通过前面使用usage按需引入的两个例子,我们可以发现虽然preset-env能支持按需引入,但是会有两个问题。

第一,就是引入的代码是全局的,容易污染环境。

webpack入门之js处理(babel、babel polyfill)

第二,就是辅助函数在每个文件都会定义,比如上面的class编译,就会单独定义_classCallCheck辅助函数。

webpack入门之js处理(babel、babel polyfill)

要解决这两个问题也很简单,那就是使用@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包中引入的。

webpack入门之js处理(babel、babel polyfill)

这样,通过从@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);

我们编译来看看

webpack入门之js处理(babel、babel polyfill)

发现编译后的代码居然和源码一样,这在低版本浏览器显然是运行不了的,因为低版本浏览器肯定是不支持新特性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" }]]
}

我们再来编译前面classincludes的例子。可以看到,辅助函数classCallCheck是从@babel/runtime-corejs3/helpers中引入的,并且includes也是从@babel/runtime-corejs3/core-js-stable引入的。

webpack入门之js处理(babel、babel polyfill)

webpack入门之js处理(babel、babel polyfill)

而且我们还发现,使用@babel/plugin-transform-runtime还会自动重命名,比如上面的includes会被重命名为_includes。重命名后的好处就是 polyfill 不污染全局。

这也就解决了我们上面使用@babel/preset-env的两个问题了。

对比总结

我们知道,@babel/polyfill@babel/preset-env@babel/plugin-transform-runtime 都能用来引入polyfill的。那到底该怎么选择呢?

@babel/polyfill不用多说了,肯定不是首选,因为它全局引入,并且还会污染环境。

@babel/preset-envusage方案其实就是按需引入@babel/polyfill,所以它不会全局引入,但是它直接引入的polyfill会污染全局环境,并且每次还会创建多余的辅助函数,会增大构建后代码体积。

@babel/plugin-transform-runtime插件搭配@babel/runtime或者@babel/runtime-corejs3

优势就是

  1. 抽离重复注入的 helper 代码,减少构建后包的体积。
  2. 每次引入 polyfill 都会定义别名,所以不会污染全局。

缺点就是

  1. 由于每次引入 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来处理jsjsx文件。

配置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的编译如果没配置 targetsignoreBrowserslistConfig@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

安装好后,我们再次编译,成功啦!

参考文档

babel 官方文档

系列文章

webpack入门之css处理(css预处理器和css后置处理器)

webpack入门之图片、字体、文本、数据文件处理

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
评论
请登录