Libon

Vite 虚拟模块

6Mins #vite
关于 Vite Virtual Module 的一知半解

ToC

前言

rollup第一次提交代码 到现在已经七年多了,算不上新奇,但即便是在现在各种打包工具百花齐放(Vite、swc、tsup、unbuild、esbuild 等)的时代,仍占据着不可或缺的一席之地,虽然在下载量这方面还远不及同类的 webpack,但这和两者的应用场景有很大关系。rollup 主要适用于插件、库的打包,而 webpack 主要适用于项目开发。但虽然在用户方面没有什么优势,但 rollup 依然创造且实装了许多非常实用且新颖的功能,比如这一次的主题:virtual module

据我所知,virtual module 在 web 开发中的第一次露面是在五年前 Rich Harris 对 [仓库](initial commit · rollup/rollup-plugin-virtual@6d7f6d0 · GitHub 进行的代码提交。可能在其他语言中这个概念早就有了,甚至已经运行了很久,但对于 web 来说,这是个先河。

Anton Medvedev 是一名非常优秀的程序员,他编写了 zxfxexpr很多很酷且有趣的东西 ,比如他将 公元日期 发布为单独的 npm 包,从 1970 年到 2038 ,每一年都有一个单独的 npm 包,虽然 2038 年只有 1 月份,但这已经足够让人感到震撼了(感到笑死 :D),你甚至可以在 npm 上找到自己的生日(const birthday = require('@year/1970/01/01')),并在项目中使用它(:D)。非常有趣,但同时这工作量也非常庞大,手动维护它太不方便了,如果我们使用虚拟模块来做的话,则非常简单。

Vite 虚拟模块

Vite 中的虚拟模块其实是沿用了 rollup 中的概念,它本身类型 alias 别名,但模块的内容本身并不是直接存储在磁盘中,而是内存中,因为它本身并不存在,而是在编译的时候动态生成的,这也是为什么说他是一个潜力被低估的原因之一。

在 Vite 文档中对虚拟模块的描述 是从这开始的:

虚拟模块是一种很实用的模式,使你可以对使用 ESM 语法的源文件传入一些编译时信息。

而在官网中也有一个现成的例子:

1
export default function myPlugin() {
2
const virtualModuleId = 'virtual:my-module'
3
const resolvedVirtualModuleId = '\0' + virtualModuleId
4
5
return {
6
name: 'my-plugin',
7
resolveId(id) {
8
if (id === virtualModuleId) {
9
return resolvedVirtualModuleId
10
}
8 collapsed lines
11
},
12
load(id) {
13
if (id === resolvedVirtualModuleId) {
14
return `export const msg = "from virtual module"`
15
}
16
}
17
}
18
}
1
import { msg } from 'virtual:my-module'
2
3
console.log(msg)

从例子中我们可以看出以上行为创建了一个虚拟模块,并将 export const msg = "from virtual module" 作为模块的内容返回了出来,这样就可以在项目中任意地方引入这个 msg 变量来使用了。虚拟模块的详细限制在文档中已经详细有写,文中就不赘述了,我们完全可以根据以上的这个例子来实现上文中讲到的 【公元日期】的虚拟模块,Let’s dance!

实现一个虚拟模块

在实现 Vite 虚拟模块之前,我们先创建一个 Vite 项目吧:

Terminal window
1
npx create-vite vite-virtual-module-plugin

会询问:

Need to install the following packages: create-vite Ok to proceed? (y)

按回车确认即可。因为我们只是想创建一个 Vite 项目来测试插件而已,所以我们选 vanilla 这个选项,即原生js,在安装以后没有什么框架的其他依赖,选择完 vanilla 以后还会出现两个选项,分别为:vanillavanilla-ts ,如果想写 ts 的,则选 ts,懒得定义的则选 vanilla 即可。插件的实现如下:

1
function MyVirtualDatesPlugin() {
2
// 虚拟模块的前缀,在使用的时候,模块名必须以这个作为前缀的模块名才会被进一步解析
3
const modulePrefix = 'virtual-dates:'
4
5
return {
6
// 我们自定义的插件名
7
name: 'my-virtual-dates-plugin',
8
resolveId(id) {
9
// 判断是否符合插件的前缀条件
10
const [, date] = id.split(modulePrefix)
19 collapsed lines
11
// 如果不符合则停止
12
if (!date) return
13
// 如果符合基本格式,但日期的值不合法,比如 13 月, 32 号
14
if (Number.isNaN(Date.parse(date))) {
15
throw new Error('Trying to load an invalid date')
16
}
17
// 一切正常则返回
18
return id
19
},
20
load(id) {
21
// 在加载文件内容的钩子中返回这个虚拟模块的内容
22
const [, date] = id.split(modulePrefix)
23
// 如果值不合法则跳过,加载下一个文件
24
if (!date) return
25
// 返回一个预期的文件内容
26
return `export default new Date('${date}')`
27
}
28
}
29
}

这样我们就完成了这个插件的开发,但如果使用了 ts,那么在导入的时候肯定是会有类型报错的,因为找不到这个模块的类型声明,这时候就需要我们去定义它。

定义 TS 模块类型声明

src 下有一个 vite-env.d.ts 文件,我们只需要在里面追加一下内容即可:

1
// 对所有以 virtual-dates: 开头的模块声明共同的类型
2
declare module 'virtual-dates:*' {
3
// 声明一个变量,保存这个值的类型
4
const date: Date
5
// 告诉 ts,这个模块默认导出的值就是这个 date 变量的类型
6
export default date
7
}

完整代码

完整的代码在这里:GitHub - libondev/https://github.com/libondev/learning-vite-virtual-module-plugin.git ,当然还可以通过在线地址来试试:Vitejs - Vite (forked) - StackBlitz

以上.


CD ..