带你玩转babel工具链(五)彻底理解@babel/helpers 与 @babel/runtime

2,736 阅读6分钟

一、前言

前面我们学习了babel插件, 下面我们详细了解下比较黑盒的@babel/helpers@babel/runtime, 虽然很少用到这个库,但他在babel工具链中也扮演了重要角色!

往期回顾:

二、babel的运行时helper

什么叫babel的运行时helper? 我们可以拆分成两个词

  • 运行时:顾名思义,就是代码运行的时候。
  • helper: 是工具的意思

再结合babel的环境,合并起来的意思就是:babel通过AST操作,为我们的代码中插入了helper工具函数

那么在产物代码中,执行的时候就会用到这些工具函数,也就是运行时

好的,了解了运行时helper的概念,我们继续看~

在日常项目中,我们通常能看到这样的产物, 我们使用了async语法,babel自动为我们的代码中注入了_asyncToGeneratorasyncGeneratorStep方法。

image.png

.babelrc配置如下

{
  "presets": [
    "@babel/preset-env"
  ]
}

如果我们不想要这些helper, 我们就不配置@babel/preset-env, 再重新打包

{ "presets": [] }

image.png

可以看到产物中已经没有了helper, 没有发生任何变化

那如果我们就是想要注入helper, 但不想引入@babel/preset-env该如何实现呢?

我们来写一个babel插件

{
  "presets": [
  ],
  "plugins": [
    ["./src/plugins/index.js", { // 此处配置插件
      "a": 1
    }]
  ]
}
module.exports = function(api, options, dirname) {
  return {
    pre(file) {
      this.addHelper('asyncToGenerator') // 关键代码
    },
    visitor: {
    }
  };
};

我们在pre函数中,调用addHelper添加了一个helper, 再重新打包看看~

image.png

可以看到,babel帮我们自动注入了asyncGeneratorStep_asyncToGenerator, 虽然babel没有帮我们把fn函数进行转换, 但是也实现了运行时helper的注入。

所以,问题又来,helpers的方法在哪里?

@babel/helpers

我们可以找到@babel/helpers下的node_modules/@babel/helpers/lib/helpers.js, 可以发现我们的helper都是通过字符串形式创建的(借助@babel/template可以把字符串代码转成AST)

image.png

感兴趣的话,大家可以打开这个文件,里面有很多helper的实现。

@babel/helpers的缺点

@babel/helpers也是有些缺点的,通过前面的演示,可以发现,helper是直接写在我们代码里的,我们先看个例子

有两个文件,都是用了async语法,按道理使用一份helper就可以了,但是最后却产生了两份!

// index.js
import test from './test'

async function fn() {
  await test()
}


// test.js
export default async function() {
  return 1
}

image.png

image.png

那么怎么能解决上述问题呢?其实需要另外两个包来解决这种问题

  • @babel/plugin-transform-runtime
  • @babel/runtime

下面我将详细介绍@babel/runtime, 帮助大家了解这个黑盒。至于@babel/plugin-transform-runtime 后续将用单独的文章讲解。

三、@babel/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

代码对比:

image.png

产物对比:

未命名文件.jpg

总结一下有什么区别:

  • 代码实现方面:@babel/runtime采用了模块化,每个方法单独导出,而@babel/helpers采用单独的文件导出。
  • 产物方面:@babel/runtime的helper是模块化加载的,不会造成代码冗余@babel/helpers是直接把代码注入到了项目代码里,会造成冗余。

但是,是不是以后项目都使用@babel/runtime + @babel/plugin-transform-runtime这种组合呢? 其实是不推荐的,为什么呢?我们接着看。

我们先了解一个知识点,@babel/plugin-transform-runtime它提供了什么功能:

关于@babel/plugin-transform-runtime的详细讲解,请参考我的另一篇文章:编写中~

  1. helper使用模块化加载
  2. polyfill 不污染全局,通过导出变量的形式引入,而不是直接覆盖全局方法的实现

由于@babel/plugin-transform-runtime并不支持targers配置。也就是说,所有的比较新的语言特性,都会被polyfill, 明明浏览器已经支持的功能,却还被polyfill,这显然是不合理的。所以我更推荐在开发lib库的时候使用@babel/plugin-transform-runtime

那造成这种问题的原因是什么呢,我不是已经配置了@babel/preset-envtargets吗?

再来回顾下pluginspresets的执行顺序:

image.png

可以发现@babel/plugin-transform-runtime优先执行的, 如果代码已经被转换过了,到达@babel/preset-env处理的时候,就没有效果了。

四、helper是如何注入的

在前面,我们提到过,使用addHelper可以插入一段运行时Helper

module.exports = function(api, options, dirname) {
  return {
    pre(file) {
      this.addHelper('asyncToGenerator') // 关键代码
    },
    visitor: {
    }
  };
};

下面我们来具体分析:

@babel/helper的注入逻辑

首先我们定位到一段源码node_modules/@babel/core/lib/transformation/file/file.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;
  }

其中有几段逻辑

  • 获取helperGenerator方法,如果存在就调用该方法,不会走后面逻辑,这也是@babel/plugin-transform-runtime实现helper的地方,替换原有逻辑

    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一样的方法名,也会进行重命名。

    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, 插入到你的代码中~

      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就是基于此实现的。

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

    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;
      }
    }

  };

其中关键点在这段代码

image.png

它为我们拼接了最终helper对应的路径,例如@babel/runtime/helpers/asyncToGenerator

最后再调用addDefaultImport, 添加import代码,在我们的代码里

好的以上就是helper注入的逻辑,后面我将分析@babel/preset-env的内容,大家敬请期待。

五、总结

在一开始,我们一块了解了@babel/helper的具体作用,以及一些缺点。

然后我们又了解了@babel/runtime到底是干啥的,其实就是模块化导入的helper

最后我们了解了helper的注入逻辑。

当然,最后还有一点悬念@babel/plugin-transform-runtime真正的原理是什么。后面文章将逐步分析。