Liaoct / blog

在这里记录一些个人经验与思考
22 stars 2 forks source link

如何使用Babel 7优化打包代码? #27

Open ghost opened 5 years ago

ghost commented 5 years ago

我有幸经历过Babel 5、6时代,直到今天的Babel 7时代。如今的Babel,已经变得更加清晰、高效。

大多数人都知道Babel能干什么,也能通过一些配置来转化代码。但是,究竟怎么使用Babel,才能最大化的优化代码呢?今天,我们便以Babel 7来进行探讨下。

注意,本文将不会深入的介绍Babel的一些概念,比如babel-clibabel-runtimebabel-polyfillpresetplugin等,这些内容请读者自行去研究。但是,为了由浅入深,以及行文方便,本文也会捎带提点。

Babel 基础知识

Babel 本身并不会做任何转换

mkdir babel-test && cd babel-test
yarn init
yarn add @babel/cli @babel/core -D
const title = 'babel-learn';
npx babel index.js -o output.js

仍然原样输出:

// output.js
const title = 'babel-learn';

使用插件转换语法

yarn add @babel/plugin-transform-block-scoping -D
npx babel index.js -o output.js --plugins=@babel/plugin-transform-block-scoping

查看output.jsconst语法已经成功转换。

var title = 'babel-learn';
// index.js

const title = 'babel-learn';

const obj = {
    data: {
        auth: { name: 'liaoct' }
    },
    msg: 'ok'
};

const getValByPath = p => o =>
    p.split('.').reduce((xs, x) =>
        (xs && (xs[x] || xs[x] === 0)) ? xs[x] : null, o);

const name = getValByPath('data.auth.name')(obj);

添加@babel/plugin-transform-arrow-functions插件,并进行编译:

yarn add @babel/plugin-transform-arrow-funtions -D
npx babel index.js -o output.js --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions

查看编译后的文件:

var title = 'babel-learn';
var obj = {
  data: {
    auth: {
      name: 'liaoct'
    }
  },
  msg: 'ok'
};

var getValByPath = function (p) {
  return function (o) {
    return p.split('.').reduce(function (xs, x) {
      return xs && (xs[x] || xs[x] === 0) ? xs[x] : null;
    }, o);
  };
};

var name = getValByPath('data.auth.name')(obj);

使用.babelrc配置文件

现在每次编译时,均需要在命令行输入很长的plugin列表,这极其不方便,babel允许使用.babelrc配置文件来进行指定,以后编译时,直接从配置文件进行读取。

// .babelrc

{
    "plugins": [
        "@babel/plugin-transform-block-scoping",
        "@babel/plugin-transform-arrow-functions"
    ]
}

现在进行编译时,可以不用带--plugins选项了:

npx babel index.js -o output.js

编译结果,和之前一致。

使用Preset

一般我们会在源代码中使用多种语法,如果像上面那样安装一堆转译插件,将非常繁琐。Babel非常人性化的为我们提供了常见插件的集合,叫preset

preset分为以下几种:

  1. 官方常用插件集合。如envreactflowtypescript
  2. stage-x,这里包含当年最新规范的提案。包含stage-0stage-1stage-2stage-3
  3. es201x,latest。这些是已经纳入规范的语法,在babel 7中已经放弃维护,使用env代替。

在babel 5、babel 6时代,存在众多es201x的preset,但是在Babel 7,我们只需要使用env即可。如果不写任何配置参数,env等价于latest,也等价于es2015 + es2016 + es2017(不包含stage-x中的插件)。

其他preset这里不再展开介绍。

yarn remove @babel/plugin-transform-block-scoping @babel/plugin-transform-arrow-functions
yarn add @babel/preset-env -D
{
    "presets": [
        "@babel/preset-env"
    ]
}
npx babel index.js -o output.js

编译结果,同之前一致。

Babel 进阶

使用polyfill

index.js中新增如下内容:

Promise.resolve();

在浏览器中运行编译后的文件。在ie9、ie10,以及低版本浏览器,如Chrome 28等浏览器中,会报错:Promise未定义。

这是因为Babel只会做ES6+语法的转译,但是无法转换全局Api,如Promise、set、map等,如果要在未实现这些全局对象的浏览器上正常使用,则需要使用polyfill

除了build-ints(eg: Promise, set, map),还有class static function(eg: Array.from, Object.assign),prototype function(eg: [].includes,string.trim, array.reduce), regenerator(eg: async, generator)均需要polyfill

在Babel 7中使用polyfill有三种方式:

  1. 直接将preset-envuseBuiltIns设置为usage
  2. 安装babel-polyfill为项目依赖,并在入口文件引入,然后将preset-envuseBuiltIns设置为entry
  3. 直接在入口文件引入babel-polyfill。注意:不设置preset-env,将全量引入babel-polyfill

注意,之前在babel-plugin-transform-runtime中也可以配置polyfill选项,在Babel 7中已经移除该选项,将polyfill功能完全交给preset-env来控制。 另外,关于tranform-runtimepolyfillpreset-env的关系这里不细讲。

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

查看编译结果:

"use strict";

require("core-js/modules/es6.promise");

require("core-js/modules/es6.regexp.split");

require("core-js/modules/es6.array.reduce");

...

由上面的编译结果可知,preset-env自动为我们引入了promise。并且,它还自动为我们引入了splitreduce的垫片。

preset-env会根据browserlist来确定需要兼容的目标浏览器,然后自动选择需要的polyfill进行引入。关于preset-envtarget配置选项,这里不深入讲解。如果没有配置该选项,将使用默认的browserlist:

0.5%
last 2 versions
Firefox ESR
not dead

你可以在.browserlistrc配置文件中,指定自己的目标浏览器:

1%
last 2 versions
Chrome >= 28
Firefox >= 28
Safari >= 6
ie >= 8

另外,上面的编译结果中包含了require语法,这在浏览器中并不能直接运行。因为,自动Babel6之后,babel不再默认支持浏览器,需要配合webpack、browserify、rollup等模块打包工具,才能在浏览器使用模块代码。此处,仅介绍babel的打包及优化,文章后面会介绍结合webpack的用法。

安装babel-polyfill为项目生产依赖,注意不是开发依赖:

yarn add @babel/polyfill

index.js顶部入口处引入:

import '@babel/polyfill'

const title = 'babel-learn';
...

修改useBuiltInsentry:

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

查看编译结果:

"use strict";

require("core-js/modules/es6.array.copy-within");
require("core-js/modules/es6.array.every");
require("core-js/modules/es6.array.fill");
require("core-js/modules/es6.array.filter");
require("core-js/modules/es6.array.find");
require("core-js/modules/es6.array.find-index");
require("core-js/modules/es6.array.for-each");
require("core-js/modules/es6.array.from");
require("core-js/modules/es7.array.includes");
require("core-js/modules/es6.array.index-of");
require("core-js/modules/es6.array.is-array");
...

由编译结果可知,该选项会把目标浏览器所需要的所有polyfill都引入,不管该语法(或者API)有没有使用到,均会引入。

直接在入口文件引入polyfill,设置useBuiltIns设置为false

useBuiltIns = "usage" vs useBuiltIns = "entry" vs useBuiltIns = false

由上可知,useBuiltIns = "usage"打包体积最小,useBuiltIns = "entry"次之,useBuiltIns = false并且全量引入打包体积最大。

使用transform-runtime

修改index.js为如下代码:

async function request() {
    await new Promise((resolve) => {
        setTimeout(() => resolve(), 1000);
    })
}

request();

.babelrc的设置如下:

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

编译结果大致为:

"use strict";

require("regenerator-runtime/runtime");

require("core-js/modules/es6.promise");

function asyncGeneratorStep() { ... } // 很长的一个function定义

function _asyncToGenerator(fn) { return function () { ... }; } // 很长的一个function定义

function request() {
  return _request.apply(this, arguments);
}

function _request() {
  _request = _asyncToGenerator(
  /*#__PURE__*/
  regeneratorRuntime.mark(function _callee() {
    return regeneratorRuntime.wrap(function _callee$(_context) {
      ...
    }, _callee, this);
  }));
  return _request.apply(this, arguments);
}

从上面的编译结果可以看到async/await语法,被编译成了asyncGeneratorStep_asyncToGenerator函数。

这类由babel编译产生的函数,或者babel transform过程中会用到的辅助函数(eg:_extend),叫做helper函数

我们能推测到,如果有多个文件中都用到了async/await语法,那么是不是这两个函数会在每个文件中都定义一遍。我们可以验证一下。

新建一个util.js文件:

// util.js 
async function requestUtil() {
    await 0;
}

export { requestUtil }

然后在index.js文件引入:

require('./util');

...

编译发现require语句以及util.js文件都没有被编译,那是因为Babel本身不具备模块处理的能力。下面使用webpack 4让模块打包功能可用。

安装webpack:

yarn add webpack webpack-cli babel-loader -D

新增webpack.config.js:

const path = require('path');

module.exports = {
    entry: './index.js',
    output: {
        path: path.join(__dirname, '/'),
        filename: 'output.js',
        library: 'babel-learn',
        libraryTarget: 'umd'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'babel-loader'
            }
        ]
    }
};

现在即可以通过webpack来启动babel

npx webpack --watch --mode=development

为了方便观察结果,上面使用development模式,这样编译之后的代码不会被会压缩混淆,方便我们查看编译结果。

在输出文件ouput.js下面两个代码片段中搜索_asyncToGenerator,可以发现该函数被定义了两次。

/***/ "./index.js":
/*!******************!*\
  !*** ./index.js ***!
  \******************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);
... 
/***/ }),

/***/ "./util.js":
/*!*****************!*\
  !*** ./util.js ***!
  \*****************/
/*! exports provided: requestUtil */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);
...
/******/ });

由此可知,如果babel在编译的时候检测到模块需要helpers,然后在输出时会把这些helpers放到模块的顶部。

但是如果多个模块都需要这些helpers,就会导致每个模块都定义一份,代码冗余。transform-runtime可以帮助我们解决这个问题。

安装transform-runtime:

yarn add @babel/runtime
yarn add @babel/plugin-transform-runtime -D

我们把@babel/runtime@babel/plugin-transform-runtime统称为transform-runtime,因为他们一般都是一起使用。至于他们的组成部分,以及作用这里不详细解释。

.babelrc中添加transform-runtime

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

再次编译,查看编译结果,发现_asyncToGenerator已经只有一份定义,并且该函数的定义来自@babel/runtime/helpers/asyncToGenerator.js

/***/ "./node_modules/@babel/runtime/helpers/asyncToGenerator.js":
/*!*****************************************************************!*\
  !*** ./node_modules/@babel/runtime/helpers/asyncToGenerator.js ***!
  \*****************************************************************/
/*! no static exports found */
/***/ (function(module, exports) {

eval("function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
...
/***/ }),

这也是解释了为什么@babel/runtime要作为生产依赖,而不是开发依赖。因为它在运行时会被加载使用。

注意,在babel 6中,babel-plugin-transform-runtime可以设置polyfill选项,这与preset-env有些重复,因此babel 7已经移除transform-runtimepolyfill功能。

总结

Babel 7相较与Babel 5、6配置项变得更少,插件功能更加单一,相对来说使用来变得更加简单。通过上面的讨论,在Babel 7中,相对较优化的配置如下:

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

preset-env自动的去处理浏览器polyfill,不用再手动引入babel-polyfill,让transform-runtime去解决helpers函数问题,从而最优的减少打包体积。

bitQ2019 commented 3 years ago

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