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

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

简介

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

看完本文你将学到:

  1. 知道babel的核心包
  2. 怎么配置和使用babel
  3. babel polyfill概念以及使用
  4. babel结合webpack的使用

Babel 是什么?

Babel 是一个 JavaScript 编译器。

Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

下面列出的是 Babel 能为你做的事情:

  • 语法转换
  • 通过 Polyfill 方式在目标环境中添加缺失的特性

babel 的编译流程

babel 是 source to source 的转换,整体编译流程分为三步:

  • parse:通过 parser 把源码转成抽象语法树(AST)
  • transform:遍历 AST,调用各种 transform 插件对 AST 进行增删改
  • generate:把转换后的 AST 打印成目标代码,并生成 sourcemap

image.png

简单总结一下就是:为了让计算机理解代码需要先对源码字符串进行 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进行编译。我们来看看编译后的效果。

image.png

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

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

image.png

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

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

image.png

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

image.png

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

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

image.png

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

需要说明的是,@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,再次进行编译,我们来看看编译后的效果。

image.png

可以发现,源码和编译后的代码居然是一样的。为什么呢?因为最近的两个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方法

image.png

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

// babel.config.json

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

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

image.png

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

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

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

npm install --save @babel/polyfill

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

image.png

然后我们再次编译

image.png

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

image.png

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

我们来看看编译后的效果

image.png

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

配置@babel/runtime和@babel/plugin-transform-runtime实现按需引入

除了前面说的配置@babel/preset-env来实现按需引入,我们还可以使用@babel/runtime@babel/plugin-transform-runtime来实现按需引入。

@babel/runtime

简单来讲,@babel/runtime更像是一种按需加载的解决方案,但是babel-runtime会将引入方式由智能完全交由我们自己,我们需要什么自己引入什么。比如哪里需要使用到Promise,就需要手动在文件顶部添加import promise from 'babel-runtime/core-js/promise'

它的用法很简单,只要我们去安装npm install --save @babel/runtime后,在需要使用对应的polyfill的地方去单独引入就可以了。比如:

// 如果需要使用Promise 我们需要手动引入对应的运行时polyfill
import Promise from 'babel-runtime/core-js/promise'

const promsies = new Promise()

到这里能看出来@babel/runtime的问题了吧,虽然能实现按需引入,但是全部得手动处理,这谁顶得住。

@babel/plugin-transform-runtime

所以就有了@babel/plugin-transform-runtime插件,这个插件能帮助我们自动按需引入,而不再手动了,是不是很爽,我们来使用下。

首先安装

npm i @babel/plugin-transform-runtime -D

因为@babel/plugin-transform-runtime会使用到@babel/runtime所以请确保系统中也安装了@babel/runtime

然后配置,在这里我们没有配置@babel/preset-envuseBuiltIns参数而是配置的@babel/plugin-transform-runtime插件

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

我们再来变一下,看下效果

image.png

可以发现,它自动引入了我们需要的东西,并且重命名了。为什么重命名呢?好什么好处呢?

重命名后的好处就是 polyfill 不污染全局。plugin-transform-runtime提供的runtime形式的polyfill都是这种形式。

并且,@babel/plugin-transform-runtime还有个功能,就是能实现代码的重用。

比如我们有这样一个源码

class People {}

直接使用@babel/preset-env编译后的效果如下

image.png

使用添加@babel/plugin-transform-runtime插件并配置后的编译效果如下

image.png

可以发现,使用@babel/preset-env它会给我们的代码中定义一个 _classCallCheck()工具函数,这些工具函数的代码会包含在编译后的每个文件中。如果我们项目中存在多个文件使用了class,那么无疑在每个文件中注入这样一段冗余重复的工具函数将是一种灾难。

但是使用@babel/plugin-transform-runtime插件,他是辅助函数是从runtime包中引入的,所以能减小构建后包的体积。

所以总结@babel/plugin-transform-runtime插件优势就是

  1. 抽离重复注入的 helper 代码,减少产物体积
  2. polyfill 不污染全局

然后我就想,既然这种形式不会污染变量,那当然能用就用这种了,答案是否定的,具体使用还是得根据实际情况来。

runtime 不污染全局变量,但是会导致多个文件出现重复代码。
写类库的时候用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来抽取公共方法减少代码整体体积。

在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文件了。箭头函数被转换成普通的函数了。

这里我们并没有直接使用babel命令来编译我们的js,而是使用了webpack,并配置了babel-loader

参考文档

babel 官方文档

后记

感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!

猜你喜欢

转载自juejin.im/post/7126465727178997791