虚拟 HTML 生成(virtual-html)
为什么使用
- 无需维护
index.html文件:通过配置对象生成根 HTML,减少重复编辑与合并冲突。 - 与 Vite 生态兼容:开发阶段仍走
transformIndexHtml,其他 HTML 插件的变换不会丢失。 - 更友好的抽象:标签统一用对象描述,易于版本化与复用,适合多项目模板化交付。
- 默认即开箱可用:内置默认配置(标题、入口、基础
meta与favicon、div#app),零成本启动。 - 构建也生效:构建时生成临时
/.quiteer/index.html并作为 Rollup 输入,保持“无 HTML 文件”的体验。
快速上手
ts
// vite.config.ts
import { defineConfig } from 'vite'
import { virtualHtmlPlugin } from '@quiteer/vite-plugins'
export default defineConfig(() => ({
plugins: [
// 使用文件配置(默认 'html.config.ts'),存在真实 index.html 时是否覆盖由 fallback 控制
virtualHtmlPlugin({ configFile: 'html.config.ts', fallbackWhenIndexExists: false })
]
}))ts
// html.config.ts(可选,未提供时使用内置默认配置)
export default {
title: 'Vue App',
entry: '/src/main.ts',
htmlAttrs: { lang: 'zh-CN' },
bodyAttrs: { class: 'theme-light' },
// 外链样式与脚本(可选)
style: {
src: '/src/style.css',
media: 'screen',
position: 'head',
attrs: { id: 'main-style' }
},
script: {
src: 'https://unpkg.com/lodash@4.17.21/lodash.min.js',
async: true,
position: 'body-append',
attrs: { 'data-demo': 'quiteer' }
},
tags: [
{ tag: 'meta', attrs: { charset: 'utf-8' }, selfClosing: true, position: 'head' },
{ tag: 'meta', attrs: { name: 'viewport', content: 'width=device-width, initial-scale=1' }, selfClosing: true, position: 'head' },
{ tag: 'link', attrs: { rel: 'icon', href: '/vite.svg' }, selfClosing: true, position: 'head' }
],
appRoot: { id: 'app', tag: 'div' }
}配置来源与覆盖规则
- 文件配置:
configFile指向html.config.ts,若不存在则使用内置默认配置。 - 内联配置:通过
options.config直接传入,优先于文件;与默认进行“浅合并”与“整体替换”。 - 覆盖策略:
title/entry/tags:提供即整体替换,不提供保留默认htmlAttrs/headAttrs/bodyAttrs:与默认浅合并(同键覆盖)appRoot.attrs:浅合并;appRoot.id/appRoot.tag单独覆盖
- 重复
meta避免:若tags已包含charset或viewport,默认不会再次注入。
选项说明
root?: string:项目根目录,默认vite的root。configFile?: string:配置文件路径,默认'html.config.ts'。config?: VirtualHtmlConfig:内联配置对象,优先于文件读取。fallbackWhenIndexExists?: boolean:当根下存在真实index.html时是否仍启用虚拟 HTML,默认false。
外链脚本与样式(新增)
script?: { src: string; position?: 'head' | 'body-prepend' | 'body-append'; attrs?: Record<string, any>; type?: string; async?: boolean; defer?: boolean; crossorigin?: 'anonymous' | 'use-credentials'; integrity?: string; referrerpolicy?: string; nonce?: string; fetchpriority?: 'high' | 'low' | 'auto' }- 生成
<script>标签;默认位置为body-append - 可通过
type: 'module'指定模块脚本;async/defer控制加载与执行时机 - 外部资源建议配合
integrity + crossorigin提升安全性 attrs可扩展自定义属性(如data-*)
- 生成
style?: { src: string; position?: 'head' | 'body-prepend' | 'body-append'; attrs?: Record<string, any>; rel?: 'stylesheet'; media?: string; crossorigin?: 'anonymous' | 'use-credentials'; integrity?: string; referrerpolicy?: string }- 生成
<link rel="stylesheet" href="...">;默认位置为head - 可设置
media(如screen/print)与crossorigin/integrity/referrerpolicy - 若需内联样式,使用
tags: [{ tag: 'style', children: '...'}]
- 生成
ts
// 示例:完整的脚本/样式外链
export default {
style: { src: '/src/style.css', media: 'screen', position: 'head', attrs: { id: 'demo-style' } },
script: { src: 'https://cdn.example.com/analytics.js', async: true, position: 'body-append', attrs: { 'data-app': 'quiteer' } }
}类型与 TS 提示
ts
import type { VirtualHtmlOptions, VirtualHtmlConfig, VirtualHtmlTag } from '@quiteer/vite-plugins'
const tags: VirtualHtmlTag[] = [
{ tag: 'link', attrs: { rel: 'icon', href: '/icons/app.svg' }, selfClosing: true, position: 'head' },
{ tag: 'script', attrs: { type: 'module', src: '/feature.ts' }, position: 'body-append' },
{ tag: 'meta', attrs: { name: 'theme-color', content: '#222' }, selfClosing: true, position: 'head' }
]
const cfg: VirtualHtmlConfig = {
title: 'Quiteer App',
entry: '/src/main.ts',
htmlAttrs: { lang: 'zh-CN' },
bodyAttrs: { class: 'theme-dark' },
style: { src: '/src/style.css', media: 'screen', position: 'head', attrs: { id: 'main-style' } },
script: { src: '/vendor/feature.js', defer: true, position: 'body-append' },
tags,
appRoot: { id: 'app', tag: 'div', attrs: { 'data-app': 'root' } }
}
const opt: VirtualHtmlOptions = {
config: cfg,
fallbackWhenIndexExists: true
}- IDE 会对
tag/attrs/selfClosing/position提示与约束;attrs支持string|number|boolean|null|undefined。 position支持head | body-prepend | body-append;当未指定且为script时默认视为body-append。style以<link rel="stylesheet" href="...">注入,默认位置head;script默认位置body-append。
工作机制
- 开发阶段
- 中间件拦截
/与/index.html,根据配置生成 HTML,并调用server.transformIndexHtml应用其它插件的变换。 - 根下存在真实
index.html时,默认放行;可用fallbackWhenIndexExists强制覆盖。
- 中间件拦截
- 构建阶段
- 在
config钩子生成临时/.quiteer/index.html并设置到build.rollupOptions.input,让 Vite 以该文件作为入口。
- 在
应用场景
- 脚手架与模板工程:统一 HTML 输出,项目间复用一套配置。
- 多主题/多品牌:通过配置切换
favicon、title、meta,无需维护多份index.html。 - 组件/插件 Demo:快速生成可运行的 HTML 根结构,突出入口脚本与根节点。
- CI/CD 流程:构建时生成入口 HTML,弱化仓库中的静态 HTML 依赖。
与手写 index.html 的对比
- 可维护性:对象化描述更利于版本控制与复用;多人协作减少冲突点。
- 兼容性:仍走 Vite 的
transformIndexHtml,与现有生态(注入、变换)兼容。 - 灵活性:支持“文件配置 or 内联配置”,默认值开箱即可。
示例:自定义标签
ts
export default {
title: 'App',
entry: '/src/main.ts',
tags: [
{ tag: 'meta', attrs: { name: 'color-scheme', content: 'dark light' }, selfClosing: true, position: 'head' },
{ tag: 'script', children: 'window.__BOOT__ = Date.now()' } // 内联脚本
],
appRoot: { id: 'app' }
}注意事项
- 安全:避免将不受信任的文本直接放入
children;必要时自行转义或过滤。 - 外部资源:若引用第三方脚本/样式,建议设置
integrity与crossorigin,并评估可信来源。 - 入口:
entry默认'/src/main.ts',需为 ES Module 并能正常挂载应用至appRoot。 - 真实 HTML:如需在某些项目保持手写
index.html,将fallbackWhenIndexExists设为false即可。
性能与安全
- 性能:生成与渲染为轻量的字符串拼接操作,开销极小。
- 安全:不对外注入环境变量;与 Vite 的 HTML 流程兼容,避免破坏其它插件的注入逻辑。