github: babel 归纳总结
在前端的发展过程中,javascript 的兼容性,一直是前端头痛的问题,在以前的一些有些项目中,为解决浏览器兼容而花费的时间甚至还要多余实际的业务逻辑开发时间,babel 就是其中处理兼容的转译工具(或者叫平台)。
babel 是什么
javascript 在不断发展,新的提案标准每年都会有,在得到广泛普及之前,Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本
babel 的编译过程分为 3 步,解析(parse),转换(transform),生成(generate),对应的三个插件分别是 Babylon、babel-traverse、babel-generator。babylon 将源码转换为抽象语法树(AST);babel-traverse 通过 AST 生成一个便于操作、转换的 path 对象,供我们的 babel 插件处理;babel-generator 读取 AST 并将其转换为代码和源码映射。这些过程不是本文的关注点,我们关注的是结果,哪些插件与我们的生产息息相关,我们如何去使用 babel 的插件。
通过 vue 中的 babel 配置来了解 babel
vue 脚手架生成的项目在.babelrc 文件中的配置:
{
“presets”: [
[“env”, {
“modules”: false,
“targets”: {
“browsers”: [“> 1%”, “last 2 versions”, “not ie <= 8”]
}
}],
“stage-2”
],
“plugins”: [“transform-vue-jsx”, “transform-runtime”]
}
plugin 配置项
babel 插件推崇的是功能的单一性,就是每个插件的功能尽可能的单一,比如我要使用 es6 的箭头函数,那就可以装一个转换插件 npm i -D @babel/plugin-transform-arrow-functions,将其写进.babelrc 文件里就行了:详情
{
“presets”: [],
“plugins”: [“@babel/plugin-transform-arrow-functions”]
}
这样,我们写的:
(a) => […a]
会被该插件转换为:
function (a) {
return […a]
}
这个插件也只解决箭头函数的转换,就算函数内部用了其它新语法也不管,这个好处是很明显的,就跟咱写项目推崇组件的细腻度一个道理
presets 配置项
然而呢,js 发展有点快,想一下那个 es2015(es6)一下加了多少东西,我们要使用还得一个一个的 npm i -D xxx,这个有点小麻烦,所以就可以采用 presets 配置项。npm i -D babel-preset-es2015,然后配置.babelrc。详情
为了承接上文,这里暂时先用 babel6 的写法,babel7 里也可以用 babel-preset-es2015,但是文档里去掉了,es2015、es2016、es2017(2018 年的东西直接写在 env 里了,7 月份 2019 年的新标准就要来罗 @_@)等都被放在 env 里面了,以后这几个 preset 会不会砍掉就不知道咯
{
“presets”: [“es2015”],
“plugins”: []
}
这样我们就可以使用包括箭头函数在内的 es6 的新语法而不用去担心兼容问题。这下这两个的关系也就清晰了,presets 里面配置的是一些 plugins 集合
在 babel 7.3.0 里面,presets — 对应插件有这些:
env — @babel/preset-env
stage-0 — @babel/preset-stage-0
stage-1 — @babel/preset-stage-1
stage-2 — @babel/preset-stage-2
stage-3 — @babel/preset-stage-3
flow — @babel/preset-flow
react — @babel/preset-react
minify — babel-preset-minify
typescript — @babel/preset-typescript
env
在 presets 配置里面,我们看到了:
[“env”, {
“modules”: false,
“targets”: {
“browsers”: [“> 1%”, “last 2 versions”, “not ie <= 8″]
}
}]
这个 env 是 @babel/preset-env 这个集合插件配置项,这里的配置项:
modules:”amd” | “umd” | “systemjs” | “commonjs” | “cjs” | “auto” | false, defaults to “auto”.
意思就是让 babel 把 es6 模块转化为其它模块化类型。如果选择 false 就不进行模块化转,我们的目标是浏览器,es6 以前 js 是没有模块化的,commonjs、amd 等只是社区方案,没有浏览器支持的,所以我们设置为 false,如果我们写 node 上运行的代码,就要设置为 ”commonjs”
target:就是告诉 babel 你的 js 要兼容哪些环境,它会帮你将你写的 js 转译成目标环境兼容的 js 语法,这个具体配置可以看 browserslist
那就是说,js 无论用什么新玩意,@babel/preset-env 都能跟我兼容到我想要的环境?带着问题,我们再看看官网的介绍,What is Babel:
Transform syntax(转换语法)
Polyfill features that are missing in your target environment(Ployfill 新特性 – 也就是 Api)
Source code transformations(源码转换)
And more!
所以呢,我们的.babelrc 文件里面配置的只是转换语法的,所以,对于上面的问题答案是“js 无论用什么新语法,@babel/preset-env 都能帮你兼容到目标环境”。比如:
a => a
// 转为
function (a) {return a}
至于第二点,api 的 polyfill,我们在下面介绍
@babel/preset-env 具体可以帮我们转化哪些呢?看这儿 JavaScript 新特性和 Babel 插件的映射关系,这个是 @babel/preset-env 集合插件所包含的插件列表,每个插件对应转换一个新特性,至于没有的,比如 promise,请往下看。
stage-2
在上面的配置中,我们看到 env 下面有个 stage-2。stage-x,这里面包含的都是当年最新规范的草案,每年更新。细分为如下几步
Stage 0 – 稻草人: 只是一个想法,经过 TC39 成员提出即可。
Stage 1 – 提案: 初步尝试。
Stage 2 – 初稿: 完成初步规范。
Stage 3 – 候选: 完成规范和浏览器初步实现。
Stage 4 – 完成: 将被添加到下一年度发布。
官网里有一句话 It is important to note that @babel/preset-env does not support stage-x plugins.,就是说 @babel/preset-env 中不包含在草案阶段的新属性
其实我们通过 plugin-features,以及 proposals/finished-proposals(其中 2019 就是今年的 stage-4), 可以发现 @babel/preset-env 是包含了 stage- 4 阶段的 plugins 的。
比如写 react 的同学比较熟悉的 decorators 目前就处于 stage- 2 阶段,我们要用这些处于草案阶段的新属性,可以安装 npm i -D @babel/preset-stage-2,然后在 presets 里写上 stage-2,babel 就会通过那些处于草案阶段的新属性的插件将我们代码中的用到的新属性转译成为 es5
此外,低一级的 stage 会包含所有高级 stage 的内容,例如 stage-2 会包含 stage-2, stage-3 的所有内容。
babel-ployfill
Babel 几乎可以编译所有时新的 JavaScript 语法,但对于 APIs 来说却并非如此。比如说:Promise、WeakMap、Array.from、Object.assign、Array.prototype.includes、generator 等。为了解决这个问题,我们使用一种叫做 Polyfill(代码填充,也可译作兼容性补丁)的技术。简单地说,polyfill 即是在当前运行环境中用来复制(意指模拟性的复制,而不是拷贝)尚不存在的原生 api 的代码。能让你提前使用还不可用的 APIs。
引入它很简单,我们 npm i -S @babel/polyfill,
在 vue 中的入口文件 main.js 文件的最上面:
import “@babel/polyfill”;
或者在 webpack 入口里引入:
module.exports = {
entry: [“@babel/polyfill”, “./main.js”],
};
两者任选其一
上面这两种方式是将整个 polyfill 都引入了,很多代码其实对我们是没有用的,比如,我们的 env 配置的是不需要兼容 ie9 以下的浏览器,这种引入方式把所有兼容 ie 的代码都引入了,包含 ie8 以下,所以,一般我们会在.babelrc 文件里的 env 里配置下 useBuiltIns 参数为 true,这样 babel 在引入的时候会根据我们 env 环境去加载相应的 polyfill:详细
// .babelrc
{
[“env”, {
“modules”: false,
“targets”: {
“browsers”: [“> 1%”, “last 2 versions”, “not ie <= 8”]
},
// 是否自动引入 polyfill,开启此选项必须保证已经安装了 babel-polyfill
// 在这里设置自动引入后,babel 会根据你配置的兼容的环境,去按需加载 polyfill 的代码,这样能保证代码量最少
// 参数:Boolean,默认为 false.
“useBuiltIns”: true
}]
}
@babel/plugin-transform-runtime
我们看到上面的配置中有个 transform-runtime,这个是配置 @babel/plugin-transform-runtime,它是做什么的呢?官网说:一个插件,通过重复使用 babel 注入的助手(helper)代码,来减少代码体积,我们看看它是如何工作的。
npm i -D @babel/plugin-transform-runtime
// .babelrc
{
“plugins”: [
“@babel/plugin-transform-runtime”,
// 默认配置
{
“absoluteRuntime”: false,
“corejs”: false,
“helpers”: true,
“regenerator”: true,
“useESModules”: false
}
]
}
比如这个 es6 的 class 类:
class Person {
}
在没有使用 transform-runtime 时,每个使用 class 函数处,Babel 会生成 class 的 helper 函数放置在文件顶部,就是说多个文件使用了 class, babel 就会在每个文件里面生成一个相同的 helper:
“use strict”;
function _classCallCheck(instance, Constructor) {if (!(instance instanceof Constructor)) {throw new TypeError(“Cannot call a class as a function”); } }
var Person = function Person() {
_classCallCheck(this, Person);
};
这样不必要的重复会使我们的代码体积非常雍肿,transform-runtime 就是来解决这个重复生成 helper 的问题的,它会将这个 es6 的 class 语法的转译函数放在 babel-runtime/helpers/classCallCheck 里,然后在使用处通过 require 引入,这样将转译函数放在一起,就可以通过 webpack 将其打包成一个文件,浏览器加载一次就行了。
“use strict”;
var _classCallCheck2 = require(“babel-runtime/helpers/classCallCheck”);
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { default: obj}; }
var Person = function Person() {
(0, _classCallCheck3.default)(this, Person);
};
@babel/plugin-transform-runtime && @babel/runtime 对比 babel-polyfill
官方推荐我们将 @babel/plugin-transform-runtime 和 @babel/runtime 结合使用,有必要吗?按照官网的原话,这个 @babel/runtime 就是个以模块化方式包含函数实现的包,我们从名字上来看,plugin-transform-runtime 就是通过插件将我们的新语法转换 polyfill 包,而 runtime 就是个现成的 polyfill 库。有点绕了,直接给结果:在我们开发前端项目中,不需要 @babel/runtime,它作用在 node 项目中。
tranform-runtime 和 babel-polyfill 的区别
babel-polyfill 是当前环境注入这些 es6+ 标准的垫片,好处是引用一次,不再担心兼容,而且它就是全局下的包,代码的任何地方都可以使用。缺点也很明显,它会污染原生的一些方法,polyfill 把原生的方法重写了,如果当前项目已经有一个 polyfill 的包了,那你只能保留其一。而且一次性引入这么一个包,会大大增加体积。如果你只是用几个特性,就没必要了,如果你是开发较大的应用,而且会频繁使用新特性并考虑兼容,那就直接引入吧。
transform-runtime 是利用 plugin 自动识别并替换代码中的新特性,你不需要再引入,只需要装好 babel-runtime 和 配好 plugin 就可以了。好处是按需替换,检测到你需要哪个,就引入哪个 polyfill,如果只用了一部分,打包完的文件体积对比 babel-polyfill 会小很多。而且 transform-runtime 不会污染原生的对象,方法,也不会对其他 polyfill 产生影响。所以 transform-runtime 的方式更适合开发工具包,库,一方面是体积够小,另一方面是用户(开发者)不会因为引用了我们的工具,包而污染了全局的原生方法,产生副作用,还是应该留给用户自己去选择。缺点是随着应用的增大,相同的 polyfill 每个模块都要做重复的工作(检测,替换),虽然 polyfill 只是引用,编译效率不够高效。另外,instance 上新添加的一些方法,babel-plugin-transform-runtime 是没有做处理的,比如 数组的 includes, filter, fill 等,这个算是一个关键问题吧,直接推荐用 polyfill
几个常用的 babel 插件
babel-cil
Babel 的 CLI 是一种在命令行下使用 Babel 编译文件的简单方法。有时候我们只是写一个插件,需要用 babel 转一下我们代码中的高阶语法,因为项目可能不太大,用不到构建工具,就可以用 babel-cil。转换依据我们的.babelrc 文件或者 package.json 中 babel 选项
编译一个文件
babel my-file.js
如果我们想要把一个目录整个编译成一个新的目录,可以使用 –out-dir 或者 -d。.
$ babel src –out-dir lib
# 或
$ babel src -d lib
babel-loader
babel-loader 是什么呢?前面说了,我们可以通过 babel-cil 在命令行里告诉 babel 转译哪些 js,也可以通过 babel-register,在代码里通过 require 来转,但是,现在前端开发是一个工程化过程,依赖关系比较复杂,在一个稍微大点儿的项目中还真没法手动告诉 babel 要处理哪些文件,比如一个.vue 文件,里面还包含 html、css,还有一些不是 js 的鬼语法,这时候就要借助其它插件先提前处理下,所以,webpack 根据依赖关系,加载文件的时候遇到 js 文件后,会将文件内容的 js 字符串根据 loader 配置的先后顺序,挨个儿传递给它们处理,babel-loader 就是其中之一
总结
什么是 babel
babel 就是将目标环境(浏览器)通过打补丁升级成支持最新 javascript 语法的环境。
用 vue 脚手架生成的项目,js 怎么兼容到 ie9
// .babelrc
{
“presets”: [
[“env”, {
// 这里默认是 false,不用再写一遍
– // “modules”: false,
// 一般不单独写出来,babel/preset-env 会自个读取 package 里面的 browserslist,与 css 兼容环境保持一致
// https://github.com/browserslist/browserslist
– // “targets”: {
– // “browsers”: [“> 1%”, “last 2 versions”, “not ie <= 8”]
– // },
+ “useBuiltIns”: true
}],
“stage-2”
],
“plugins”: [“transform-vue-jsx”, “transform-runtime”]
}
// webpack.base.conf.js
module.exports = {
entry: [“@babel/polyfill”, “./main.js”],
};
插件快照
名称
作用
备注
babel/cli
允许命令行使用 babel 命令转译文件
一般在写插件时使用
babel/polyfill
为所有 API 增加兼容方法
需要在所有代码之前 require,且体积比较大
babel/plugin-transform-runtime
把帮助类方法从每次使用前定义改为统一 require,精简代码
—
babel/runtime
helper 库
需要安装为依赖,而不是开发依赖,node 环境使用,web 环境不需要
babel/loader
babel 插件在 webpack 项目中的一个入口
—
babel/core
babel 的 polyfill 库
—
babel/preset-env
babel 预制环境的集合插件,通过配置目标环境,转换标准上的新特性
只转新特性,不转 api
babel/preset-stage-2
转换草案 stage- 2 以及 stage- 3 阶段的的新属性
—
参考:
babel
Babel 用户手册
Babel 插件手册
babel-vs-babel-core-vs-babel-loader
Babel 入门教程
一口 (很长的) 气了解 babel
tc39 草案
你真的会用 babel 吗?
babel-preset-env/data/plugin-features.js
babel-plugin-transform-runtime 和 babel-runtime 的区别?