Es-Module-Lexer,ES Module 语法的词法分析利器

前言

说到词法分析,我想很多同学第一时间想到的可能是 Babel、Acorn 等工具。不可否认,它们都很强大 。

但是,具体到今天这个话题 ES Module 语句的词法分析而言,es-module-lexer 会胜过它们很多!

那么,今天我们将围绕以下 2 点,深入浅出一番 es-module-lexer:

  • 认识 es-module-lexer
  • 实际场景下如何应用 es-module-lexer

1 认识 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 方法,下面我们来看一下它们分别做了什么?

1.1 init(Promise 对象)

init 必须parse() 方法前 Resolve(解析),它的实现可以分为 3 个步骤:

  • 首先,调用 WebAssembly.compile() 方法编译 WebAssembly 二进制代码到为 WebAssembly.ModulePromise 对象
  • 然后,再调用 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

1.2 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(解析)initparse() 方法会自己先 Resolve(解析) init。然后,在 .then 中调用并返回 parse() 方法,所以在这种情况下,parse() 方法会返回一个 Promise 对象。

当然,不管任何情况下,parse() 方法的本质是返回一个数组(长度为 3)。并且,和我们使用密切相关的主要是 importsexports

importsexports 都是一个数组,其中每个元素(对象)代表一个导入语句的解析后的结果,具体会包含导入或导出的模块的名称、在源代码中的位置等信息。

接下来,我们通过一个简单的例子来认识一下 es-module-lexer 的基本使用。

1.3 基本使用

首先,我们基于 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 的实现原理和使用后,我想同学们可能会思考它在实际场景下中要如何运用?(请继续阅读 )

2 实际场景下如何应用 es-module-lexer

在同学们可能还没意识到哪里用到了 es-module-lexer 的时候,其实它已经走进了我们平常的开发中。

那么,这里我们以 vite-plugin-style-import 插件为例,认识一下它又是如何使用 es-module-lexer 的?(别走开,接下来会非常有趣 )

2.1 浅析 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),选择性地注入特定的代码到源代码中

2.2 使用 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~

点赞

通过阅读本篇文章,如果有收获的话,可以点个赞,这将会成为我持续分享的动力,感谢~

你可能感兴趣的:(前端构建工具,前端,词法分析,es,module,es-module-lexer)