2022-10-7 About 18 min

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

和 Babel 相关的工具包括:

  • 解析工具: babel-parser
  • api 服务: babel-core
  • 命令行: babel-cli
  • node 增强: babel-register
  • 命令行: babel-node
  • 浏览器支持: babel-standalone
  • 代码生成器: babel-generator
  • 代码高亮: babel-highlight
  • 模板引擎: babel-template
  • webpack 支持: babel-loader(社区提供)

本章中涉及 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 的使用和原理,便于后续章节的理解。

  • commander.js 版本

    本书中 babel-cli 使用的 commander.js 的版本是^4.0.1,截至写稿时,commander.js 的最新版本是7.0.0。

    为了理解最新的设计理念,本书只介绍 commander.js 7.0.0版本的使用和实现,相信了解该版本实现后,^4.0.1 版本的理解也就顺理成章了。

  • commander.js 的使用

    commander.js 在 Github 的 Readme 介绍的比较详细,这里无意做文档翻译机,在此对 commander.js 的功能特点做一些归纳整理。

    先准备 index.js 文件以便讲解,代码中涵盖了一部分使用特点。

    后续的案例中我们使用 node index * 启动执行该文件。

    #!/usr/bin/env node
    
    // index.js
    const { Command } = require('commander');
    const program = new Command();
    
    // 链式调用
    program
        /******************** 版本配置区域 **********************/
        .version('0.0.1', '-v, --version') // 版本信息
    
        /******************** 选项区域 **********************/
        // boolean 类型的选项
        .option('-d, --debug', 'output extra debugging')
    
        // 带参数类型的选项: <type> 必填
        .option('-c, --cheese <type>' , 'cheese type', 'blue') // (选项定义, 描述, 默认值)
    
        // 带参数类型的选项: [type] 选填;
        //      type 有值时,banana 选项为字符串类型
        //      type 无值时,banana 选项为 boolean 类型
        .option('-b, --banana [type]' , 'banana type') // (选项定义, 描述)
    
        // 自定义参数类型的选项
        .option('-i, --integer <number>', 'integer argument', function(number) {
            return parseInt(number);
        })
    
        /******************** 解析命令 **********************/
        .parse();
    
    // 打印options
    console.log(program.opts());
    
    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
    • 链式调用

      commander.js 暴露的方法会返回 cmd 实例或子命令实例对象,以支持链式调用,该做法还是很常见的。

    • 版本配置

      执行 node index.js -v 终端会显示 0.0.1

    • 多样的选项配置

      • boolean 类型的选项

        program.option('-d, --debug', 'output extra debugging')
        
        1

        扩展:"取反 boolean"类型

        program.option('--no-debug', 'not output extra debugging')
        
        1
      • 非 boolean 类型的选项

        program.option('-c, --cheese <type>' , 'cheese type', 'blue') // (选项定义, 描述, 默认值)
        
        1

        参数可设置默认值。

      • 既可是 boolean 也可非 boolean 类型的选项

        program.option('-b, --banana [type]' , 'banana type') // (选项定义, 描述)
        
        1

        type 无值时,banana 选项的值为 boolean 类型。

        type 有值时,banana 选项的值为非 boolean 类型。

      • 不定数量的选项

        program
            .option('-n, --number <numbers...>', 'specify numbers')
            .option('-l, --letter [letters...]', 'specify letters');
        
        1
        2
        3
  • commander.js 原理解析

    • 概览

      commander.js 7.0.0 版本的核心代码就一个文件 index.js,2200 多行代码,代码的注释比较丰富,代码可读性也是不错的,感兴趣的同学可以通读一下。

      commander.js 包含以下类:

      • Option
      • Help
      • Command
      • CommandError
      • InvalidOptionArgumentError

      如下图:

      Babel概览/commander-概览

    • Option

      Option 类的实例存储选项的各类信息:

      • 选项是否必填(requred)
      • 描述信息(description)
      • 可变参数(variadic)
      • 是否反向 boolean,也就是 --no-xx类型的选项
      • 长名称/短名称,也就是 -c, --cheese
      • 参数的值
      • 其他

      Babel概览/Commander-Option 类

      命令行实例执行 program.option("-c, --cheese", "add cheese"),会创建一个新的 Option 实例,存储该选项的各类信息。

      Option 类内部通过各种正则和字符串的计算各类信息。

      比如,Option 类接受参数的长短名称有三种写法:

      • "-c, --cheese"
      • "-c|--cheese"
      • "-c --cheese"

      其解析参数的时候以正则/[ |,]+/对字符串做分隔计算: flags.split(/[ |,]+/)

    • Help

      Help 类主要负责帮助信息的展示、配置等工作。

      比如执行命令行 node index.js -h 会打印出:

      Usage: index [options]
      
      Options:
          -v, --version           output the version number
          -d, --debug             output extra debugging
          -c, --cheese <type>     cheese type (default: "blue")
          -b, --banana [type]     banana type
          -i, --integer <number>  integer argument
          -h, --help              display help for command
      
      1
      2
      3
      4
      5
      6
      7
      8
      9

      Help 类中相对值得说的是 formatHelp 方法了。

      formatHelp 接收当前命令行和 help 实例对象,返回格式化后的帮助信息。

      在生成帮助信息的过程中,有几个小的编程技巧可以借鉴:

      • 日志信息字符串,通过数组形式组织,最终拼接为字符串

        该方法对比直接拼接字符串,代码可维护性更强。

      • 利用 String.prototype.padEnd 方法实现字符串补全

        日常工作中用到该方法的场景可能不多,容易遗漏。

        比如:'hello'.padEnd(7, '~') 的结果是 hello~~

        formatHelp 里用空格补全,用于字符串显示的格式化。

      • 根据终端宽度动态计算显示宽度

        TODO

    • Command

      Command 类是 commander.js 的核心类,提供了命令行的各类方法。

      下图是 Command 类使用时的主要流程:

      Babel概览/Commander-Command类

      我们简要介绍下其中的一些点:

      • version(str, flags, description)

        该方法注册了命令的版本信息,利用 createOption() 实现的一个快捷方法。

      • command(nameAndArgs, actionOptsOrExecDesc, execOpts)

        该方法注册子命令,有两种模式:

        • 绑定函数实现命令

          program
              .command('start')
              .action(function() {
                  console.log('actor');
              });
          
          1
          2
          3
          4
          5

          执行 node index start的时候,会执行 action 注册的回调,打印 actor

        • 启动独立文件执行命令

          program.command('start', 'start runner');
          
          1

          执行 node index start 的时候,会启动 index-start.js 文件。

        该方法内部通过是否含有描述信息判断是哪种模式。

      • 重复注册命令时,会使用第一个注册的命令

        比如:

        program
                .command('start')
                .action(function() {
                    console.log('start 1');
                });
        program
                .command('start')
                .action(function() {
                    console.log('start 2');
                });
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10

        在执行 node index start 的时候,只会打印 start 1,因为内部找到匹配的命令的代码是:

        this.commands.find(cmd => cmd._name === name || cmd._aliases.includes(name));
        
        1

        Array.prototype.find 方法会返回数组第一个匹配的元素。

      • EventEmitterCommand 类中的使用

        Node 内置模块 EventEmitter 提供了事件机制,最常见的 API 是 on/emit

        Command 类中几处利用事件机制的地方举例:

        • 注册选项参数时,会注册 option:${optionName} 事件(on(option:${optionName})),在命令行执行时触发回调(emit(option:${optionName}))。
        • 执行命令时,如果没有匹配的命令,会通过 this.listenerCount('command:*') 获取 command:xx 事件(* 为通配符)的监听者数量,决定是否触发该事件
    • Error

      下图是 Commander.js 内部定义的几个 Error 类的继承关系。

      Babel概览/Commander-Error类

      在内部实现上,分别定义了每个类自身的特殊字段。但值得注意的是,Error.captureStackTrace(this, this.constructor) 被频繁使用。

      • Error.captureStackTrace 使用

        Error.captureStackTrace(targetObject[, constructorOpt])

        其作用是在 targetObject 中添加一个 stack 属性。当访问 targetObject.stack 时,将以字符串的形式返回 Error.captureStackTrace 方法被调用时的代码位置信息。举例:

        index.js

        > 1 const myObject = {};
        > 2 Error.captureStackTrace(myObject);
        > 3 console.log(myObject.stack);
        
        1
        2
        3

        执行 node index.js 后,终端输出:

        Error
            at Object.<anonymous> (xxx/index.js:2:7)
            at Module._compile (internal/modules/cjs/loader.js:689:30)
            at ...
            at ...
        
        1
        2
        3
        4
        5

        当传入 constructorOpt 时,代码如:

        > 1 function MyError() {
        > 2    Error.captureStackTrace(this, MyError);
        > 3 }
        > 4
        > 5 console.log(new MyError().stack)
        
        1
        2
        3
        4
        5

        终端输出:

        Error
            at Object.<anonymous> (xxx/index.js:5:13)
            at Module._compile (internal/modules/cjs/loader.js:689:30)
            at ...
            at ...
        
        1
        2
        3
        4
        5

        可以看出,MyError 函数内部的堆栈细节被隐藏了。

      • Error.captureStackTrace 优点

        相对于 new Error().stackError.captureStackTrace 有以下优点:

        • 更简洁

          无需 new 一个新的 Error 对象,节省内存空间,同时代码上也会更加优雅。

          一般而言,捕获错误信息通常的做法是:

          try {
              new Error();
          } catch(err) {
              // err.stack 包含了堆栈信息,可以对其处理
          }
          
          1
          2
          3
          4
          5

          而使用 Error.captureStackTrace 可以直接获取堆栈信息,实现方式更简洁。

        • 更安全

          如果需要忽略部分堆栈信息,使用 Error.captureStackTrace 会更加方便,无需手工操作。

        • 更少资源

          使用 Error.captureStackTrace 时,只有访问 targetObject.stack 时,才会进行堆栈信息的格式化工作。

          如果 targetObj.stack 未被访问,则堆栈信息的格式化工作会被省略,从而节省计算资源。

      • Error.captureStackTrace 使用场景

        Error.captureStackTrace 并不是 Node.js 创造的,而是 V8 引擎的 Stack Trace API。语法上,Node.js 中的 Error.captureStackTrace()与 V8 引擎中所暴露的接口完全一致。

        事实上,Node.js 的 Error 类中,所有与 stack trace 有关的内容均依赖于 V8 的 Stack Trace API。

        因此,Error.captureStackTrace(targetObject[, constructorOpt]) 使用的场景有:

        • 基于 V8 引擎的运行环境,如 Node.js、Chrome 浏览器

        • Error.captureStackTrace(this, MyError)

          作用也是隐藏构造函数内部的堆栈信息,但需要明确指定构造函数名,通用性不强。

        • Error.captureStackTrace(this, arguments.callee)

          arguments.callee 表示当前函数,也有通用性。但 ES3 及之后的严格模式禁用了 arguments.callee,因此不建议使用。

        • Error.captureStackTrace(this, this.constructor)

          该做法可以隐藏构造函数内部的堆栈信息,无需指定构造函数名,通用型强。

# 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

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

  • 参数定义

    babel-cli 的参数定义基于 commander.js。

    例如:

    import commander from "commander";
    
    commander.option(
        "-f, --filename [filename]",
        "The filename to use when reading from stdin. This will be used in source-maps, errors etc.",
    );
    
    1
    2
    3
    4
    5
    6
  • globs 语法

    babel-cli 支持通配符匹配源文件,例如:

    babel *.js --output-file dist.js
    
    1

    *.js 会匹配当前目录所有以 .js 为后缀的文件。

    babel 使用 npm 包 glob 提供对通配符的支持。

    TODO: globs 语法

  • 参数错误信息提示

    解析参数的过程中,对于输入错误的情况,babel-cli 在解析过程中用数组统一收集错误信息,最后统一输出给使用者。

    这个技巧值得借鉴。

    // 错误收集
    const errors = [];
    
    if (commander.outFile && commander.outDir) {
        errors.push("--out-file and --out-dir cannot be used together");
    }
    
    // 错误展示
    if (errors.length) {
        console.error("babel:");
        errors.forEach(function (e) {
            console.error("  " + e);
        });
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  • 不向前兼容的提示

    babel-cli 历史版本可能是支持用户这样使用:

    const babel = require("babel-cli");
    
    babel.transform(...);
    
    1
    2
    3

    但后续的升级移除了对该功能的支持,为了提示使用者,babel-cli 保留了旧的 index.js,内容比较直接:

    throw new Error("Use the `@babel/core` package instead of `@babel/cli`.");
    
    1

    在做工具的破坏性升级时,这也是可以借鉴的提示技巧。

# 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 就不需要被转码了。

有两点需要注意:

  • babel-register 不会对所在的当前文件转码,也就是 index.js 不会被转码
  • babel-register 是实时转码,会影响运行性能,只适合在开发环境使用

# 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 方法,它具有如下能力:

  • 传入路径名,找到目标文件
  • 执行目标文件代码,并注入 module / require 等方法
  • 返回模块的 module.exports

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

// 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 的执行过程如下:

  • 执行 require(request: 模块名称)

    直接执行 Module._load(request: 模块名称)

  • 执行 Module._load(request: 模块名称)

    • 解析出绝对路径 Module._resolveFilename(request, parent)
      • 利用 const [id, paths] = Module._resolveLookupPaths(request, parent) 生成路径查找集合
        • 依赖 const paths = Module._nodeModulePaths(from: 当前模块的父路径)
      • 利用 Module._findPath(request, paths) 遍历 paths 找到目标模块的绝对路径
    • 优先返回缓存对象
    • 如果没有缓存对象,执行 Module.load
  • 执行 Module.load(filename: 绝对路径)

    • 针对不同文件后缀执行不同逻辑
      • js 文件会执行 module._compile(content: 模块内容, filename: 绝对路径)
  • 执行 module._compile(content: 模块内容, filename: 绝对路径)

    • 将目标模块源码用 exports, require... 包裹

      在 node 代码里常见的 exportsrequiremodule__dirname__filename 均是被注入的局部变量

    • 执行目标模块代码

      目标模块的代码是通过 node 虚拟机 require('vm').runInThisContextrequire('vm').runInNewContext 执行的

# 5.4.3 为 require 添加钩子

  • 基本原理

    我们已经了解了 require 的加载机制,那么,假如文件 run.js 的内容如下:

    console.log('Hi, this is run.js.');
    
    1

    另有文件 index.js 的内容如下:

    require('./run.js')
    
    1

    当执行 node index.js 时,终端会打印:

    Hi, this is run.js.
    
    1

    然后,我们尝试下拦截 require,在执行 node index.js 时,终端打印:

    Knock knock, I'm here!
    Hi, this is run.js.
    
    1
    2

    结合前面讲解的 require 加载机制,我们可以从以下点入手:

    • 重写 Module._extensions['.js'],拦截所有 .js 文件的 require 行为

      const Module = require('module');
      
      const oldLoader = Module._extensions['.js'];
      
      Module._extensions['.js'] = function(module, filename) {
          // 这里可以添加新逻辑
          
          oldLoader(module, filename);
      };
      
      require('./run.js');
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
    • 在新的处理函数中,重写 module 实例的 _compile 方法

      const Module = require('module');
      
      const oldLoader = Module._extensions['.js'];
      
      Module._extensions['.js'] = function (module, filename) {
          const oldCompile = module._compile;
      
          // 重写 module._compile
          module._compile = function (code) {
              const newCode = `
                      console.log("Knock knock, I'm here!");
                      ${code}
                  `;
      
              return oldCompile.apply(this, [newCode, filename]);
          };
      
          oldLoader(module, filename);
      };
      
      require('./run.js');
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21

    这样,重新执行 node index.js,终端打印出:

    Knock knock, I'm here!
    index.js
    
    1
    2
  • pirates

    很显然上面的代码并不优雅,真实的需求中我们可能无需拦截所有的 .js 文件,可能也需要拦截其他后缀的文件,各种操作还是比较繁琐的。

    babel-register 使用 npm 包 pirates 提供的 addHook 方法拦截 require,拦截时利用 Babel 对源码转译。

    // 拦截 require 注册 compileHook 方法
    // compileHook 会利用Babel转译模块源码,返回转译后的代码
    addHook(compileHook, { exts, ignoreNodeModules: false })
    
    1
    2
    3

    addHook 的核心原理同上,但处理了更多细节:

    • 增强安全性

      import BuiltinModule from 'module';
      const Module = module.constructor.length > 1 ? module.constructor : BuiltinModule;
      
      1
      2

      Module 对象的获取优先选取 module.constructor,其次选取内置的 "module" 模块,以适配 mock 出的 module 实例。

    • 能够恢复被拦截方法到原始状态

      function revert() {
          if (reverted) return;
          reverted = true;
      
          exts.forEach((ext) => {
              // if the current loader for the extension is our loader then unregister it and set the oldLoader again
              // if not we can not do anything as we cannot remove a loader from within the loader-chain
              if (Module._extensions[ext] === loaders[ext]) {
                  Module._extensions[ext] = oldLoaders[ext];
              }
          });
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
    • 根据配置项确定是否处理目标模块源码

      function shouldCompile(filename, exts, matcher, ignoreNodeModules) {
          if (typeof filename !== 'string') {
              return false;
          }
          if (exts.indexOf(path.extname(filename)) === -1) {
              return false;
          }
      
          const resolvedFilename = path.resolve(filename);
      
          if (ignoreNodeModules && nodeModulesRegex.test(resolvedFilename)) {
              return false;
          }
          if (matcher && typeof matcher === 'function') {
              return !!matcher(resolvedFilename);
          }
      
          return true;
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19

# 5.4.4 babel-register 内部实现

  • 主要流程

    在理解了 require 的加载机制、如何为 require 添加钩子后,继续阅读 babel-register 源码会得心应手很多。

    Babel工具集/babel-register

    babel-register 依赖 pirates.js 提供的 addHook 方法,实现拦截 require,同时在拦截函数里执行 Babel 的转译逻辑。

  • 缓存处理

    TODO

# 5.5 命令行:babel-node

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

Babel工具集/babel-node

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

  • v8flags

    TODO

  • kexec 与 spawn

    TODO

  • 处理 exit 事件

    TODO

  • repl

    TODO

  • vm.runInThisContext

    TODO

  • Module

    TODO

  • node 参数处理

    TODO

# 5.6 浏览器支持:babel-standalone

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

Babel工具集/babel-standalone

# 5.7 babel-highlight

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

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

  • token 化处理

    babel-highlight 依赖 js-tokens 对代码块解析为不同类型的 token 单元。

    TODO

  • 高亮工具:chalk

    babel-highlight 使用 chalk 实现代码块在终端的高亮。

    TODO

  • 高亮规则

    babel-highlight 会用不同颜色对以下目标类型进行高亮处理:

    • keyword: 关键词
    • capitalized: 大写
    • jsxIdentifier: JSX标识符
    • punctuator: 标点
    • number: 数字
    • string: 字符串
    • regex: 正则
    • comment: 注释
    • invalid: 非正常写法
    • uncolored

# 5.8 babel-template

TODO

# 5.9 webpack 支持:babel-loader

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

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

Babel工具集/babel-loader-总览

  • 转译模块

    babel-loader 直接使用 babel-core 的 transform 方法执行源码的转译。而 transform 的转译结果对象包含众多属性,babel-loader 选取了部分属性作为新的结果对象。

    根据 Github issue 中的描述,babel-loader 开发者注意到,transform 返回的原始结果对象中,含有难以序列化的属性值(如 options),而 Babel 本身也会提供本地化的缓存结果,对于难以序列化的 options 等属性,缓存时会丢失一些信息,因此,babel-loader 只会返回可以序列化的部分属性组成的新结果对象。

  • 缓存模块

    babel-loader 将转移过的结果缓存到本地文件系统。

    缓存路径优先使用开发者设置的 cacheDirectory 路径,否则使用 babel-loader 提供的默认缓存路径。

    经过 babel-core 的 transform API 处理的转译结果 result 对象,会直接被 JSON.stringify(result) 转为字符串,并写入本地缓存。如果开发者指定需要对缓存文件压缩,则会压缩为 .gz 文件,否则会明文写入缓存文件。

    下图描述了缓存处理过程:

    Babel工具集/babel-loader-cache

  • 错误处理模块

    对于转译过程发生的错误,babel-loader 并没有直接使用 Node 的原生错误表现,而是基于原生 Error 类,封装了定制化的 LoaderError 类。 LoaderError 类对错误信息执行格式化,主要规范了以下属性:

    • name

      报错名称统一为"BabelLoaderError"。

    • message

      去掉 message 信息中的冗余信息。

    • hideStack

      隐藏堆栈信息。

    • 使用 Error.captureStackTrace 隐藏非必要信息

      TODO

Last update: October 11, 2022 15:38
Contributors: hoperyy