Appearance
带你玩转 babel 工具链(五)彻底理解@babel/helpers 与 @babel/runtime
一、前言
前面我们学习了babel插件
, 下面我们详细了解下比较黑盒的@babel/helpers
与 @babel/runtime
, 虽然很少用到这个库,但他在 babel 工具链中也扮演了重要角色!
往期回顾:
二、babel 的运行时 helper
什么叫babel的运行时helper
? 我们可以拆分成两个词
运行时
:顾名思义,就是代码运行的时候。helper
: 是工具的意思
再结合 babel 的环境,合并起来的意思就是:babel 通过 AST 操作,为我们的代码中插入了helper
工具函数。
那么在产物代码中,执行的时候就会用到这些工具函数,也就是运行时
。
好的,了解了运行时 helper 的概念,我们继续看~
在日常项目中,我们通常能看到这样的产物, 我们使用了async
语法,babel 自动为我们的代码中注入了_asyncToGenerator
和asyncGeneratorStep
方法。
.babelrc
配置如下
js
{
"presets": [
"@babel/preset-env"
]
}
{
"presets": [
"@babel/preset-env"
]
}
如果我们不想要这些helper
, 我们就不配置@babel/preset-env
, 再重新打包
js
{ "presets": [] }
{ "presets": [] }
可以看到产物中已经没有了helper
, 没有发生任何变化
那如果我们就是想要注入 helper, 但不想引入@babel/preset-env
该如何实现呢?
我们来写一个babel插件
js
{
"presets": [
],
"plugins": [
["./src/plugins/index.js", { // 此处配置插件
"a": 1
}]
]
}
{
"presets": [
],
"plugins": [
["./src/plugins/index.js", { // 此处配置插件
"a": 1
}]
]
}
js
module.exports = function (api, options, dirname) {
return {
pre(file) {
this.addHelper("asyncToGenerator"); // 关键代码
},
visitor: {},
};
};
module.exports = function (api, options, dirname) {
return {
pre(file) {
this.addHelper("asyncToGenerator"); // 关键代码
},
visitor: {},
};
};
我们在pre
函数中,调用addHelper
添加了一个helper
, 再重新打包看看~
可以看到,babel 帮我们自动注入了asyncGeneratorStep
和_asyncToGenerator
, 虽然 babel 没有帮我们把fn函数
进行转换, 但是也实现了运行时 helper 的注入。
所以,问题又来,helpers
的方法在哪里?
@babel/helpers
我们可以找到@babel/helpers
下的node_modules/@babel/helpers/lib/helpers.js
, 可以发现我们的helper
都是通过字符串形式创建的(借助@babel/template
可以把字符串代码转成 AST)
感兴趣的话,大家可以打开这个文件,里面有很多helper
的实现。
@babel/helpers 的缺点
@babel/helpers 也是有些缺点的,通过前面的演示,可以发现,helper 是直接写在我们代码里的,我们先看个例子
有两个文件,都是用了async
语法,按道理使用一份helper
就可以了,但是最后却产生了两份!
js
// index.js
import test from "./test";
async function fn() {
await test();
}
// test.js
export default async function () {
return 1;
}
// index.js
import test from "./test";
async function fn() {
await test();
}
// test.js
export default async function () {
return 1;
}
那么怎么能解决上述问题呢?其实需要另外两个包来解决这种问题
- @babel/plugin-transform-runtime
- @babel/runtime
下面我将详细介绍@babel/runtime
, 帮助大家了解这个黑盒。至于@babel/plugin-transform-runtime
后续将用单独的文章讲解。
三、@babel/runtime 到底是什么?
首先我会在代码中切换两种配置
js
// @babel/helpers
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3
}]
],
"plugins": [
]
}
// @babel/runtime
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3
}]
],
"plugins": [
"@babel/plugin-transform-runtime"
]
}
// @babel/helpers
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3
}]
],
"plugins": [
]
}
// @babel/runtime
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3
}]
],
"plugins": [
"@babel/plugin-transform-runtime"
]
}
注意:只有配置
@babel/plugin-transform-runtime
, 才会使用@babel/runtime
。
我们对比一下@babel/helpers
和@babel/runtime
代码对比:
产物对比:
总结一下有什么区别:
- 代码实现方面:
@babel/runtime
采用了模块化,每个方法单独导出,而@babel/helpers
采用单独的文件导出。 - 产物方面:
@babel/runtime
的 helper 是模块化加载的,不会造成代码冗余。@babel/helpers
是直接把代码注入到了项目代码里,会造成冗余。
但是,是不是以后项目都使用@babel/runtime
+ @babel/plugin-transform-runtime
这种组合呢? 其实是不推荐的,为什么呢?我们接着看。
我们先了解一个知识点,@babel/plugin-transform-runtime
它提供了什么功能:
关于
@babel/plugin-transform-runtime
的详细讲解,请参考我的另一篇文章:编写中~
helper
使用模块化加载polyfill
不污染全局,通过导出变量的形式引入,而不是直接覆盖全局方法的实现
由于@babel/plugin-transform-runtime
并不支持targers
配置。也就是说,所有的比较新的语言特性,都会被polyfill
, 明明浏览器已经支持的功能,却还被polyfill
,这显然是不合理的。所以我更推荐在开发lib
库的时候使用@babel/plugin-transform-runtime
。
那造成这种问题的原因是什么呢,我不是已经配置了@babel/preset-env
的targets
吗?
再来回顾下plugins
和presets
的执行顺序:
可以发现@babel/plugin-transform-runtime
是优先执行的, 如果代码已经被转换过了,到达@babel/preset-env
处理的时候,就没有效果了。
四、helper 是如何注入的
在前面,我们提到过,使用addHelper
可以插入一段运行时Helper
js
module.exports = function (api, options, dirname) {
return {
pre(file) {
this.addHelper("asyncToGenerator"); // 关键代码
},
visitor: {},
};
};
module.exports = function (api, options, dirname) {
return {
pre(file) {
this.addHelper("asyncToGenerator"); // 关键代码
},
visitor: {},
};
};
下面我们来具体分析:
@babel/helper 的注入逻辑
首先我们定位到一段源码node_modules/@babel/core/lib/transformation/file/file.js
js
addHelper(name) {
const declar = this.declarations[name];
if (declar) return cloneNode(declar);
const generator = this.get("helperGenerator");
// transform-runtime 在此处拦截 实现了自己的generator
if (generator) {
const res = generator(name);
if (res) return res;
}
// 加载helper对应的
helpers().ensure(name, File);
const uid = this.declarations[name] = this.scope.generateUidIdentifier(name);
const dependencies = {};
for (const dep of helpers().getDependencies(name)) {
dependencies[dep] = this.addHelper(dep);
}
const {
nodes,
globals
} = helpers().get(name, dep => dependencies[dep], uid, Object.keys(this.scope.getAllBindings()));
globals.forEach(name => {
if (this.path.scope.hasBinding(name, true)) {
this.path.scope.rename(name);
}
});
nodes.forEach(node => {
node._compact = true;
});
this.path.unshiftContainer("body", nodes);
this.path.get("body").forEach(path => {
if (nodes.indexOf(path.node) === -1) return;
if (path.isVariableDeclaration()) this.scope.registerDeclaration(path);
});
return uid;
}
addHelper(name) {
const declar = this.declarations[name];
if (declar) return cloneNode(declar);
const generator = this.get("helperGenerator");
// transform-runtime 在此处拦截 实现了自己的generator
if (generator) {
const res = generator(name);
if (res) return res;
}
// 加载helper对应的
helpers().ensure(name, File);
const uid = this.declarations[name] = this.scope.generateUidIdentifier(name);
const dependencies = {};
for (const dep of helpers().getDependencies(name)) {
dependencies[dep] = this.addHelper(dep);
}
const {
nodes,
globals
} = helpers().get(name, dep => dependencies[dep], uid, Object.keys(this.scope.getAllBindings()));
globals.forEach(name => {
if (this.path.scope.hasBinding(name, true)) {
this.path.scope.rename(name);
}
});
nodes.forEach(node => {
node._compact = true;
});
this.path.unshiftContainer("body", nodes);
this.path.get("body").forEach(path => {
if (nodes.indexOf(path.node) === -1) return;
if (path.isVariableDeclaration()) this.scope.registerDeclaration(path);
});
return uid;
}
其中有几段逻辑
获取 helperGenerator 方法,如果存在就调用该方法,不会走后面逻辑,这也是
@babel/plugin-transform-runtime
实现helper
的地方,替换原有逻辑jsconst declar = this.declarations[name]; if (declar) return cloneNode(declar); const generator = this.get("helperGenerator"); // transform-runtime 在此处拦截 实现了自己的generator if (generator) { const res = generator(name); if (res) return res; }
const declar = this.declarations[name]; if (declar) return cloneNode(declar); const generator = this.get("helperGenerator"); // transform-runtime 在此处拦截 实现了自己的generator if (generator) { const res = generator(name); if (res) return res; }
这一段代码,首先加载了
helper
, 创建了对应的 ast nodes, 后面主要是处理helper
依赖其他helper
的情况,如果存在,就调用addHelper
动态添加。并且如果你的代码里有和helper
一样的方法名,也会进行重命名。jshelpers().ensure(name, File); const uid = (this.declarations[name] = this.scope.generateUidIdentifier(name)); const dependencies = {}; for (const dep of helpers().getDependencies(name)) { dependencies[dep] = this.addHelper(dep); } const { nodes, globals } = helpers().get( name, (dep) => dependencies[dep], uid, Object.keys(this.scope.getAllBindings()) ); globals.forEach((name) => { if (this.path.scope.hasBinding(name, true)) { this.path.scope.rename(name); } }); nodes.forEach((node) => { node._compact = true; });
helpers().ensure(name, File); const uid = (this.declarations[name] = this.scope.generateUidIdentifier(name)); const dependencies = {}; for (const dep of helpers().getDependencies(name)) { dependencies[dep] = this.addHelper(dep); } const { nodes, globals } = helpers().get( name, (dep) => dependencies[dep], uid, Object.keys(this.scope.getAllBindings()) ); globals.forEach((name) => { if (this.path.scope.hasBinding(name, true)) { this.path.scope.rename(name); } }); nodes.forEach((node) => { node._compact = true; });
最后这一段, 其实就是把
nodes
, 插入到你的代码中~jsthis.path.unshiftContainer("body", nodes); this.path.get("body").forEach((path) => { if (nodes.indexOf(path.node) === -1) return; if (path.isVariableDeclaration()) this.scope.registerDeclaration(path); });
this.path.unshiftContainer("body", nodes); this.path.get("body").forEach((path) => { if (nodes.indexOf(path.node) === -1) return; if (path.isVariableDeclaration()) this.scope.registerDeclaration(path); });
@babel/runtime 的注入逻辑
在上面我们提到了,helperGenerator
方法, transform-runtime
就是基于此实现的。
js
const generator = this.get("helperGenerator");
// transform-runtime 在此处拦截 实现了自己的generator
if (generator) {
const res = generator(name);
if (res) return res;
}
const generator = this.get("helperGenerator");
// transform-runtime 在此处拦截 实现了自己的generator
if (generator) {
const res = generator(name);
if (res) return res;
}
那这个方法从哪里来的呢?
我们一块定位到如下代码node_modules/@babel/plugin-transform-runtime/lib/index.js
js
pre(file) {
if (!useRuntimeHelpers) return;
file.set("helperGenerator", name => {
if (file.availableHelper && !file.availableHelper(name, runtimeVersion)) {
return;
}
const isInteropHelper = HEADER_HELPERS.indexOf(name) !== -1;
const blockHoist = isInteropHelper && !(0, _helperModuleImports.isModule)(file.path) ? 4 : undefined;
const helpersDir = esModules && file.path.node.sourceType === "module" ? "helpers/esm" : "helpers";
let helperPath = `${modulePath}/${helpersDir}/${name}`;
if (absoluteRuntime) helperPath = (0, _getRuntimePath.resolveFSPath)(helperPath);
return addDefaultImport(helperPath, name, blockHoist, true);
});
const cache = new Map();
function addDefaultImport(source, nameHint, blockHoist, isHelper = false) {
const cacheKey = (0, _helperModuleImports.isModule)(file.path);
const key = `${source}:${nameHint}:${cacheKey || ""}`;
let cached = cache.get(key);
if (cached) {
cached = _core.types.cloneNode(cached);
} else {
// 使用 @babel/helper-module-imports 创建一个导入
cached = (0, _helperModuleImports.addDefault)(file.path, source, {
importedInterop: isHelper && supportsCJSDefault ? "compiled" : "uncompiled",
nameHint,
blockHoist
});
cache.set(key, cached);
}
return cached;
}
}
};
pre(file) {
if (!useRuntimeHelpers) return;
file.set("helperGenerator", name => {
if (file.availableHelper && !file.availableHelper(name, runtimeVersion)) {
return;
}
const isInteropHelper = HEADER_HELPERS.indexOf(name) !== -1;
const blockHoist = isInteropHelper && !(0, _helperModuleImports.isModule)(file.path) ? 4 : undefined;
const helpersDir = esModules && file.path.node.sourceType === "module" ? "helpers/esm" : "helpers";
let helperPath = `${modulePath}/${helpersDir}/${name}`;
if (absoluteRuntime) helperPath = (0, _getRuntimePath.resolveFSPath)(helperPath);
return addDefaultImport(helperPath, name, blockHoist, true);
});
const cache = new Map();
function addDefaultImport(source, nameHint, blockHoist, isHelper = false) {
const cacheKey = (0, _helperModuleImports.isModule)(file.path);
const key = `${source}:${nameHint}:${cacheKey || ""}`;
let cached = cache.get(key);
if (cached) {
cached = _core.types.cloneNode(cached);
} else {
// 使用 @babel/helper-module-imports 创建一个导入
cached = (0, _helperModuleImports.addDefault)(file.path, source, {
importedInterop: isHelper && supportsCJSDefault ? "compiled" : "uncompiled",
nameHint,
blockHoist
});
cache.set(key, cached);
}
return cached;
}
}
};
其中关键点在这段代码
它为我们拼接了最终 helper 对应的路径,例如@babel/runtime/helpers/asyncToGenerator
最后再调用addDefaultImport
, 添加 import 代码,在我们的代码里
好的以上就是helper
注入的逻辑,后面我将分析@babel/preset-env
的内容,大家敬请期待。
五、总结
在一开始,我们一块了解了@babel/helper
的具体作用,以及一些缺点。
然后我们又了解了@babel/runtime
到底是干啥的,其实就是模块化导入的helper
。
最后我们了解了helper
的注入逻辑。
当然,最后还有一点悬念@babel/plugin-transform-runtime
真正的原理是什么。后面文章将逐步分析。