babel怎么用?(基础版)
序言
前段时间在做vue UI组件库打包,打包UMD格式的时候rolup的babel插件报错:
[!] (plugin commonjs--resolver) Error: You have declared using "bundled" babelHelpers, but transforming xx/components/index.js resulted in "runtime". Please check your configuration.
事倍功半,这让我比较迫切的想要详细了解下babel的使用方式。
从业了几年,就接触过babel几年。做过不少项目,babel配置差不多,对babel的了解停留在最底层。
2020年12月份时看过文档,学习过一些,但因为这中间的日常工作挺大部分时间在Node.js上,babel用的少,就又还回去了。而且现在来看,当时了解过于浅显且结构不太清晰。
说明
- 本文涉及的使用方法的babel版本为7.18.x,babel不同版本 6.x, 7.4以下的存在差异
- 本文只是单纯babel的使用说明,不含不同使用场景的配置推荐,不含其他构建工具(比如rollup、webpack)的babel使用说明
- 文章写于2022-08-24,时间不止,变化不停
babel的作用
官网说明
Babel 是一个 JavaScript 编译器
Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情:
- 语法转换
- 通过 Polyfill 方式在目标环境中添加缺失的特性
- 源码转换 (要完成上面两个功能,代码肯定会被转换,不太清楚这里具体指的是什么。可能是指可以通过babel插件来方便的改变源码?暂时忽略)
怎么理解?
我们对ES2015+的JS新特性进行分类:
- 新语法
例:箭头函数、array spread、for of、async、class
- 新增对象和对象静态/实例方法
- 新对象
例:Promise、Generator、Map
- 新增对象实例/静态方法
例:array.includes、Object.assign
- 新对象
现在来翻译一下babel官网说明—babel能为你做的事情:
- 将新的语法转换成旧的语法
- 通过polyfill方式在目标环境中添加新的对象和对象方法
准备工作
1. 确认babel使用方式,babel的使用方式有三种:
- 引入@babel/core,自行写代码调用babel的API
- 安装@babel/cli,通过命令行的形式执行调用
- 使用打包工具(比如webpack、rollup)并安装相应插件来进行调用
为了更详细的了解、排除其他因素的干扰,我们采用第一种最原始的方式。
2.准备源码,包含以下ES2015+的新特性:
- 箭头函数
- array spread操作符
- async/await
- Array.prototype.includes
- Promise
// 项目index.js
const sleep = (seconds) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('ok');
}, seconds * 1000);
});
};
function getHighlightNodeIds(historyNodeIds, currentNodeIds) {
const nodeIds = [
...historyNodeIds,
...currentNodeIds,
];
// 去重
const result = [];
nodeIds.forEach((item) => {
if(!result.includes(item)){
result.push(item);
}
});
return result;
}
async function run() {
console.time('run');
await sleep(5);
const historyNodeIds = ['1', '2', '3', 'a'];
const currentNodeIds = ['a', 'b', 'c'];
const highlightNodeIds = getHighlightNodeIds(historyNodeIds, currentNodeIds);
console.log('highlightNodeIds: ', highlightNodeIds);
console.timeEnd('run');
}
run().then(() => {
console.log('执行完毕');
});
作用一:语法转换
// tranform/1.js
const path = require('path');
const fs = require('fs');
const babel = require('@babel/core');
// 1.获取源码
const filepath = path.resolve(__dirname, '../index.js');
const sourceCode = fs.readFileSync(filepath, 'utf8');
// 2. 执行编译(babel的转换函数有好几个,感兴趣的自行查看)
const result = babel.transformSync(sourceCode);
// 3. 将编译后的结果输出到文件
fs.writeFileSync('./dist/1.js', result.code);

编译前后除了缩进、换行这种无关紧要的变化外,其他就没了。 那我们应该如果转换语法?
方式一:手动添加语法转换插件
语法插件列表见Plugins List
// transform/2.js
// 引入依赖省略 同上
// 1.获取源码 同上
// 2. 执行编译
const result = babel.transformSync(sourceCode, {
sourceMaps: 'both', // 增加下sourceMap
plugins: [
'@babel/plugin-transform-arrow-functions',
'@babel/plugin-transform-spread',
'@babel/plugin-transform-async-to-generator'
]
});
// 3. 将编译后的结果输出到文件
fs.writeFileSync('./dist/2.js', result.code);
fs.writeFileSync('./dist/2.js.map', JSON.stringify(result.map, null, 4));
可以看到array spread、箭头函数、async函数都已经转换了,Promise、array.includes保持原样,符合预期。
这种方式的缺点很明显,开发者必须知道项目用了哪些语法,然后添加对应的插件。这样太琐碎且重复,不利于长期维护。所以babel还提供另外一种方式—preset-env(预设)
方式二:preset-env
@babel/preset-env让你可以使用最新的JS 提案(不低于stage 3)且不需要自己声明目标环境需要哪些插件。它的依赖中含有官方所有的plugins list
怎么告诉preset-env希望转换的语法?
使用
targets
配置项,声明编译后的代码需要运行的目标环境
// 其他同上
// 2. 执行编译
const result = babel.transformSync(sourceCode, {
presets: [
[
'@babel/preset-env',
{
targets: {'chrome': '53'}
}
]
]
});
// 3. 将编译后的结果输出到文件
fs.writeFileSync('./dist/3.js', result.code);
可以看到,此时只有async函数被编译了(使用到了generator函数),其他两个语法保持原样,因为目标环境已经可以支持,不需要转换了。
targets
的更多值请参考官网
作用二:对象及对象方法支持(polyfill)
新语法可以编译成旧语法,但是新增的对象和对象方法不同,这些需要环境支持。如果环境不支持,就需要自己来新增实现,当然实际开发我们基本会用工具来做,babel就可以。
方式一:全局修改
特点
- 已有对象的新方法 -> 通过修改原型链新增方法
- 新对象 -> 增加新的全局对象
验证
- 当前运行环境并不支持
at
方法 - 引入core-js
const customPromise = require("core-js/stable/promise"); require("core-js/stable/array/at"); /** * customPromise 和 Promise 全等 * => core-js/stable/promise 运行后增加了全局对象Promise */ console.log('customPromise === Promise:' , customPromise === Promise); /** * 引入at方法后,即可直接在数组实例上调用了 */ const array1 = [5, 12, 8, 130, 44]; const index = 2; console.log('at运行结果', array1.at(index));
依赖
注意:依赖中前三个是代码运行的依赖,最后一个是babel打包的依赖
- core-js(3.x):提供了全局新对象和方法和修改原型链
- regenerator-runtime:实现了generator对象及相关方法
- @babel/runtime:提供辅助函数
- @babel/plugin-transform-runtime:提取编译用到的辅助函数
可以看出已有对象的新方法core-js是通过修改原型来实现的。
使用
-
方式一:手动引入依赖 +
useBuiltIns: 'entry'
我们先引入依赖文件从 can i use中查到这些特性开始支持的chrome版本: promise 32,arrow 45,spread 46,includes 47,async 62(generator 39)
我们使用46,此时目标环境不支持includes和async。编译后的代码如下:
- includes方法使用保持原样
- generator和arrow function也被转换了,有点不符合预期,但这个疑问先放一下
源码中引入
core-js/stage
(内部引入了提案阶段的所有特性),编译后的代码这里保持原样。如此,编译代码就引入了目标环境已经支持的、源码中没有使用到的特性。这显然不适合,编译过程需要更多的处理。所幸babel已经提供了配置,我们使用就好。preset-env增加配置
useBuiltIns: 'entry'
(默认为false)增加此配置后,babel会根据targets值,全量引入那些目前环境不支持的特性。 但是这种方式还是会引入项目中不需要的特性,如果场景不需要,可以使用下一种方式。
-
方式二:
useBuiltIns: 'usage'
注意,此时不需要在源码中引入上面的core-js和regenerator-runtime了
-
其他:提取辅助函数 上面两种方式都可以看到babel(@babel/core)有引入辅助函数
_regneratorRuntime, asyncGeneratorStep
等。如果我们使用其他特性,比如class也会有别的辅助函数。如果一个项目要打包多个文件(结果是多文件,比如开发库的时候),不希望每个文件都包含辅助函数,因为这样会有重复代码,会增加文件体积。此时,我们就可以使用@babel/plugin-transform-runtime & @babel/runtime这两个依赖。
如果helpers参数设置为
false
,帮助函数会和之前一样以源码的形式包含在文件中。
方式二:局部修改
特点
- 已有对象的新方法 -> 增加方法
- 新对象 -> 增加新的局部对象
注:babel会,对象、方法的调用方式会发生改变
验证 运行环境和之前的验证相同
const customPromise = require("@babel/runtime-corejs3/core-js-stable/promise");
const at = require("@babel/runtime-corejs3/core-js/instance/at");
/**
* 1. customPromise的API和Promise相同
* 2. customPromise 和 Promise 不相等
*/
new customPromise((resolve) => {
setTimeout(() => {
resolve('ok');
console.log('promise resolved');
}, 100);
});
console.log('customPromise === Promise:' , customPromise === Promise);
/**
* array实例的at方法
* 1. at方法的调用方法和原生支持时的不一样
* 2. 引入at后,直接实例调用还是抛错,说明未做全局修改
*/
const array1 = [5, 12, 8, 130, 44];
const index = 2;
console.log('at返回值:' , at(array1).call(array1, index));
const array2 = [5, 12, 8, 130, 44];
array2.at(index);
依赖
- @babel/runtime-corejs3:提供非全局修改的特性(代码运行需要)
- @babel/plugin-transform-runtime:提取辅助函数(babel编译需要)
使用
方式:@babel/plugin-transform-runtime
增加配置corejs
如果此时把helpers改成false,辅助函数一样会变成源码
参考文档
转载自:https://juejin.cn/post/7136169010542379039