缘起
@babel/register是比较常见的一种babel处理方式,仅需一行代码即可实现即时编译。
有一次因为错误地发布了某个使用了 @babel/register 的 package ,但又不想再修改版本号,于是自然地使用了’npm unpublish’ + 'npm publish’的方式做了重复发布。
奇怪的是,在重新npm install
之后,node_modules
目录下该 package 的代码已经更新,但实际运行时似乎并没有生效。😑
这种玄学问题一时间似乎并没有办法找到一个合理的解释,网上搜寻一番亦无果,只好从源码中找端倪了。
精致的小玩意儿
私以为 @babel/register 这种高端工具应该是个很庞大的工程,其实不然,它真的非常的小巧!打开项目地址,项目核心就node.js
、cache.js
两个代码文件,一共300行不到的代码量。实在是太酷了!
入口
在开始真正的源码分析之前,先来看看用户是怎么使用的:
1 | require("@babel/register"); |
用户的代码加载了@babel/register
的入口文件index.js
:
1 | exports = module.exports = function (...args) { |
入口文件index.js
加载了node.js
。
node.js
node.js
是整个项目核心中的核心,@babel/register
的处理过程可以拆解成三个步骤:hook
、compile
、cache
,而这三大步骤的控制中枢就是node.js
!
入口文件index.js
加载node.js
时,会执行node.js
中的静态代码:
1 | register(); |
register()
做了什么呢?接着往下看:
1 | export default function register(opts?: Object = {}) { |
为了突出重点,上述代码省略了源代码中的一些处理逻辑,直接通过注释说明这部分功能。
可以看到register()
的两个关键代码块hookExtensions
及registerCache
hookExtensions
顾名思义,hookExtensions
当然是用来处理hook
流程了,compile
的调用应该也是在hook时做了相应的声明的:
1 | import { addHook } from "pirates"; |
这里引出了一个周下载量近千万,但却鲜为人知的功能库: pirates,在 Github 上甚至只有可怜的192颗星。事实上,就是这个看似其貌不扬的库,实现了require
的hook
~
根据API介绍,上述addHook
的作用是: 对于符合exts
后缀的文件(包括node_modules
目录下的),当调用require
时,使用compileHook
替代其行为,compileHook
接收一个形如(code, filename)
的参数列表,返回filename
对应的代码。此例中,exts
为[’.js’,’.jsx’,’.es6’,’.es’,’.mjs’]
如此一来,便完成了hook
步骤。
现在,我们再来看看compile
:
1 | function compile(code, filename) { |
本质上,compile
还是调用babel.transform
来处理。但需要注意的是,处于性能的考量,并不是所有情况都会去compile
,在缓存开启的情况下,会优先使用缓存数据。
compile
方法中的缓存仅仅是整个缓存机制的一部分,即在编译时期根据编译配置生成缓存键值并配置到缓存数组中,说白了就是内存缓存。
实际上,这个缓存是持久化的。
registerCache
我们回到register()
,当缓存开关开启时,@babel/register
通过registerCache
对象读取持久化缓存信息,并初始化内存缓存:
1 | import * as registerCache from "./cache"; |
这就引出了第三个步骤: cache
cache.js
cache.js
负责处理持久化缓存,我们先来看看上面提到的两个方法:
1 | export function load() { |
load()
方法将FILENAME
文件中的信息读取并解析到内存,通过get()
提供给外部。在进程退出时,调用save()
方法做持久化。
1 | export function save() { |
save()
很简单,就是load()
的一个逆操作: 字符串化内存缓存,存储于FILENAME
。
我们来看看FILENAME
是什么:
1 | const DEFAULT_CACHE_DIR = |
FILENAME
的具体值取决于版本信息与环境变量
至此,@babel/register
的神秘面纱终于揭开。
回归
让我们重新审视开篇的那个玄学问题: 为什么node_modules
目录下该 package 的代码已经更新,但实际运行时似乎并没有生效呢?
这是由于使用了旧的缓存信息,@babel/register
在运行时并不会再去动态地编译相关文件。
找到了原因,再思考解决方法自然不成问题,有两种策略:
- 从代码层级处理,使用
@babel/register
时传入配置cache: false
,禁用缓存 - 从系统层级处理,清除缓存文件或变更项目路径使缓存失效