Libon

Vite 虚拟模块

#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 语法的源文件传入一些编译时信息。

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

export default function myPlugin() {
  const virtualModuleId = 'virtual:my-module'
  const resolvedVirtualModuleId = '\0' + virtualModuleId

  return {
    name: 'my-plugin',
    resolveId(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId
      }
    },
    load(id) {
      if (id === resolvedVirtualModuleId) {
        return `export const msg = "from virtual module"`
      }
    }
  }
}
import { msg } from 'virtual:my-module'

console.log(msg)

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

实现一个虚拟模块

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

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 即可。插件的实现如下:

function MyVirtualDatesPlugin() {
  // 虚拟模块的前缀,在使用的时候,模块名必须以这个作为前缀的模块名才会被进一步解析
  const modulePrefix = 'virtual-dates:'

  return {
    // 我们自定义的插件名
    name: 'my-virtual-dates-plugin',
    resolveId(id) {
      // 判断是否符合插件的前缀条件
      const [, date] = id.split(modulePrefix)
      // 如果不符合则停止
      if (!date) return
      // 如果符合基本格式,但日期的值不合法,比如 13 月, 32 号
      if (Number.isNaN(Date.parse(date))) {
        throw new Error('Trying to load an invalid date')
      }
      // 一切正常则返回
      return id
    },
    load(id) {
      // 在加载文件内容的钩子中返回这个虚拟模块的内容
      const [, date] = id.split(modulePrefix)
      // 如果值不合法则跳过,加载下一个文件
      if (!date) return
      // 返回一个预期的文件内容
      return `export default new Date('${date}')`
    }
  }
}

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

定义 TS 模块类型声明

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

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

完整代码

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

以上.


CD ..