Vite 虚拟模块入门:自动生成 Changelog
虚拟模块是什么
Tip
Vite 官方对它的介绍是:虚拟模块是一种很实用的模式,使你可以对使用 ESM 语法的源文件传入一些编译时信息。
通俗点说:虚拟模块并没有实际的文件存在与文件系统中,而是由 Vite 内部插件或构建工具动态生成的。通过虚拟模块,开发者可以直接 import 一些特定标识符(如 virtual:xxx),获取动态内容。
ts
import data from 'virtual:my-module'
console.log(data) // 输出虚拟模块的内容
虚拟模块的作用和运用场景
它主要有以下几个作用:
- 动态内容注入:允许开发者根据运行时环境生成动态内容。例如,提供项目配置、动态构建信息等。
- 避免文件创建:无需生成实际的物理文件,简化开发过程,同时减少磁盘
I/O
。 - 增强开发体验:在开发环境中提供内置功能模块,例如热重载、状态管理工具的动态注入。
- 插件扩展能力:插件可以通过虚拟模块向项目注入特定功能,而不需要开发者手动添加文件或代码。
而根据它的作用,我们可以利用这些特性来实现一些特定的功能,例如:
- 构建元信息的注入,例如版本号,GIT 提交信息、项目文件信息等。
- 开发工具支持,例如热重载、状态管理工具的动态注入。
- 跨语言资源加载,例如通过虚拟模块将
.wasm
、.json
等非 JavaScript 资源动态加载为模块。
Note
总的来说,适合采用虚拟模块的场景有:
- 内容动态生成:当模块内容是动态生成的,比如构建信息、配置项。
- 避免生成实际文件:无需生成临时文件或过多中间文件时。
- 插件注入功能:开发一个插件时需要向项目注入额外功能或数据。
- 优化构建性能:通过虚拟模块减少文件扫描和读写操作,提升效率。
- 调试和开发工具支持:比如工具链扩展、实时注入调试信息等。
举例实现
此处我们以 Gupo-Admin 项目为例,实现一个虚拟模块,用于自动生成每个组件下的 Changelog 信息。
首先,我们需要一个获取 git 提交日志的方法:
tsimport { simpleGit } from 'simple-git' const git = simpleGit({ maxConcurrentProcesses: 200, }) export async function getChangeLog(count = 200) { const logs = (await git.log({ maxCount: count })).all.filter(commit => commit.message.startsWith('feat(components):') || commit.message.startsWith('fix(components):')) const result: { hash: string date: string message: string components: string[] }[] = [] for (const log of logs) { const raw = await git.raw(['diff-tree', '--no-commit-id', '--name-only', '-r', log.hash]) const files = raw.split('\n').filter(Boolean) // 提取该条提交影响的组件 const components = Array.from(new Set(files .filter(file => file.startsWith('components/src/') && !file.includes('components/src/utils') && !file.includes('components/src/hooks')) .map(file => file.split('/').at(-3)) as string[], )) result.push({ hash: log.hash, date: log.date, message: log.message, components, }) } return result }
这里我们过滤出
feat(components):
和fix(components):
的提交日志,并获取每个提交的 hash、日期、消息和影响的组件。创建对应的虚拟模块:
tsimport type { PluginOption } from 'vite' import process from 'node:process' import { getChangeLog } from '../utils/changelog' export function ChangeLog(): PluginOption { const virtualModuleId = 'virtual:changelog' const resolvedVirtualModuleId = `\0${virtualModuleId}` return { name: 'vite:changelog', resolveId(id) { if (id === virtualModuleId) { return resolvedVirtualModuleId } }, async load(id) { if (id === resolvedVirtualModuleId) { const changelog = await getChangeLog(process.env.DEV ? 5000 : 50) return `export default ${JSON.stringify(changelog)}` } }, } }
虚拟模块在 Vite(以及 Rollup)中都以 virtual: 为前缀,作为面向用户路径的一种约定。在内部,使用了虚拟模块的插件在解析时应该将模块 ID 加上前缀
\0
,这一约定来自 rollup 生态。在
Changelog.vue
组件中使用虚拟模块:vue<script lang="ts" setup> import { NEllipsis } from 'naive-ui' import changelog from 'virtual:changelog' import { computed } from 'vue' const props = defineProps<{ component: string }>() const commits = computed(() => changelog .filter(commit => commit.components.includes(props.component))) </script> <template> <div class="space-y-1"> <div v-for="commit of commits" :key="commit.hash" class="flex items-center gap-2"> <div class="flex shrink-0 items-center justify-center rounded-full bg-[var(--vp-c-bg-alt)] p-1.5"> <span class="i-lucide-git-commit-vertical size-4" /> </div> <a :href="`https://codeup.aliyun.com/gupo/rd-frontend/dev/dev-business-kits-docs/commit/${commit.hash}`" class="text-sm font-mono"> {{ commit.hash.slice(0, 5) }} </a> - <NEllipsis :tooltip="{ contentClass: 'max-w-300px max-h-300px overflow-y-auto' }" class="text-[var(--vp-c-text-alt)]!"> {{ commit.message }} </NEllipsis> <span class="ml-auto shrink-0 text-[var(--vp-c-text-2)] font-mono"> on {{ new Date(commit.date).toLocaleDateString() }} </span> </div> </div> </template>
在
vite.config.ts
中使用该插件:tsimport { ChangeLog } from './plugins/changelog' export default defineConfig({ plugins: [ ChangeLog(), { name: 'vite:add-changelog-info', enforce: 'pre', transform(code, id) { const match = id.match(/components\/at-(.*)\.md/) if (match) { const componentName = match[1] return `${code} \n\n ## changelog \n\n <Changelog component="${componentName}" />` } return code }, }, ], })
此场景下,为了避免手动给每个组件的文档加上日志展示,我们可以再写一个简单的插件,在文档中自动注入日志展示。(当然此处的
Changelog
组件已经全局注册了)。
大功告成
最终实现的效果图如下: