字数:4627,浏览工夫:12 分钟,点击浏览原文
枫林新叶催陈叶,雏凤清于老风声。一一李商隐
【前端工程化】系列文章链接:
- 01 扬帆起航 - 开发环境
- 02 白璧微瑕 - 包管理器
- 03 席卷八荒 -Webpack 根底篇
- 04 席卷八荒 -Webpack 进阶篇
示例代码仓库:https://github.com/BWrong/dev-tools
申明:如依照文中代码执行报错,请查看依赖版本是否和示例代码仓库中统一。
概述
作为一个有谋求的前端,如果你没有听过 Babel
的小名,那就真的是 out 了。不过说实话不太理解 Babel 的话,其实对日常开发的影响也不大,因为很多脚手架曾经配置好了,不必本人折腾。然而如果你想玩转前端工程化这块,这就是绕不过的坎。
babel 官网定义为一款 JavaSscript
编译器。
Use next generation JavaScript, today.
当初就应用下一代 JavaScript 语法吧
这里的“下一代”是泛指新的、临时还不被浏览器广泛支持的,而不是局限于某个确定的版本。
简略来说,浏览器(特地是一些低端浏览器,说你呢 IE)对 ECMAScript 标准的反对是滞后的,新的语法尽管可能给咱们带来更好的编程体验,却奈何往往不能不能间接在用户的浏览器运行,所以就须要在浏览器运行这些代码前将其转换成反对的语法,而 Babel 就是负责这个的,当然它的能力远不止如此,前面咱们一一道来。
Babel 次要做的就是这几点:
- 语法转换:将新的语法转换成兼容性更好的旧语法。
- 个性垫片:通过
Polyfill
实现目标环境中短少的个性,将环境的个性差别垫平,说白了还是做兼容。 - more…
// Babel Input: ES2015 arrow function
[1, 2, 3].map((n) => n + 1);
// Babel Output: ES5 equivalent
[1, 2, 3].map(function(n) {return n + 1;});
运行形式
Babel 的运行过程分为三个阶段:解析 — 转换 — 生成
- 解析:解析代码,并生成 AST 形象语法树
- 转换:将上一步的 AST 转换成指标环境反对的 AST
- 生成:依据转换后的 AST 生成指标代码
不难发现,第二步就是 Babel 执行的外围操作。但 Babel 本身是不具备任何转换能力,必须借助一些插件来实现语法的转换。对于插件,后续会具体介绍,这里只需记住,插件赋予了 babel 弱小的能力,没有插件它算个球。
如果想理解具体的转换过程,请浏览从 babel 讲到 AST。
babel 解析语法的内核应用的是
babylon
,当初已更名为@babel/parser
。
Babel7
作者在写这篇文章的时候,babel 的版本是 7.10.0
,babel7
带来了很多新的个性,为了防止给大家造成误导,这里先列出 babel7 的变动,防止和之前版本的配置混同。
次要的变动如下:
- preset:举荐应用
env
代替es201x
;删除stage-x
,因为这些草案中的个性不稳固,保护这些 preset 会节约大量精力,所以官网放弃了。如果须要应用能够本人显式的增加对应的插件。 -
npm 包名:将所有
babel-*
更改为@babel/*
,更加合乎 npm 命名标准。babel-cli
->@babel/cli
。babel-preset-env
->@babel/preset-env
,简写为@babel/env
- 对低于 nodejs6.0 不再提供反对。
@babel/cli
中不再蕴含@babel/node
,如要应用须要独自装置。- 提供
babel-upgrade
帮忙开发者从 6 到 7 的版本升级。
应用和配置
应用形式
babel 的应用场景次要分为两种:命令行和构建工具。
- 命令行
babel 为命令行提供了 @babel/cli
工具,反对在命令行间接执行 babel 命令。
首先,装置必要的包:
npm install --save-dev @babel/core @babel/cli
这里必须要装置
@babel/core
,它是 babel 的 外围,它是后续所有的基石,无论哪种形式都必不可少。
装置好在我的项目中执行 babel
命令就能够进行转换了。
babel src --out-dir lib # 将 src 下的文件解决后输入到 lib
- 构建零碎
在构建工具中应用才是咱们应用 babel 更罕用的形式。这里咱们以 webpack 为例。
babel 提供了一个 loader:
babel-loader
,应用这个 loader 咱们就能很轻松实现代码转换。
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
options: {// 配置项}
}
]
}
- 运行时(不举荐)
babel 还提供了运行时环境下的解决,能够在代码运行时,实时进行转换。不过并不举荐这种形式,因为它是在代码运行时实时进行转换,效率较低。
除了以上形式,还有一些其余的应用形式,详情能够查看 babel 配置。
配置文件
后面的形式咱们都没有提供配置,所以输入和输出是一样的,这齐全没有意义。所以咱们个别会提供一些配置,来通知 babel 如何进行转换。
babel7 反对多种形式来进行配置:
babel.config.json
(我的项目范畴).babelrc.json
(绝对文件)package.json
中增加 babel 字段
{
"name": "my-package",
"version": "1.0.0",
"babel": {"presets": [ ...],
"plugins": [...],
}
}
后面两种配置文件都应用了 json 格局,其实 babel 还反对其余格局,如 .js
,.cjs
(Commonjs)和.mjs
(ESModule),它们绝对于 json 格局更加灵便,能够在配置中进行编程解决,然而会使配置无奈动态剖析,失去可缓存性。
// babel.config.js
module.exports = {presets: [],
plugins: []};
所有形式的配置内容都是相似的,咱们能够依据本人的应用场景进行灵便抉择。
babel 尽管有多种形式来写配置文件,然而配置的内容都是相似的,次要蕴含 plugins
和presets
两局部,不过这里须要留神几点:
- 配置格局:
plugins
和presets
配置项均为数组,如果每项不须要配置的话,间接放入数组(值为项的字符串名称);如果要配置的话,则须要把格局改成数组,该数组的第一个元素是该项的字符串名字,第二个元素是该项的配置对象。这里填写的 plugins 和 presets,babel 会主动查看是否已被装置在
node_modules
上面,当然,也能够应用相对路径来应用本地文件。
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
],
"@babel/plugin-transform-runtime"
]
- 执行程序:
plugins
会从前到后执行,presets
会从后向前执行,且plugins
会比presets
先执行presets
逆序执行的目标是为了保障向后兼容,咱们在配置的时候依照标准的工夫程序列出即可,如['es2015', 'stage-1']
,这样就会先应用stage-1
,否则就会呈现谬误。 - 短名称:在配置 plugin 和 preset 名字的时候,能够省略名字中的
babel-plugin-
和preset-
字样,仅应用插件的名字就能够了。
{
"plugins": [
"myPlugin", // 残缺名字:"babel-plugin-myPlugin"
"@org/myPlugin" // 残缺名字:"@org/babel-plugin-myPlugin"
],
"presets":[
"myPreset", // 残缺名字:"babel-preset-myPreset"
"@babel/myPreset" // 残缺名字:@babel/preset-myPreset
]
}
对于一些罕用 plugins
和presets
的罕用配置,咱们在上面会有具体介绍。
外围
babel 的外围除了 @babel/core
就是 plugins
和presets
了,咱们个别应用 babel 其实就是在折腾这两块内容。上面咱们就来一探到底。
Plugin
后面咱们说了,babel 的转换齐全依附插件,babel 领有的能力齐全取决于给它配置了哪些插件。
babel 的插件分为两种:
- 语法插件:赋予 babel 解析特定语法的能力,能够了解一个为 babel 服务的翻译官,仅做翻译解析工作,工作在
AST
转换阶段。 - 转译插件:转译插件即把特定的语法转换成指定规范的语法。
比方将箭头函数
(x) => x
转换成function (x) {return x}
,只须要配置箭头函数转译插件即可。
{
"plugins": ["@babel/plugin-transform-arrow-functions"]
}
留神:应用了转译插件,就不再须要配置对应的语法插件了,因为转译插件会主动启用相应的语法插件。其实很容易了解,连解析都做不到,又何谈转换呢。
插件的应用大略分为两步:
- 将插件增加到配置文件的
plugins
属性中,可依据插件文档做相应配置。 - 装置插件到我的项目中,应用 npm 或者 yarn 均可
每个插件的性能是繁多的,所以个别转换需要要应用的插件数量比拟多,这时候能够思考 presets。罕用的插件和用法能够查看 babel-plugins
自定义插件
除了应用社区提供的插件,咱们也能够本人开发。
module.exports = function() {
return {
visitor: {Identifier(path) {
const name = path.node.name;
// reverse the name: JavaScript -> tpircSavaJ
path.node.name = name
.split("")
.reverse()
.join("");
},
},
};
}
这里只是一个简略的例子,理论的插件开发远不是如此简略,能够执行查看 Babel 插件手册学习。
Presets
在理论的开发中,咱们如果用 ES6 来进行开发的话,那须要转换的语法是十分多的,难道要一个一个去配置吗?还要不要配陪妹子了?
这个时候,就该 presets 退场了。presets 能够了解为一组插件的汇合,就像你去德克士间接点一个全家桶,而不用一个一个给服务员说要鸡腿、鸡翅、薯条 …(如果想和服务员小姐姐多待一会,也不是不能够)
presets 的起源次要有以下几种路径:
-
官网:官网为罕用的环境提供了 preset。
- @babel/preset-env
- @babel/preset-flow
- @babel/preset-react
- @babel/preset-typescript
- stage-X (试验性 Presets)
- 社区:在 npm 下面能够找到许多社区保护的 preset,比方 vue 和 react 都有保护了本人的 preset。
- 自定义:在有非凡需要的时候,能够本人创立 preset。
module.exports = () => ({
presets: [ // 援用其余 preset
require("@babel/preset-env"),
],
plugins: [ // 要汇合的插件
[require("@babel/plugin-proposal-class-properties"), {loose: true}],
require("@babel/plugin-proposal-object-rest-spread"),
],
});
留神:Stage-x) presets 中的任何转换都是针对未公布的 Javascript 个性(如 ES6 / ES2015)的更改,都是不稳固的,未来这些提案可能会发生变化,请审慎应用,特地是第三阶段前的提案。从 v7 开始,所有针对规范提案阶段的性能所编写的预设 (stage preset) 都已被弃用,官网曾经移除了 @babel/preset-stage-x
。
@babel/preset-env
{"presets": ["@babel/preset-env"]
}
这个是咱们最罕用的,也是官网当初举荐的 preset,它是一个智能预设,咱们无需再关怀特定环境下所需的转换插件(只须要指定指标环境),就能够应用最新的语法。除了可能缩小应用难度,还可能使转换进去的代码体积更小。
preset-env 会依据配置的指标环境中短少的性能,抉择对应的插件进行必要的转换和polyfill
,而指标环境反对的个性就不会转换,而不是无脑的一把梭哈。
如果不进行配置,它将应用默认配置,即 latest(env 保护的插件列表),会将最新的 JS 个性(ES2015,ES2016…,不蕴含 stage 阶段),将其转换成 ES5 代码。所以如果应用了提案阶段的语法,则须要自行额定配置。
如何指定指标环境
- 配置文件:通过配置文件的 targets 属性指定指标环境
{
"presets": [
["env", {
"targets": {"browsers": ["last 2 versions", "safari >= 7"],
"node": "12.10"
}
}]
]
}
- browserslistrc:如果是浏览器或者
Electron
,官网举荐应用.browserslistrc
文件来指定指标环境(能够和 autoprefixer、stylelint 等其余工具共享配置)。不过如果在配置文件中设置了
targets
或ignoreBrowserslistConfig
,.browserslistrc
中配置的内容将不会失效。
# .browserslistrc
last 2 Chrome versions
具体用法请查看browserslist
的更多配置。
其余配置
除了 targets
外,还有一些配置项也比拟罕用:
- modules: 用于指定转换后的模块标准,可设置为
"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false
,默认为"auto"
。 useBuiltIns
:此选项配置如何@babel/preset-env
解决 polyfill,在前面介绍 polyfill 咱们会具体介绍。corejs
:用于指定 corejs 的版本,仅当应用了useBuiltIns: usage
或者useBuiltIns: entry
此配置才无效,应用时肯定要配置正确的版本,否则会报错。以后默认的版本是
2.0
,不过倡议应用3.0
版本,因为 2.0 曾经不会再增加新的个性了,如果应用了一些较新的语法,将会无奈转换。
除了上述配置,还有一些不罕用的配置,能够查看官网文档。
Polyfill
后面咱们的配置其实是不完满的,转换进去的代码间接在浏览器运行是可能会出问题的。这是因为 babel 默认只转换了语法,而对于一些新的 API 是并没有解决的,如 Generator、Set、Maps、Proxy、Promise 等全局对象及其办法都不会转码,如果咱们在代码中应用到了就会报错。
此时,咱们就须要做 Polyfill,也就是所谓的 ’ 垫片 ’,作用就是把浏览器的差别垫平,人话就是通过模仿为这些浏览器补全缺失的全局对象及 API。
@babel/polyfill(废除)
晚期的计划就是 @babel/polyfill
(晚期的名字是babel-polyfill
,外部集成了 core-js
和 regenerator
),在入口或者 webpack 配置的 entry 中引入@babel/polyfill
,此工具会模仿残缺的 ES2015 + 环境(不蕴含第 4 阶段的提议),会创立一些全局对象,或者在已有全局对象的原型上补全办法,让咱们能够应用Promise
、WeakMap
、Array.from
等新的 API。
比方
Array.prototype.includes
是 ES6 的 API,就会在Array.prototype
下面增加一个本人实现的 includes 办法来模仿原生的 includes。
import '@babel/polyfill';
let include = [1,2,3].includes(1);
new Promise((resolve, reject) => {resolve();
});
当初,咱们的代码就能够在低版本的浏览器运行了。不过此计划有一些毛病:
- 文件体积大:因为
@babel/polyfill
会把所有 API 都补全,不论你有没有应用,比方你可能在我的项目中只应用了一个新的 API,后果他给你梭哈了,把所有的 API 都整进去了。所以最初打进去的包会十分大,十分节约。 - 净化全局变量:
@babel/polyfill
会在全局创建对象或者批改全局对象的原型链,所以会对全局变量进行净化。
对于第一个毛病,咱们能够应用后面 @babel/preset-env
提供的 useBuiltIns
配置,当设置为 usage
时(须要制订 corejs 版本),babel 会查看咱们的代码,只引入咱们代码中须要的polyfill
,这样打包进去的文件就会小很多。
// babel.config.json
{
"presets": [
[
"@babel/env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
例如后面的代码:
// import '@babel/polyfill'; 应用 useBuiltIns: 'usage' 时,会主动导入须要的垫片,所以不必再手动引入
let include = [1,2,3].includes(1);
new Promise((resolve, reject) => {resolve();
});
编译后的代码:
"use strict";
require("core-js/modules/es.array.includes");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
// import '@babel/polyfill';
let include = [1,2,3].includes(1);
new Promise((resolve, reject) => {resolve();
});
咱们能够看到,在文件的头部仅退出了文件中用到的新语法的polyfill
,是不是棒棒的了。
不过这并没有完,试想如果很多文件都应用了这些个性,岂不是每个文件都会引入一次,有没有方法改善一下呢?
@babel/plugin-transform-runtime
@babel/plugin-transform-runtime
是一个可重复使用 Babel 注入的帮忙程序的插件,防止反复注入,以节俭代码大小。通常还要搭配 @babel/runtime
一起应用。
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime # 提供缺失的个性,须要在生产环境中装置
// babel.config.json
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
],
"plugins": [["@babel/plugin-transform-runtime"]]
}
如上配置后,一个 class 转换如下:
// 源码
class Foo {method() {}}
// 转换后
import _classCallCheck from "babel-runtime/helpers/classCallCheck";
import _createClass from "babel-runtime/helpers/createClass";
let Foo = function () {function Foo() {_classCallCheck(this, Foo);
}
_createClass(Foo, [{
key: "method",
value: function method() {}
}]);
return Foo;
}();
咱们看到,这里并不是采纳全局引入的形式,而是引入了局部变量来替换新的语法,提供了一个沙箱机制,这就顺便解决了下面全局作用域净化的问题。而且这里所有模仿的个性都是从 @babel/runtime
中引入,而这个包咱们是装置在生产环境中的,并不是会打包到每个文件中,这就使这些 polyfill
失去了复用,不会打包屡次。
留神:下面的 corejs 须要为 3.0 才会齐全转换,如果应用 2.0 只会转换语法,而不会做 polyfill
,如果要做的话须要再独自引入@babel/runtime-corejs3
来进行解决,举荐应用 3.0,所以这里不再赘述。
经验了一波三折,咱们终于搞定了。然而,这不是银弹,将来可能还会呈现其余的计划,对于 Polyfill 的历史能够去看云谦大佬的 Polyfill 计划的过来、当初和将来。
生态
除了下面介绍的货色,babel 生态也是十分凋敝,还有很多其余配套的一些工具(名字通常以 babel-*
或@babel/*
结尾),上面咱们大略列举一下:
babel-cli
:babel 提供的命令行工具,提供了babel
命令来执行编译工作。babel-register
:会在require
加载文件前,先用 babel 进行转码编译,后面说到的运行时编译就是应用此工具。babel-loader
:babel 为 webpack 提供的一个 loader,也是最常见的应用形式。能够在 webpack 构建的工程中进行转码编译。babel-polyfill
/@babel/polyfill
:babel 提供的 polyfill 工具。
babel 的存在,让咱们能够突破运行环境语法标准的限度,犹如一台时光机,把咱们带到了将来,让咱们能够用彼时的语法来编写此时的利用,为咱们学习和应用最新的语法扫清了阻碍,俨然 babel 已成为咱们工程化中必不可少的一个工具。
参考文档:Babel 中文网、不容错过的 Babel7 常识、一口 (很长的) 气理解 babel