Open ghost opened 5 years ago
我有幸经历过Babel 5、6时代,直到今天的Babel 7时代。如今的Babel,已经变得更加清晰、高效。
大多数人都知道Babel能干什么,也能通过一些配置来转化代码。但是,究竟怎么使用Babel,才能最大化的优化代码呢?今天,我们便以Babel 7来进行探讨下。
注意,本文将不会深入的介绍Babel的一些概念,比如babel-cli、babel-runtime、babel-polyfill、preset、plugin等,这些内容请读者自行去研究。但是,为了由浅入深,以及行文方便,本文也会捎带提点。
babel-cli
babel-runtime
babel-polyfill
preset
plugin
mkdir babel-test && cd babel-test yarn init yarn add @babel/cli @babel/core -D
index.js
const title = 'babel-learn';
npx babel index.js -o output.js
output.js
仍然原样输出:
// output.js const title = 'babel-learn';
@babel/plugin-transform-block-scoping
const
let
yarn add @babel/plugin-transform-block-scoping -D
npx babel index.js -o output.js --plugins=@babel/plugin-transform-block-scoping
查看output.js,const语法已经成功转换。
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插件,并进行编译:
@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选项了:
--plugins
编译结果,和之前一致。
一般我们会在源代码中使用多种语法,如果像上面那样安装一堆转译插件,将非常繁琐。Babel非常人性化的为我们提供了常见插件的集合,叫preset。
preset分为以下几种:
env
react
flow
typescript
stage-0
stage-1
stage-2
stage-3
在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
@babel/preset-env
yarn add @babel/preset-env -D
preset-env
{ "presets": [ "@babel/preset-env" ] }
编译结果,同之前一致。
polyfill
在index.js中新增如下内容:
Promise.resolve();
在浏览器中运行编译后的文件。在ie9、ie10,以及低版本浏览器,如Chrome 28等浏览器中,会报错:Promise未定义。
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有三种方式:
useBuiltIns
usage
entry
注意,之前在babel-plugin-transform-runtime中也可以配置polyfill选项,在Babel 7中已经移除该选项,将polyfill功能完全交给preset-env来控制。 另外,关于tranform-runtime、polyfill、preset-env的关系这里不细讲。
babel-plugin-transform-runtime
tranform-runtime
{ "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。并且,它还自动为我们引入了split和reduce的垫片。
promise
split
reduce
preset-env会根据browserlist来确定需要兼容的目标浏览器,然后自动选择需要的polyfill进行引入。关于preset-env的target配置选项,这里不深入讲解。如果没有配置该选项,将使用默认的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的用法。
preset-env会根据browserlist来确定需要兼容的目标浏览器,然后自动选择需要的polyfill进行引入。关于preset-env的target配置选项,这里不深入讲解。如果没有配置该选项,将使用默认的browserlist:
browserlist
target
0.5% last 2 versions Firefox ESR not dead
你可以在.browserlistrc配置文件中,指定自己的目标浏览器:
.browserlistrc
1% last 2 versions Chrome >= 28 Firefox >= 28 Safari >= 6 ie >= 8
另外,上面的编译结果中包含了require语法,这在浏览器中并不能直接运行。因为,自动Babel6之后,babel不再默认支持浏览器,需要配合webpack、browserify、rollup等模块打包工具,才能在浏览器使用模块代码。此处,仅介绍babel的打包及优化,文章后面会介绍结合webpack的用法。
require
安装babel-polyfill为项目生产依赖,注意不是开发依赖:
yarn add @babel/polyfill
在index.js顶部入口处引入:
import '@babel/polyfill' const title = 'babel-learn'; ...
修改useBuiltIns为entry:
{ "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。
false
useBuiltIns = "usage"
useBuiltIns = "entry"
useBuiltIns = false
由上可知,useBuiltIns = "usage"打包体积最小,useBuiltIns = "entry"次之,useBuiltIns = false并且全量引入打包体积最大。
transform-runtime
helper
修改index.js为如下代码:
async function request() { await new Promise((resolve) => { setTimeout(() => resolve(), 1000); }) } request();
.babelrc的设置如下:
编译结果大致为:
"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函数。
async/await
asyncGeneratorStep
_asyncToGenerator
这类由babel编译产生的函数,或者babel transform过程中会用到的辅助函数(eg:_extend),叫做helper函数。
helper函数
我们能推测到,如果有多个文件中都用到了async/await语法,那么是不是这两个函数会在每个文件中都定义一遍。我们可以验证一下。
新建一个util.js文件:
util.js
// util.js async function requestUtil() { await 0; } export { requestUtil }
然后在index.js文件引入:
require('./util'); ...
编译发现require语句以及util.js文件都没有被编译,那是因为Babel本身不具备模块处理的能力。下面使用webpack 4让模块打包功能可用。
webpack 4
webpack
安装webpack:
yarn add webpack webpack-cli babel-loader -D
新增webpack.config.js:
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:
babel
npx webpack --watch --mode=development
为了方便观察结果,上面使用development模式,这样编译之后的代码不会被会压缩混淆,方便我们查看编译结果。
development
在输出文件ouput.js下面两个代码片段中搜索_asyncToGenerator,可以发现该函数被定义了两次。
ouput.js
/***/ "./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,因为他们一般都是一起使用。至于他们的组成部分,以及作用这里不详细解释。
@babel/runtime
@babel/plugin-transform-runtime
在.babelrc中添加transform-runtime:
{ "presets": [ ["@babel/preset-env", { "useBuiltIns": "usage" }] ], "plugins": ["@babel/plugin-transform-runtime"] }
再次编译,查看编译结果,发现_asyncToGenerator已经只有一份定义,并且该函数的定义来自@babel/runtime/helpers/asyncToGenerator.js。
@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-runtime的polyfill功能。
Babel 7相较与Babel 5、6配置项变得更少,插件功能更加单一,相对来说使用来变得更加简单。通过上面的讨论,在Babel 7中,相对较优化的配置如下:
让preset-env自动的去处理浏览器polyfill,不用再手动引入babel-polyfill,让transform-runtime去解决helpers函数问题,从而最优的减少打包体积。
helpers
{ "presets": [ ["@babel/preset-env", { "useBuiltIns": "usage", "corejs": { "version": "3.8", "proposals": true } }] ], "plugins": ["@babel/plugin-transform-runtime"] }
我有幸经历过Babel 5、6时代,直到今天的Babel 7时代。如今的Babel,已经变得更加清晰、高效。
大多数人都知道Babel能干什么,也能通过一些配置来转化代码。但是,究竟怎么使用Babel,才能最大化的优化代码呢?今天,我们便以Babel 7来进行探讨下。
Babel 基础知识
Babel 本身并不会做任何转换
index.js
文件output.js
仍然原样输出:
使用插件转换语法
@babel/plugin-transform-block-scoping
插件,用于转化const
、let
语法查看
output.js
,const
语法已经成功转换。index.js
,并添加新的转译插件进行转换添加
@babel/plugin-transform-arrow-functions
插件,并进行编译:查看编译后的文件:
使用
.babelrc
配置文件现在每次编译时,均需要在命令行输入很长的
plugin
列表,这极其不方便,babel允许使用.babelrc
配置文件来进行指定,以后编译时,直接从配置文件进行读取。现在进行编译时,可以不用带
--plugins
选项了:编译结果,和之前一致。
使用Preset
一般我们会在源代码中使用多种语法,如果像上面那样安装一堆转译插件,将非常繁琐。Babel非常人性化的为我们提供了常见插件的集合,叫
preset
。preset分为以下几种:
env
、react
、flow
、typescript
。stage-0
、stage-1
、stage-2
、stage-3
。env
代替。在babel 5、babel 6时代,存在众多es201x的
preset
,但是在Babel 7,我们只需要使用env
即可。如果不写任何配置参数,env等价于latest,也等价于es2015 + es2016 + es2017(不包含stage-x中的插件)。@babel/preset-env
.babelrc
中使用preset-env
编译结果,同之前一致。
Babel 进阶
使用
polyfill
在
index.js
中新增如下内容:在浏览器中运行编译后的文件。在ie9、ie10,以及低版本浏览器,如Chrome 28等浏览器中,会报错:
Promise
未定义。这是因为Babel只会做ES6+语法的转译,但是无法转换全局Api,如Promise、set、map等,如果要在未实现这些全局对象的浏览器上正常使用,则需要使用
polyfill
。在Babel 7中使用
polyfill
有三种方式:preset-env
的useBuiltIns
设置为usage
babel-polyfill
为项目依赖,并在入口文件引入,然后将preset-env
的useBuiltIns
设置为entry
babel-polyfill
。注意:不设置preset-env
,将全量引入babel-polyfill
useBuiltIns
为usage
查看编译结果:
由上面的编译结果可知,
preset-env
自动为我们引入了promise
。并且,它还自动为我们引入了split
和reduce
的垫片。babel-polyfill
,并设置useBuiltIns
设置为entry
安装
babel-polyfill
为项目生产依赖,注意不是开发依赖:在
index.js
顶部入口处引入:修改
useBuiltIns
为entry
:查看编译结果:
由编译结果可知,该选项会把目标浏览器所需要的所有
polyfill
都引入,不管该语法(或者API)有没有使用到,均会引入。polyfill
直接在入口文件引入
polyfill
,设置useBuiltIns
设置为false
。useBuiltIns = "usage"
vsuseBuiltIns = "entry"
vsuseBuiltIns = false
useBuiltIns = "usage"
:按需加载polyfill
,当目标浏览器需要对应的polyfill
才会被加载,代码中没有使用到的就不会被加载。useBuiltIns = "entry"
:目标浏览器所需要的所有polyfill
都会被加载,代码中没有被使用到的也会被加载。useBuiltIns = false
并且全量引入polyfill
:所有的polyfill
都会被加载,不管目标浏览器是否需要。由上可知,
useBuiltIns = "usage"
打包体积最小,useBuiltIns = "entry"
次之,useBuiltIns = false
并且全量引入打包体积最大。使用
transform-runtime
helper
函数修改
index.js
为如下代码:.babelrc
的设置如下:编译结果大致为:
从上面的编译结果可以看到
async/await
语法,被编译成了asyncGeneratorStep
与_asyncToGenerator
函数。我们能推测到,如果有多个文件中都用到了
async/await
语法,那么是不是这两个函数会在每个文件中都定义一遍。我们可以验证一下。新建一个
util.js
文件:然后在
index.js
文件引入:编译发现
require
语句以及util.js
文件都没有被编译,那是因为Babel本身不具备模块处理的能力。下面使用webpack 4
让模块打包功能可用。webpack
加载模块安装
webpack
:新增
webpack.config.js
:现在即可以通过
webpack
来启动babel
:为了方便观察结果,上面使用
development
模式,这样编译之后的代码不会被会压缩混淆,方便我们查看编译结果。在输出文件
ouput.js
下面两个代码片段中搜索_asyncToGenerator
,可以发现该函数被定义了两次。和
由此可知,如果babel在编译的时候检测到模块需要helpers,然后在输出时会把这些helpers放到模块的顶部。
但是如果多个模块都需要这些helpers,就会导致每个模块都定义一份,代码冗余。
transform-runtime
可以帮助我们解决这个问题。transform-runtime
安装
transform-runtime
:在
.babelrc
中添加transform-runtime
:再次编译,查看编译结果,发现
_asyncToGenerator
已经只有一份定义,并且该函数的定义来自@babel/runtime/helpers/asyncToGenerator.js
。这也是解释了为什么
@babel/runtime
要作为生产依赖,而不是开发依赖。因为它在运行时会被加载使用。总结
Babel 7相较与Babel 5、6配置项变得更少,插件功能更加单一,相对来说使用来变得更加简单。通过上面的讨论,在Babel 7中,相对较优化的配置如下:
让
preset-env
自动的去处理浏览器polyfill
,不用再手动引入babel-polyfill
,让transform-runtime
去解决helpers
函数问题,从而最优的减少打包体积。