2022-10-7 About 18 min

本章介绍 Babel 生态体系相关的各种工具,这些工具在 Babel 内部和外部均可使用,了解这些工具和其背后的原理,对理解 Babel 非常有用。

和 Babel 相关的工具包括:

本章中涉及 Babel 转译部分,可以理解为下述代码:

import * as babel from "@babel/core";
const newCode = babel.transform(code, options)
1
2

# 5.1 commander.js

Babel 对外提供了几个命令行工具,babel-cli/babel-node 均使用了 commander.js 提供命令行服务。在如今的前端领域,commander.js 是命令行开发的重要基础工具了。这里先介绍 commander.js 的使用和原理,便于后续章节的理解。

# 5.2 api 服务: babel-core

TODO

# 5.3 命令行: babel-cli

babel-cli 本身的用法本书不再赘述,官方文档讲解的非常详细。

babel-cli 是基于 commander.js 的命令行,了解 commander.js 的使用之后,babel-cli 的原理也比较容易理解了。

babel-cli 的运行过程并不复杂,运行流程图如下:

Babel工具集/babel-cli

以下是其源码中的部分细节:

# 5.4 node 增强:babel-register

# 5.4.1 babel-register 用法

babel-register 模块会改写 node 的 require 方法,为其加上一个钩子,当 require 加载 .js.jsx.es6.es.mjs 后缀的时候,会先用 Babel 转码。

后缀可配置
.js.jsx.es6.es.mjs为默认配置

举例:

index.js

require('babel-register');
require('./run');
1
2

run.js

import fs from 'fs';
console.log(fs);
1
2

执行 node index 时,run.js 就不需要被转码了。

有两点需要注意:

# 5.4.2 require 加载机制

(TODO:附录链接 https://github.com/nodejs/node-v0.x-archive/blob/master/lib/module.js)

理解 node 中 require 的加载机制,有助于理解 babel-register 是如何为 require 添加钩子的。

require 的源码定义在 node 源码 lib/module.jsModule.prototype.require 方法。

在此实现一个简单版本的 require 方法,它具有如下能力:

以下是简要实现,辅以注释:

// require 位于 Module 类的原型
function Module(id = '') {
	this.id = id; // id 就是 require 路径
	this.path = path.dirname(id);
	this.exports = {};
	this.filename = null;
	this.loaded = false;
}

Module.prototype.require = function(request) {
	return Module._load(request);
};

// Module._load 有如下功能
// 1. 优先返回缓存
// 2. 如果没有缓存,就新建一个 Module 实例 module,用该实例加载目标模块,并返回目标模块的 module.exports
Module._load = function(request) {
	// 根据 request 获取目标文件的绝对路径
    // filename 是绝对路径
	const filename = Module._resolveFilename(request); // 下文有该方法的定义

	const cachedModule = Module._cache[filename];

	// 优先返回缓存
	if (cachedModule !== undefined) {
		return cachedModule.exports;
	}

	// 没有缓存,为目标文件新建一个 Module 实例
	const module = new Module(filename);

	// 写入缓存
    // 这样写有一个好处: 可以处理循环引用
    // a -> b -> a 的情况下,a require b,b 会进入缓存,然后 b require a,a require b 时,拿到的是缓存里的 b
	Module._cache[filename] = module;

	module.load(filename);

	return module.exports;
};

// 解析出绝对路径
// request: 是用户传入的名称,如 'path', '../xx' 等,不一定是绝对路径
Module._resolveFilename = function(request) {
    // parent 是父模块描述对象,其 parent.paths 是该父级模块的 path 查找集合
    // Module._resolveLookupPaths 会返回 request 的 path 查找集合
    const [ id, paths ] = Module._resolveLookupPaths(request, parent);

    // 最终遍历 paths 找到目标 filename,它是绝对路径
    const filename = Module._findPath(request, paths);

	return filename;
};
Module.prototype.load = function (filename) {
    // 获取文件后缀名
    const extname = path.extname(filename);

    // 调用后缀名对应的处理函数来处理
    Module._extensions[extname](this, filename);

    this.loaded = true;
};

// 针对不同的文件后缀,执行不同的解析方法
Module._extensions = {
	'.js': function(module, filename) {
		// 先读文件
		const content = fs.readFileSync(filename, 'utf8');
		// 然后运行文件代码
		module._compile(content, filename);
	},
	'.json': function(module, filename) {
		// 先读文件
		const content = fs.readFileSync(filename, 'utf8');
		// 然后运行文件代码
		module.exports = JSONParse(content);
	},
};


// 该方法会将目标文件 filename 的代码在最外层包裹 exports, require, module, __dirname, __filename
Module.prototype._compile = function(content, filename) {
	const wrapper = `(function (exports, require, module, __filename, __dirname) { 
		${script}	
	});`

	// vm 是 nodejs 的虚拟机沙盒模块,runInThisContext 方法可以接受一个字符串并将它转化为一个函数
  	// 返回值就是转化后的函数,所以 compiledWrapper 是一个函数
	const compiledWrapper = vm.runInThisContext(wrapper, {
		filename,
		lineOffset: 0,
		displayErrors: true,
	});

	const dirname = path.dirname(filename);
	compiledWrapper.call(this.exports, this.exports, this.require, this, filename, dirname);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

总结而言,require 的执行过程如下:

# 5.4.3 为 require 添加钩子

# 5.4.4 babel-register 内部实现

# 5.5 命令行:babel-node

babel-node 也是基于 commander.js 的命令行服务。下图是 babel-node 的运行过程:

Babel工具集/babel-node

由上图可以看出,babel-node 的运行过程比较直观。解析其源码,也有一些值得注意的细节:

# 5.6 浏览器支持:babel-standalone

babel-standalone 提供浏览器环境实时转译 JavaScript 的能力。

Babel工具集/babel-standalone

# 5.7 babel-highlight

babel-highlight 可以将一段代码字符串在终端高亮展示。

代码中已经可以看到 Babel8 的处理方式了。无论是 Babel7 还是 Babel8 的处理方式,总体思路都是:将代码块解析为不同类型的 token 单元,并对不同类型显示不同的颜色。

# 5.8 babel-template

TODO

# 5.9 webpack 支持:babel-loader

babel-loader 非 Babel 官方出品,也是 webpack 生态中的重要基础工具,其承担了对 webpack 打包过程中相关文件的转译工作。

下图是 babel-loader 的大致运行过程:

Babel工具集/babel-loader-总览