前言
说到词法分析,我想很多同学第一时间想到的可能是 Babel、Acorn 等工具。不可否认,它们都很强大 😶。
但是,具体到今天这个话题 ES Module 语句的词法分析而言,es-module-lexer 会胜过它们很多!
那么,今天我们将围绕以下 2 点,深入浅出一番 es-module-lexer:
- 认识 es-module-lexer
- 实际场景下如何应用 es-module-lexer
认识 es-module-lexer
es-module-lexer 是一个可以对 ES Module 语句进行词法分析的工具包。它压缩后之后只有 4 KiB,其底层通过内联(Inline) WebAssembly 的方式来实现对 ES Module 语句的快速词法分析。
1KiB = 1,024Byte
那么,具体会有多快?根据官方给的例子,Angular1(720 KiB)使用 Acorn 解析所需要的时间为 100 ms,而 es-module-lexer 解析只需要 5 ms,也就是前者的 1/20 😵。
并且,es-module-lexer 的使用也非常简单,它提供了 init Promise 对象和 parse 方法,下面我们来看一下它们分别做了什么?
init(Promise 对象)
init 必须在 parse() 方法前 Resolve(解析),它的实现可以分为 3 个步骤:
- 首先,调用 WebAssembly.compile() 方法编译 WebAssembly 二进制代码到为 WebAssembly.Module 的 Promise 对象
- 然后,再调用 WebAssembly.Instantiate() 方法创建一个实例
- 最后,则可以在实例上访问 exports 属性来获取调用的模块提供的方法
这个过程对应的代码:
let wasm;
const init = WebAssembly.compile(
(binary => typeof window !== 'undefined' && typeof atob === 'function' ? Uint8Array.from(atob(binary), x => x.charCodeAt(0)) : Buffer.from(binary, 'base64'))
('WASM_BINARY')
).then(WebAssembly.instantiate)
.then(({ exports }) => { wasm = exports; });
而这里的二进制代码,则是由 C 实现的对 ES Module 语句进行词法分析的代码编译得来。
并且,可以看到实例的 exports 会被赋值给 wasm。
parse() 方法
parse() 方法则会使用在上面得到的 WebAssembly.Module 提供的方法(即 wasm)来实现对 ES Module 语法的词法分析。
这个过程对应的代码(伪代码):
function parse (source, name = '@') {
if (!wasm)
return init.then(() => parse(source));
// 调用 wasm 上的方法进行对应的操作
return [imports, exports, !!wasm.f()];
}
注意,这里不对 wasm 上提供的方法进行分析,有兴趣的同学可以自行了解~
可以看到,如果我们在调用 parse() 方法之前没有 Resolve(解析)init,parse() 方法会自己先 Resolve(解析) init。然后,在 .then 中调用并返回 parse() 方法,所以在这种情况下,parse() 方法会返回一个 Promise 对象。
当然,不管任何情况下,parse() 方法的本质是返回一个数组(长度为 3)。并且,和我们使用密切相关的主要是 imports 和 exports。
imports 和 exports 都是一个数组,其中每个元素(对象)代表一个导入语句的解析后的结果,具体会包含导入或导出的模块的名称、在源代码中的位置等信息。
接下来,我们通过一个简单的例子来认识一下 es-module-lexer 的基本使用。
基本使用
首先,我们基于 es-module-lexer 定义一个 parseImportSyntax() 方法:
const { init, parse } = require("es-module-lexer")
async function parseImportSyntax(code = "") {
try {
await init
const importSpecifier = parse(code)
return importSpecifier
} catch(e) {
console.error(e)
}
}
可以看到 parseImportSyntax() 方法会返回 parse 后的结果。假设,此时我们需要解析导入 ant-design-vue 的 Button 组件的语句:
const code = `import { Button } from 'ant-design-vue'`
parseImportSyntax(code).then(importSpecifier => {
console.log(importSpecifier)
})
对应的输出:
[
[
{
n: 'ant-design-vue',
s: 24,
e: 38,
ss: 0,
se: 39,
d: -1
}
],
[],
true
]
由于,我们只声明了导入语句,所以最后解析的结果只有 imports 内有元素,该元素(对象)的每个属性对应的含义:
- n 表示模块的名称
- s 表示模块名称在导入语句中的开始位置
- e 表示模块名称在导入语句中的结束位置
- ss 表示导入语句在源代码中的开始位置
- se 表示导入语句在源代码中的结束位置
- d 表示导入语句是否为动态导入,如果是则为对应的开始位置,否则默认为 -1
那么,在简单了解完 es-module-lexer 的实现原理和使用后,我想同学们可能会思考它在实际场景下中要如何运用?(请继续阅读 😎)
实际场景下如何应用 es-module-lexer
在同学们可能还没意识到哪里用到了 es-module-lexer 的时候,其实它已经走进了我们平常的开发中。
那么,这里我们以 vite-plugin-style-import 插件为例,认识一下它又是如何使用 es-module-lexer 的?(别走开,接下来会非常有趣 😋)
浅析 vite-plugin-style-import 原理
在正式开讲 es-module-lexer 在 vite-plugin-style-import 中的使用之前,我们需要知道 vite-plugin-style-import 做了什么?
它解决了我们按需引入组件时,需要手动引入对应组件样式的问题。例如,在使用 ant-design-vue 的时候,按需引入 Button 只需要声明:
import { Button } from "ant-design-vue"
然后,经过 vite-plugin-style-import 处理后对应的代码片段:
import { Button } from 'ant-design-vue';
import 'ant-design-vue/es/button/style/index.js';
而这个过程的实现可以分为以下 3 个步骤:
- 使用 es-module-lexer 对源代码的导入(import)语句进行词法分析
- 根据配置文件 vite.config.js 中的 vite-plugin-style-import 的配置项来构造样式文件的导入语句
- 根据环境(会区分 Dev 或 Prod),选择性地注入特定的代码到源代码中
使用 es-module-lexer 的黑魔法
在1.3 基础使用小节的部分,我们讲了 es-module-lexer 解析导入语句时,只会返回导入模块相关的信息,那么这在 vite-plugin-style-import 中显然是不够的!
因为,vite-plugin-style-import 还需要知道此时导入了该模块的什么组件,这样才能去拼接生成对应的样式文件的导入语句。
那么,这个时候使用 es-module-lexer 的黑魔法就来了,我们可以将原来的导入语句的 import 替换为 export,然后 es-module-lexer 就会解析出导出的组件信息(想不到吧 😲)!
例如,同样是上面导入 ant-design-vue 的 Button 的例子,替换 import 后会是这样:
export { Button } from "ant-design-vue"
这个时候,使用 es-module-lexer 解析后返回的结果:
[
[
{
n: 'ant-design-vue',
s: 24,
e: 38,
ss: 0,
se: 39,
d: -1
}
],
[ 'Button' ],
true
]
可以看到,Button 被放到了解析结果的(数组第二个元素) exports 中,这样一来我们就知道了使用导入模块的组件有哪些 😎。
而这个过程,在 vite-plugin-style-import 中是由 transformImportVar() 方法完成的:
function transformImportVar(importStr: string) {
if (!importStr) {
return [];
}
const exportStr = importStr.replace('import', 'export').replace(/\s+as\s+\w+,?/g, ',');
let importVariables: readonly string[] = [];
try {
importVariables = parse(exportStr)[1];
debug('importVariables:', importVariables);
} catch (error) {
debug('transformImportVar:', error);
}
return importVariables;
}
结语
很有趣的一点是 awesome-vite 上有两个支持按需引入组件样式文件的插件。通过阅读,我想同学们应该知道用哪个了吧 😎!
最后,用一句话总结 es-module-lexer 的优点,那就是:“快到飞起”。如果,文中存在表达不当或错误的地方,欢迎同学提 Issue~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。