前言
本教程基于目前最新的Babel7.26.0版本,后续Babel版本如果有大的变动,我会另写一篇介绍变动点。
1. Babel是什么
Babel是一个工具链,主要用于将es6+代码转换为当前和旧版浏览器或环境中向后兼容的JavaScript版本
2. Babel编译流程
Babel的编译流程分为三步:parsing(解析)、transforming(转化)、generating(生成)
-
解析:@babel/parser 负责将ES6代码进行语法分析和词法分析后转换成抽象语法树AST
-
转换:@babel/traverse 负责遍历AST并进行节点的操作
-
生成:@babel/generator 负责通过AST树生成ES5代码
而整个过程,由@babel/core负责编译过程的控制和管理。它会调用其他模块来解析、转换和生成代码
3. Babel编译流程示例
情景:
将函数中的 let 转为 const,访问AST转换平台:astexplorer.net/,将以下函数输入,右侧将出现转换结果
function example() {
let a = 1;
let b = 2;
return a + b;
}
右侧部分截图:
body是函数体部分,函数体内部又包含两个变量声明VariableDeclaration以及一个return语句ReturnStatement
思路:
-
将源码转成 ast
-
遍历树节点,当遇到 type === 'VariableDeclaration' && kind === 'let' 时,将其 kind 转为 'const'
-
将 ast 转为源码
步骤:
-
新建一个文件夹,运行npm init初始化一个package.json
-
分别安装@babel/parser,@babel/traverse,@babel/generator
-
新建index.js
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const code = `function example() {
let a = 1;
let b = 2;
return a + b;
}`;
// 1.解析:使用babel解析器解析源码为AST
const ast = parser.parse(code);
// 定义一个遍历AST的访问器对象,也就是访问到目标节点【这里是VariableDeclaration】的时候会做什么处理
const visitor = {
VariableDeclaration(path) { // 这里的 path 是指当前的上下文,而不是路径
if (path.node.kind === 'let') {
path.node.kind = 'const';
}
}
};
// 2. 转换:使用traverse遍历AST并应用访问器,也就是遍历并应用刚才那个 visitor 规则
traverse(ast, visitor);
// 3. 生成:使用generate根据修改后的AST生成新的代码
const output = generate(ast, {});
// 打印修改后的代码
console.log(output.code);
使用VS Code的插件Code Runner直接运行index.js查看打印结果,可以看到输出的函数中let已全部被替换为const
4. Babel重要组成
plugins 插件
Babel 插件是一段代码,用于对 Babel 在编译过程中生成的抽象语法树(AST)进行操作。Babel 强调功能的单一性,即每个插件应专注于实现一个特定的功能。例如,如果你想使用 ES6 的箭头函数,就需要使用 @babel/plugin-transform-arrow-functions 插件。想要使用React的jsx语法,就需要使用@babel/plugin-transform-react-jsx。
然而,ES6、ES7 等版本引入了许多新语法特性,难道要一个个单独安装这些插件吗?这时,预设(presets)就派上用场了。
presets 预设
presets预设实际上是一组插件的集合,它们被预先配置好,以解决特定的需求。例如,@babel/preset-env 是一个常用的预设,它可以根据你的目标环境自动选择需要的插件,以便将代码转换为兼容的版本。
查看Babel文档,可以看到@babel/preset-env中有许多plugins。例如,ES6的箭头函数使用arrow-functions处理,也就是@babel/plugin-transform-arrow-functions这个插件。
plugins 和 presets 的运行顺序
Babel 配置可以通过 .babelrc 文件、package.json 文件中的 babel 字段或者通过配置文件 babel.config.js 来指定。plugins字段指定插件的集合,Presets字段指定预设的集合。
"plugins": ["pluginA", "pluginB"],
"presets": ["a", "b", "c"]
Plugins 执行顺序从前往后:pluginA,pluginB
presets 执行顺序从后往前: c , b ,a
Plugins 在 Presets 前运行
5. polyfill 介绍
垫片的主要作用就是支持旧版浏览器。对于支持新语法的浏览器不进行代码兜底兼容,对于旧版浏览器使用旧语法来兼容。
访问 browsersl.ist/ 查看浏览器的使用情况,例如在右侧输入dead,查看哪些浏览器处于不再维护状态
可以看到dead状态的浏览器,全球还有0.62%的人们在使用。IE浏览器11版本,全球范围还有0.45%的人在用。未来使用人数只会更低。
新建一个文件夹,运行npm init初始化一个package.json,修改package.json,配置browserslist,指定最低的浏览器版本
"browserslist": [
"Chrome >= 69",
"Firefox >= 95",
"last 2 safari version",
"ie 8"
]
browserslist 配置会告诉 Babel 需要支持哪些浏览器。Babel 会根据这些浏览器的特性支持情况,自动选择需要转换的语法特性。 例如,如果某个现代 JavaScript 特性在 Chrome 69 及以上版本中已经支持,Babel 就不会对其进行转换。
6. 安装
yarn add @babel/core @babel/cli @babel/preset-env @babel/plugin-transform-runtime -D
yarn add core-js@3 @babel/runtime-corejs3 -S
@babel/core:负责编译过程的控制和管理。它会调用其他模块来解析、转换和生成代码
@babel/cli:一个内置的CLI命令行工具,可通过命令行编译文件
@babel/preset-env:根据目标环境自动选择需要的转义插件,转换现代 JavaScript 代码。
@babel/runtime:包含一些辅助(helpers)函数,实现polyfill时函数的复用。@babel/runtime不需要额外安装,安装@babel/preset-env时,会作为依赖包一同被安装。
regenerator-runtime:负责处理异步函数(如generator、async/await)。不需要额外安装,作为@babel/runtime的依赖,会一同被安装。
@babel/plugin-transform-runtime:在编译过程中,自动使用 @babel/runtime 里的辅助函数。
core-js:它提供了广泛的老版本浏览器兼容的 JavaScript 功能实现,包括但不限于 Promise、Map、Set、Array.from、String.prototype.includes 等。
@babel/runtime-corejs3:专门为与 @babel/plugin-transform-runtime 一起使用而设计,提供 core-js-pure 的 polyfills 和一些辅助函数。
提示:@babel/polyfill由于不支持按需引入和会全局污染window对象等缺点,从Babel7.4开始,@babel/polyfill就不再推荐使用。
7. polyfill 三种方式
babel将ES6+版本的代码分为了两种情况,语法层和api层面:
语法层: let、const、class、箭头函数等,这些默认会被Babel转义
api层面:Promise、includes、map等,这些是在全局或者Object、Array等原型上新增的方法,需要借助polyfill来完成转义
1. @babel/preset-env entry模式
- 新建babel/input.js
const fn = () => {
console.log("测试箭头函数");
};
new Promise((resolve, reject) => {
resolve("测试Promise");
}).then((res) => {
console.log(res);
});
- 修改package.json的scripts,新增启动命令
"babel": "babel babel/input.js --out-file babel/output.js"
- 新建babel.config.json
{
"presets": ["@babel/preset-env"],
"plugins": []
}
运行 npm run babel 进行打包,生成的output.js
"use strict";
var fn = function fn() {
console.log("测试箭头函数");
};
new Promise(function (resolve, reject) {
resolve("测试Promise");
}).then(function (res) {
console.log(res);
});
可以看到语法层面的箭头函数降级了。但是api层面的Promise并未做处理,对于不支持Promise语法的浏览器,需要polyfill垫片处理。
- 修改babel.config.json,配置@babel/preset-env做polyfill垫片处理
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": {
"version": 3
}
}
]
],
"plugins": []
}
entry模式,并且指定corejs的版本。
- 修改input.js,手动导入core-js/stable
import "core-js/stable";
const fn = () => {
console.log("测试箭头函数");
};
new Promise((resolve, reject) => {
resolve("测试Promise");
}).then((res) => {
console.log(res);
});
运行 npm run babel 进行打包,生成的output.js的部分截图,光是require就有2百多行。
useBuiltIns 设置为 'entry'时,会注入目标环境不支持的所有api层面方法。但大部分情况下肯定是希望按需引入,减小打包体积的,这时可使用usage模式。
2. @babel/preset-env usage模式
- 修改babel.config.json,配置@babel/preset-env做polyfill垫片处理
{
"presets": [
[
"@babel/preset-env",
{
// 实现按需加载
"useBuiltIns": "usage",
"corejs": {
"version": 3
}
}
]
],
"plugins": []
}
- 修改input.js,去掉import 'core-js/stable'
const fn = () => {
console.log("测试箭头函数");
};
new Promise((resolve, reject) => {
resolve("测试Promise");
}).then((res) => {
console.log(res);
});
运行 npm run babel 进行打包,生成的output.js
"use strict";
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.promise.js");
var fn = function fn() {
console.log("测试箭头函数");
};
new Promise(function (resolve, reject) {
resolve("测试Promise");
}).then(function (res) {
console.log(res);
});
可以看到只导入了相应api层面的降级处理,例如require("core-js/modules/es.promise.js");处理了Promise的降级处理。
使用@babel/preset-env进行polyfill的问题
问题1:会造成全局污染
core-js是通过require("core-js/modules/es.promise.js");方式导入了Promise的垫片用法,使得你即使在不支持Promise的浏览器中,直接使用 Promise。但如果第三方库也可能定义了相同名称的方法或属性,会造成冲突
问题2. 会生成重复的辅助函数,增大打包体积
修改input.js,增加async,await语法
const fn = () => {
console.log("测试箭头函数");
};
new Promise((resolve, reject) => {
resolve("测试Promise");
}).then((res) => {
console.log(res);
});
async function ge(){
await console.log("测试async,await");
}
运行 npm run babel 进行打包,生成的output.js部分截图
除了require的部分,还多了好多定义的函数,这些是辅助函数(比如上边的_regeneratorRuntime函数),是在编译阶段辅助 Babel 的函数;问题来了,现在只有一个JS文件需要转换,然而实际项目开发中会有大量的需要转换的文件,如果每一个转换后的文件中都存在相同的函数,那代码总体积肯定会无意义的增大。
而@babel/runtime中包含了常用的辅助函数,可以减少打包体积,这也涉及到第3种polyfill方式:@babel/plugin-transform-runtime。
3. @babel/plugin-transform-runtime
辅助函数被统一封装在@babel/runtime中提供的helper模块中,编译时,@babel/plugin-transform-runtime自动使用 @babel/runtime 里的辅助函数
- 修改babel.config.json
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": { "version": 3 }
}
]
]
}
这里指定版本3,对应着上面下载的@babel/runtime-corejs3。如果指定版本2,则下载@babel/runtime-corejs2。
运行 npm run babel 进行打包,生成的output.js部分截图
可以看到定义的函数消失了,而是通过导入辅助函数,导入了所需的函数。例如使用@babel/preset-env垫片时生成的_regeneratorRuntime,在@babel/plugin-transform-runtime中直接导入了_regenerator。
当然,如果因为某些原因,就是要在文件中定义辅助函数,而不是从外部引入,此时可通过设置helpers为false
- 修改babel.config.json
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": { "version": 3 },
"helpers": false
}
]
]
}
运行 npm run babel 进行打包,生成的output.js部分截图
可以看到用于垫片异步函数的_regeneratorRuntime又重新内联在文件内。此使用场景很少,并且helpers选项将在未来的 Babel 8 中删除。
提示:
使用@babel/plugin-transform-runtime进行垫片时,不再需要core-js,因为@babel/plugin-transform-runtime的依赖包core-js-pure,会在安装@babel/plugin-transform-runtime时一同安装。
core-js-pure是core-js的一个纯净版本,它的设计目标是不对全局作用域进行任何修改,防止全局污染。可以看到它是通过定义一个变量的方式来使用,例如_promise。
总结:
语法层面,Babel会使用@babel/preset-env默认进行转义;api方法层面,需要额外的polyfill来兼容旧环境。目前共有三种方式:
-
使用 @babel/preset-env ,useBuiltIns 设置为 'entry':会注入目标环境不支持的所有api层面方法,需在代码中主动使用import 'core-js/stable'
-
使用 @babel/preset-env ,useBuiltIns 设置为 'usage':会注入目标环境不支持的所有被用到的api层面方法
-
使用 @babel/plugin-transform-runtime:通过局部变量的方式实现了所有被用到的api层面方法,不会污染全局
8. 案例分析
分析create-react-app搭建的React项目是如何做polyfill的
1. 使用create-react-app搭建一个react项目
npm config set registry https://registry.npmmirror.com
npx create-react-app my-app
2. 运行npm run eject 将配置暴露出去,方便查看配置
3. 查看配置config/webpack.config.js,找到babel-loader
babel-loader 是一个 Webpack 加载器(loader),用于将 JavaScript 文件通过 Babel 编译器进行转换。
presets预设中通过require.resolve('babel-preset-react-app')引入了babel-preset-react-app。
4. 从node_modules找到babel-preset-react-app
babel-preset-react-app/index.js,内容如下所示
'use strict';
const create = require('./create');
module.exports = function (api, opts) {
const env = process.env.BABEL_ENV || process.env.NODE_ENV;
return create(api, opts, env);
};
可以看到主要还是用了create.js文件内容
5. 查看create.js文件return语句,找到presets配置
可以看到预设presets中主要配置了三个预设,@babel/preset-env用于转换现代 JavaScript 代码,@babel/preset-react专门用于处理React代码,@babel/preset-typescript专门用于处理TypeScript。
@babel/preset-env开启了entry模式,意味着在代码中,需手动导入core-js/stable时,才启动polyfill。一般情况下使用不到这个功能。
6. 继续看return语句,找到plugins配置
plugins中的配置较多,只展示@babel/plugin-transform-runtime相关代码
@babel/plugin-transform-runtime的corejs为false,说明没有开启相关polyfills。
为什么不需要polyfills?查看package.json的browserslist配置
7. browserslist
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
开发环境不用说,用的都是最新的浏览器,生产环境也进行了相应的浏览器配置。访问 browsersl.ist/ 查看浏览器的使用情况,例如在右侧输入> 0.2% and not dead and not op_mini all,查看浏览器使用情况。
Chrome目前最低的支持版本为109,此配置可以说的上是只支持新版浏览器了。
polyfills是什么:polyfills垫片的主要作用就是支持旧版浏览器。对于支持新语法的浏览器不进行代码兜底兼容,对于旧版浏览器使用旧语法来兼容。
既然只支持新版浏览器,那么就没有使用polyfills的需要了。
8. 为什么只支持新版浏览器?
因为React 18 引入了许多新特性,如并发模式、自动批处理等,这些特性依赖于现代浏览器提供的 API 和性能优化。React 团队选择不再支持 IE11等旧版浏览器。
结尾
本文若存在不准确或不完整之处,欢迎各位读者批评指正