本文基于 babel 7 做叙述,如果之前一直使用 babel 6 的同学可以先看本文关于 babel 6 升级 babel 7 的相关模块
Babel 是如何工作的呢?
Babel 的编译过程可以分为三个阶段:
- 解析(Parsing):将代码字符串解析成抽象语法树。
- 转换(Transformation):对抽象语法树进行转换操作。
- 生成(Code Generation): 根据变换后的抽象语法树再生成代码字符串。
解析:@babel/parser (Babylon)
使用 @babel/core 中的 parse API 来进行词法分析(分词+语法分析),转化成 AST,为 Transform 准备。
Tip:感兴趣的同学可以阅读这篇文章深入了解下:www.alloyteam.com/2017/04/ana… 本文重在讲述 babel 在项目中的实践呀。
转换:使用者可以决定的过程
使用 @bable/core 中的 transform API 来进行转义;我们熟知的插件就是应用在这一过程,如果这一阶段不使用任何插件,Babel 将在生成阶段输出原样的代码;
- plugins 和 presets 如何配合工作的呢?
- Preset 是 Babel 官方做的一些插件集,可以把它理解成一些插件集合预设,减轻繁琐的配置。每年每个 Preset 只会编译当年批准的内容;
- plugin/preset 执行顺序:对于转义的节点,babel 先按照配置文件的 plugins 数组配置去执行,执行完,再倒序读取 presets 配置项里每一个预设,如下图:
- @babel/plugin-proposal-class-properties
- @babel/plugin-proposal-decorators
- @babel/plugin-proposal-export-default-from
- babeljs.io/docs/en/bab…
- 常用预设 presets:
- @babel-preset-env:根据用户的自定义的配置项,来将 ES2015+ 转化为 ES5,详细说明可以看一下@babel-preset-env 配置说明
- @babel/preset-react:转义 react 代码,详情可以阅读:www.babeljs.cn/docs/babel-…
- @babel-preset-typescript:转义 typescript 代码,可以阅读该文章来加深对该插件的认识喔:iamturns.com/typescript-… 通常对于 react 项目,presets 使用 @babel/preset-env + @babel/preset-react 便 ok 了,但是随着 TypeScript 的火热,babel 7 为开发者带来了 @babel/preset-typescript ,用于支持 ts 开发,从下图可以看出,这个插件使用趋势在增长势头很猛呀;
- 配置推荐
// react + typescript
"presets": [
[
"@babel/preset-env",
{
"targets": "cover 95%, safari >= 7", # 根据项目实际情况配置
"modules": "cjs",
"useBuiltIns": "usage"
}
],
"@babel/react",
"@babel/preset-typescript", # 如果项目使用 typescript 开发需要安装这个预设
]
生成:
用 babel-generator 通过 AST 生成 ES5 代码; TIP:如果在 babel 的配置中,不使用任何 presets 和 plugins,那么生成的代码和原代码无异;
你需要知道的 @babel-polyfill
背景:
- 由于 babel 本身只负责语法转换,比如将 ES6+ 语法转化为 ES5。但是如果有些对象、方法,浏览器本身不支持,比如:
- 全局对象:Promise、WeakMap 等。
- 全局静态函数:Array.from、Object.assign 等。
- 实例方法:比如 Array.prototype.includes 等。 此时,需要引入 @babel-polyfill 来模拟实现这些对象与方法。
- @babel-polyfill 主要包含了core-js和regenerator两部分。
- babel-polyfill:提供了如 ES5、ES6、ES7 等规范中 中新定义的各种对象、方法的模拟实现。
- regenerator:提供 generator 支持,如果应用代码中用到 generator、async 函数的话用到。
不足:
- @babel-polyfill 造成代码代码体积过大
以往项目中使用 polyfill,常规的做法有两种:
- 在入口 js 文件中 import @babel-polyfill
- 在webpack 的 entry 配置里写入 @babel-polyfill
- @babel-polyfill 的会污染全局空间和内置原型对象 像Map,Array.prototype.find这些就存在于全局空间中。
@babel-polyfill VS @babel-runtime
@babel-runtime 出现背景
@babel-polyfill 的不足,在日常业务开发中,影响不是很大的,但是如果在通用的第三方库的开发中引入 @babel-polyfill,就会带来潜在的问题:比如我们项目中自定义了一份新的 API 的实现,但是引入的第三方通用库使用了polyfill,并且实现了同样的方法,就会将我们原有的方法覆盖(至少会有存在这样的风险),为了避免这样的问题,babel 推出了 @babel-runtime 来实现 @babel-polyfill 的绝大部分的功能,它将开发者依赖的全局内置 对象等,单独抽成模块,通过模块导入的方式,避免了对全局作用域的污染。 所以,如果是开发库、工具,可以考虑使用 @babel-runtime;
如何使用 @babel-runtime 来实现 polyfill
- 安装 @babel/plugin-transform-runtime 和 @babel-runtime
# babel-plugin-transform-runtime 用于构建过程的代码转换
npm install --save-dev babel-plugin-transform-runtime
npm install --save babel-runtime
- .babelrc plugins 中添加 @babel/plugin-transform-runtime
{
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": false,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
@babel-runtime + @babel/plugin-transform-runtime 实现细节
@babel/plugin-transform-runtime 插件主要做了如下三件事情:
- core-js aliasing:自动导入 @babel-runtime/core-js,将全局静态方法、全局内置对象映射到对应的模块;
# js
var sym = Symbol();
var promise = new Promise();
console.log(arr[Symbol.iterator]());
# 转化后
"use strict";
var _getIterator2 = require("@babel/runtime-corejs2/core-js/get-iterator");
var _getIterator3 = _interopRequireDefault(_getIterator2);
var _promise = require("@babel/runtime-corejs2/core-js/promise");
var _promise2 = _interopRequireDefault(_promise);
var _symbol = require("@babel/runtime-corejs2/core-js/symbol");
var _symbol2 = _interopRequireDefault(_symbol);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
var sym = (0, _symbol2.default)();
var promise = new _promise2.default();
console.log((0, _getIterator3.default)(arr));
- Helper aliasing:将内联的工具函数移除,改成通过 @babel-runtime/helpers 模块进行导入
# js
class Person() {}
# 转化后
"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);
};
- Regenerator aliasing:用于在项目中使用 async/generator 时,自动导入 @babel-runtime/regenerator 模块
# js
function* foo() {}
# 转化后
"use strict";
var _regenerator = require("@babel/runtime/regenerator");
var _regenerator2 = _interopRequireDefault(_regenerator);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
var _marked = [foo].map(_regenerator2.default.mark);
function foo() {
return _regenerator2.default.wrap(
function foo$(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
case 0:
case "end":
return _context.stop();
}
}
},
_marked[0],
this
);
}
@babel-runtime + @babel/plugin-transform-runtime 缺点
由于 @babel-runtime 和 @babel/plugin-transform-runtime 实现方式导致它不会对实例方法进行扩展(例如 Array.prototype.includes()
),所以在使用中,存在很大的隐患,简单的说,如果你使用的是 @babel-runtime 实现的polyfill,那么,就会出现你使用的部分原型方法得不到支持而报错,这是不能接受的,所以我推荐在项目开发中,还是使用 @babel-polyfill 比较稳妥。
@babel/preset-env preset 带来的 @babel-polyfill 按需引用
TIP: 关于 @babel/preset-env 的详细内容可以查看下一小节的内容 通过在 presets 里使用 @babel/preset-env preset,设置 useBuiltInts 为 usage 或者或者 entry 来实现按需引入; 与@babel/preset-env一起使用时(注意,仍然需要安装@ babel-polyfill。)
- 如果在.babelrc 中指定 useBuiltIns:'usage',则不要在webpack.config.js 和 入口 js 文件引入 @babel-polyfill。
- 如果在.babelrc中指定了useBuiltIns:'entry',则通过require或import将@ babel / polyfill包含在应用程序入口点的顶部。
- 如果未指定 useBuiltIns 键或在 .babelrc 中使用 useBuiltIns:false 显式设置,请将@ babel / polyfill直接添加到webpack.config.js 对应的配置中去。
你需要了解的 @babel/preset-env
使用趋势
从 2018-9-17 发布至今,每周的下载量都在稳步提升,推荐小伙伴们在项目中使用基于 babel 7的 @babel/preset-env 插件预设;
简单介绍
@babel-preset-env 将基于你的实际浏览器及运行环境,自动的确定 babel 插件及 polyfill,编译ES2015 及此版本以上的语言,在没有配置项的情况下,@babel-preset-env 表现的同 babel-preset-latest一样(或者可以说同babel-preset-es2015、babel-preset-es2016、 babel-preset-es2017 结合到一起表现的一致)。
优点
- 可以根据配置的目标环境自动采用需要的 babel 插件;
- 简化了 babel 配置
- 按需引入必要插件,优化了包体积;
- 支持 polyfill 相关配置,按需引入相关的 polyfill,优化代码体积; TIP:可以通过 www.npmjs.com/package/@ba… 来查看项目版本使用的 @babel/preset-env 版本依赖的 plugins,保证代码中使用的 js 新的语言特性得到支持,如果没有,可以通过升级 @babel/preset-env 或者安装对应的 plugin 来支持;
不足:
对于实验属性(babel-preset-latest 不支持的)需要手动安装配置相应的 plugins 或者 presets; 可以通过 babeljs.io/docs/en/plu… 来查询相关的实验属性对应的插件,来引入支持业务开发;
配置指南
浏览器配置相关
- 每个浏览器最近的两个版本和 IE 大于等于7的版本所需的 polyfill 和代码转译。
"babel": {
"presets": [
[
"env",
{
"targets": {
"browsers": ["last 2 versions", "ie >= 7"]
}
}
]
]
},
- 支持市场份额超过5%的浏览器。
"targets": {
"browsers": "> 5%"
}
- 指定浏览器版本
"targets": {
"chrome": 56
}
polyfill 配置: useBuiltIns(默认 false)
@babel-preset-env 默认只支持对语法的转化,需要开启useBuiltIns配置才能转化 API 和实例方法, 来为标准库中的新功能提供了 polyfill,为内置对象,静态方法,实例方法,生成器函数提供支持。 @babel-preset-env 可以实现基于特定环境引入需要的polyfill。
- 可选值包括:"usage" | "entry" | false, 默认为 false,表示不对 polyfills 处理;
- entry:根据target中浏览器版本的支持,将polyfills拆分引入,仅引入有浏览器不支持的polyfill;
- usage:检测代码中ES6/7/8等的使用情况,仅仅加载代码中用到的polyfills;
Node.js 配置相关
如果你通过Babel编译你的Node.js代码,babel-preset-env 很有用,设置 "targets.node" 是 "current",支持的是当前运行版本的nodejs:
const presets = [
[
"@babel/env",
{
node: 'current',
},
],];
module.exports = { presets };
spec : 启用更符合规范的转换,但速度会更慢,默认为 false
loose:是否使用 loose mode,默认为 false
modules:将 ES6 module 转换为其他模块规范,可选 "adm" | "umd" | "systemjs" | #### "commonjs" | "cjs" | false,默认为 false
debug:启用debug,默认 false
include:一个包含使用的 plugins 的数组
exclude:一个包含不使用的 plugins 的数组
babel 6 迁移 babel 7
在迁移之前建议熟悉 babel 7 具体带来的改动,这样有助于我们在项目中进行具体的优化和进行针对性的配置修改
Babel 7 带来的改动
熟悉相关重要改动,可以帮助我们更好的去配置项目,使用最新支持的特性,提高编码效率
- 移除了年度预设用法
带有env 的预设已经发布了一年多,现在它会取代一下所有的用法:
- babel-preset-es2015
- babel-preset-es2016
- babel-preset-es2017
- babel-preset-latest
- 移除 stage 预设用法,迁移时可以参考 github.com/babel/babel… 进行升级;也可以通过 babel-upgrade 来使用命令行进行替换
- 移除 @babel-polyfill 中的提议:可以参考一下这里:github.com/babel/babel…
- 包重命名:babylon 现在重新命名为 @babel/parser (看起来是被收了)
- 包命名空间
babel 6 升级 babel 7 最重要的一个改动之一就是给所有的包加了命名空间,这样做可以减少很多意外或者故意的命名问题,可以与社区的其他插件有一个清晰的区分,形成一个 babel 的命名规范;例如:babel-preset-env 改名为 @babel/preset-env; - 把 TC39 提议都换成 -proposal 把那些非年度预设的 TC39 插件中的-transform都换成了-propoal,这样可以更好的区分出一个提议是否为 javascript 官方的。例如:
- @babel/plugin-tranform-function-bind 换成 @babel/plugin-proposal-function-bind
- 移除包中的年份
一些插件中还带有 -es2015等字符,现在做了统一的规范:统统去掉,例如: @babel/plugin-transform-es2015-classes 换成了 @babel/plugin-transform-classes - 分离 React 和 Flow 之间的预设
- 解析选项修改
现在 Babel 的配置选项会比以前的 Babel6 更加严格。像这样的一个逗号分隔列表:"presets": "es2015, es2016"在以前的版本中可以运行,今后都要改成数组才可以。但是这个对 CLI 不受影响,它依然可以使用逗号分隔的字符串。
# right
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
# wrong
{
"presets": "@babel/preset-env, @babel/preset-react"
}
- 插件/预设的暴露
今后所有的插件和预设向外暴露的都必须是一个函数,而不能是一个对象,这可以帮助我们进行缓存。 - JSX Fragment Support (<>)
render() {
return (
<>
<ChildA />
<ChildB />
</>
);
}
// output 👇
render() {
return React.createElement(
React.Fragment,
null,
React.createElement(ChildA, null),
React.createElement(ChildB, null)
);
}
- TypeScript Support (@babel/preset-typescript)
使用前(带有类型声明的 react 代码块)
interface Person {
firstName: string;
lastName: string;
}
function greeter(person : Person) {
return "Hello, " + person.firstName + " " + person.lastName;
}
使用后(移除类型声明)
function greeter(person) {
return "Hello, " + person.firstName + " " + person.lastName;
}
- Automatic Polyfilling (experimental)
在 babel 6 中,对于 polyfill 的支持都是采用如下方式:
import "@babel/polyfill";
但它包括整个 polyfill,如果浏览器已经支持,你可能不需要导入所有内容。这与@babel/preset-env 试图用语法解决的问题相同,所以我们在这里将它应用于polyfill。选项 useBuiltins:“entry” 通过将原始导入仅拆分为必要的导入来实现此目的。 但是我们可以通过仅导入代码库中使用的 polyfill 来做得更好。选项“useBuiltIns:”usage“是我们第一次尝试启用类似的东西(详细说明)。 关于 @babel/preset-env 详细的说明,可以参考这篇文档:@babel-preset-env 配置说明 ,理解其中每一个配置,来利用 babel 新增的 api 来结合项目自身特点进行配置,进行优化。
推荐迁移步骤:
第一步:进入到项目根目录下:执行 Babel 升级脚本,主要进行如下三种类型的修改:
- 将 babel-xx 替换成 @babel/xx,替换成新的插件
- 升级 babel-loader 到 支持 babel 7 的版本
- 安装废弃的 stage 相关的预设对应的 plugins ,各个 stage 对应的插件查询库
# 不安装到本地而是直接运行命令,npm 的新功能
npx babel-upgrade --write
# 或者常规方式
npm i babel-upgrade -g
babel-upgrade --write
以一个依赖 babel 6 的项目为例:通过 babel 官方提供的升级工具,轻松搞定依赖相关的问题:
- 对 babel-polyfill 进行包命名空间的转化的同时,又对其进行了升级,对于用户来说,只需要去配置文件中修改对应的包就 ok 了,毫无其他痛点,做的很友好;
- 安装 babel-preset-stage-1 这个废弃的 preset 依赖的 plugins 来保证原有的功能可用
- @babel/plugin-proposal-export-default-from
- @babel/plugin-proposal-logical-assignment-operators
- @babel/plugin-proposal-optional-chaining
- @babel/plugin-proposal-pipeline-operator
- @babel/plugin-proposal-nullish-coalescing-operator
- @babel/plugin-proposal-do-expressions
- 把 TC39 提议都转化为 -proposal 例如该项目中使用的 babel-plugin-transform-decorators-legacy -> @babel/plugin-proposal-decorators
第二步:根据上一步 package.json 文件的修改,同步修改 babel 文件中对应的配置,运行开发环境,根据 webpack 报错逐个解决
- 升级 babel-plugin-module-resolver:babel 升级到 babel 7 之后,babel-plugin-module-resolve V2 的版本会出现报错,需要升级到 V3;
- issue:github.com/tleunen/bab…
- @babel/plugin-proposal-pipeline-operator 插件没有添加相关配置项报错
- 处理方案:
["@babel/plugin-proposal-pipeline-operator", {
# 由于项目中并没有使用管道符,所以我这里就直接给一个 minimal 的属性值,其他项目可以根据具体需要做调整
"proposal": "minimal"
}]
- ES6 modules 与 commonJS 混用导致报错
第三步:优化项目 polyfill 配置
babel 7 带来了@babel/preset-env 这个插件,它扩展了对 polyfill 的配置,让使用者可以优化垫片的体积,做到按需加载,具体细节可以参考 @babel-preset-env 配置说明
- 移除入口文件引入的 @babel-polyfill
- 设置 @babel/preset-env useBuiltIns 属性为 usage,实现按需加载
- 设置 @babel/preset-env 的 corejs 属性,让该插件集不仅仅支持 stable ECMAScript Features,还支持 proposal ECMAScript Features
配置 Babel 支持 TypeScript 环境开发
可以参考我这篇 JavaScript 项目迁移 TypeScript 实践分享 文章来做迁移(在余下时间我会重新整理发布到掘金)
- 安装 @babel/preset-typescript 进行 babel 配置
# .babelrc
# yarn add @babel/preset-typescript
"presets": [
[
"@babel/env",
{
"targets": "cover 95%, safari >= 7",
"modules": "cjs",
"corejs": 2,
"useBuiltIns": "usage",
}
],
"@babel/react",
"@babel/typescript"
],
- 创建 TypeScript 配置文件 tsconfig.json
{
"compilerOptions": {
"module": "esnext",
"target": "es6",
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"sourceMap": true,
"outDir": "../client-dist",
"allowJs": true,
"jsx": "react",
"noEmit": true,
"moduleResolution": "node",
"isolatedModules": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
// `vs code`所需要的,在开发时找到对应的路径,真实的引用是在`webpack`中配置的`alias`
"paths": {
}
},
# 这里这么设计,是因为我们项目历史原因,js 的业务代码比较多,在跑 ci tsc 做类型检查的时候,只走 tsx? 类型文件
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": [
"node_modules"
]
}
- webpack 配置修改,支持 TypeScript 开发环境
- babel-loader 扩展对 .ts?x 文件解析
- resolves 对应的 extensions 增加对 ts 与 tsx 文件的支持
# package.json
{
"scripts": {
"tsc": "tsc --noEmit"
}
}
- 扩展 TypeScript 相关的 Eslint 配置,进行代码规范约束
- parser 由 babel-eslint 替换为 @babel-eslint/parser
- extends 属性增加对 typescript 代码约束插件
- prettier
- prettier/react
- prettier/@typescript-eslint
- plugins 属性扩展 @typescript-eslint 插件
总结
本人想通过本文和大家分享一下自己关于 Babel 在项目中使用的一些心得,希望可以给小伙伴们带来一些帮助,Babel 不是一个黑盒子,而是一个给开发者提供了各种各样选择的工具集,术业有专攻,只需要稍微花一点时间来理解它,就可以在项目中很安全地使用它,利用 Babel 团队不断努力给我们带来的新的语言特性的支持; 大家有什么疑问可以直接给我留言评论。
参考
gitissue.com/issues/5c18… juejin.cn/post/684490… juejin.cn/post/684490… segmentfault.com/q/101000000… www.jianshu.com/p/d078b5f30… jsweibo.github.io/2019/08/05/… juejin.cn/post/684490… blog.hhking.cn/2019/04/02/… github.com/sorrycc/blo… github.com/Kehao/Blog/… juejin.cn/post/684490… jsweibo.github.io/2019/08/09/… github.com/SunshowerC/… zhuanlan.zhihu.com/p/43249121 pdsuwwz.github.io/2018/09/29/… - babel 升级踩坑 juejin.cn/post/684490… juejin.cn/post/684490… github.com/lmk123/blog… segmentfault.com/a/119000001… segmentfault.com/a/119000001… www.zcfy.cc/article/bab… juejin.cn/post/684490…