魔改版vite-plugin-html超好用的vite HTML模板插件!
Posted warmbook
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了魔改版vite-plugin-html超好用的vite HTML模板插件!相关的知识,希望对你有一定的参考价值。
使用示例
// vite.config.js
import createhtmlPlugin from './vite-plugin-html.js'
export default async ()=>
// 前置处理
const pages=[
// 默认的filename是template的文件名,即此处为index.html
template: 'templates/index.html',
injectOptions:
data:
// 替换模板的内容
,
// filename会用于路径匹配
// path模式下正则表达式为:
// `^\\\\/$filename(\\\\?\\w.*|\\\\/[^\\\\.]*)?$`
// 与之相对的是query模式,见下方pageKey
// 允许不带.html后缀,导出时会自动补上,放在dist目录下
// 不带.html后缀部分会作为build.rollupOptions.input的键名,即output的[name]
filename: 'station',
// entry是相对于filename的,插件中只处理了分隔符,即"/"符号
// 此处可以理解为在项目根路径下有一个虚拟的station.html
// html文档里有一个script[type=module]标签,其src即为entry值
entry: 'station/MP/main.js',
template: 'templates/MP.html',
injectOptions:
data:
// 替换模板的内容
,
]
return defineConfig(
// 其他配置
build:
rollupOptions:
// 如前文所述,pages中的页面不需要填入input,插件内部会处理
input:,
output:
entryFileNames:'/[name].js',
chunkFileNames:'/[name].js',
assetFileNames:'[name].[ext]'
,
,
plugins:[
createHtmlPlugin(
minify:false,
pages,
// 开发环境中,如果希望通过query参数区分页面,则需要传入pageKey
// 如下启用query模式后,插件会根据query中的app的值查找页面:
// `^\\\\/[^#\\\\\\?]*?[\\\\?&]app=$filename(&.+|#\\\\\\/.*)?`
pageKey:'app'
)
]
)
插件源码
import normalizePath,loadEnv,createFilter from 'vite';
// 下面四个模块需要安装一下,相比原版删了几个不必要的
// 最搞笑的明明开发的是一个vite插件,不知道为什么原作者重复安装了一个vite已经提供的方法createFilter
import render from 'ejs';
import parse from 'node-html-parser';
import minify from 'html-minifier-terser';
import path from 'pathe';
const fs=require('fs');
function createPlugin(entry,template='./index.html',pages=[],verbose=false,inject=,pageKey='')
const env=loadEnv(process.env.NODE_ENV,process.cwd()),
rewrites=[];
let viteConfig;
return
name: "vite:html",
enforce: "pre",
// 组建重写映射,并将filename添加到vite的input中
config(conf)
const filename=path.basename(template);
// 如果没有配置pages,则把根路径重定向到默认模板
// 允许input有多个入口,只要不和filename重名就行
// 这些html都在transform钩子中使用公共配置
if(!pages?.length)
const to = path.resolve(conf.root, template);
rewrites.push(from:new RegExp('^\\\\/$'),to);
return
build:
rollupOptions:
input:
[filename.replace(/\\.html/,'')]: to
;
let getRegStr,getInputStr,indexPage=null;
if(pageKey)
getRegStr=page=>`^\\\\/[^#\\\\\\?]*?[\\\\?&]$pageKey=$page(&.+|#\\\\\\/.*)?`;
getInputStr=page=>'/?'+pageKey+'='+page;
else
getRegStr=page=>`^\\\\/$page(\\\\?\\w.*|\\\\/[^\\\\.]*)?$`;
getInputStr=page=>'/'+page;
const input = ;
pages.forEach(page=>
const to=...page;
if(!to.template) to.template=template;
if(!to.filename) to.filename=path.basename(filename);
if (to.filename !== 'index.html'&&to.filename !== 'index')
rewrites.push(from:new RegExp(getRegStr(to.filename.replaceAll('.','\\\\.'))),to);
input[to.filename.replace(/\\.html/,'')]=getInputStr(to.filename);
else
indexPage = to;
if(!to.filename.endsWith('.html')) to.filename+='.html';
);
if(indexPage)
rewrites.push(from:new RegExp('^\\\\/(index\\\\.html)?$'),to:indexPage);
input.index='/index.html';
return
build:
rollupOptions:
input
;
,
configResolved(resolvedConfig)
viteConfig = resolvedConfig;
,
configureServer(server)
const baseUrl=viteConfig.base??'/',
proxyKeys=viteConfig.server?.proxy?Object.keys(viteConfig.server.proxy):[];
server.middlewares.use((rqst, resp, next)=>
if(!['GET','HEAD'].includes(rqst.method)||!rqst.headers) return next();
const headers = rqst.headers;
if(typeof headers.accept!=='string'||!["text/html","application/xhtml+xml"].some(accept=>headers.accept.includes(accept))) return next();
const parsedUrl = rqst._parsedUrl,
rewrite=rewrites.find(r=>parsedUrl.path.match(r.from));
if (!rewrite)
if(parsedUrl.pathname.lastIndexOf('.')<=parsedUrl.pathname.lastIndexOf('/')) rqst.url='/index.html';
return next();
if(typeof rewrite.to==='string')
rqst.url=rewrite.to;
return next();
// 遗留内容,貌似没什么用
if(proxyKeys.some(k=>parsedUrl.pathname.startsWith(path.resolve(baseUrl,k))))
rqst.url=parsedUrl.pathname.replace(baseUrl,'/');
return next();
// 调用resp的end或write方法会直接把数据发给浏览器
// 因此不会再触发transformIndexHtml钩子,需要手动调用
server.transformIndexHtml(
path.resolve(baseUrl,rewrite.to.filename),
fs.readFileSync(path.resolve(viteConfig.root,rewrite.to.template)).toString()
).then(html=>resp.end(html));
);
,
// rollup钩子,获取文件地址
resolveId(source,importer)
const rewrite=rewrites.find(r=>source.match(r.from));
if(!rewrite) return null;
if(typeof rewrite.to==='string') return rewrite.to;
return path.resolve(viteConfig.root,rewrite.to.filename);
,
// rollup钩子,根据文件地址读取文件内容
load(id)
if(typeof id!=='string') return null;
const rewrite=rewrites.filter(r=>typeof r.to!=='string')
.find(r=>path.resolve(viteConfig.root,r.to.filename)===id);
return rewrite?fs.readFileSync(path.resolve(viteConfig.root,rewrite.to.template)).toString():null;
,
// vite特有钩子,填充html文件插槽
transformIndexHtml:
enforce:'pre',
async transform(html, ctx)
let injectOptions,pageEntry;
const rewrite=rewrites.filter(r=>typeof r.to!=='string')
.find(r=>path.resolve(viteConfig.root,r.to.filename)===ctx.filename);
if(rewrite)
injectOptions=rewrite.to.injectOptions||;
pageEntry=rewrite.to.entry||entry;
else
injectOptions=inject;
pageEntry=entry;
html=await render(
html,
...viteConfig?.env ?? ,
...viteConfig?.define ?? ,
...env || ,
...injectOptions.data
,
injectOptions.ejsOptions
);
if(pageEntry)
const root=parse(html),
scriptNodes=root.querySelectorAll('script[type=module]');
if(scriptNodes?.length)
const removedNode=scriptNodes.map(item =>
item.parentNode.removeChild(item);
return item.toString();
);
if(verbose) console.warn(`vite-plugin-html: Since you have already configured entry, $removedNode.toString() is deleted. You may also delete it from the index.html.`);
html=root.toString().replace(/<\\/body>/,`<script type="module" src="$normalizePath(`$pageEntry`)"><\\/script>\\n</body>`);
return html, tags:injectOptions.tags||[] ;
,
;
const htmlFilter = createFilter(["**/*.html"]);
function getOptions(minify)
return
collapseWhitespace: minify,
keepClosingSlash: minify,
removeComments: minify,
removeRedundantAttributes: minify,
removeScriptTypeAttributes: minify,
removeStyleLinkTypeAttributes: minify,
useShortDoctype: minify,
minifyCSS: minify
;
async function minifyHtml(html, minify$1)
if (typeof minify$1 === "boolean" && !minify$1)
return html;
let minifyOptions = minify$1;
if (typeof minify$1 === "boolean" && minify$1)
minifyOptions = getOptions(minify$1);
return await minify(html, minifyOptions);
function createMinifyHtmlPlugin(minify = true = )
return
name: "vite:minify-html",
enforce: "post",
async generateBundle(_, outBundle)
if (minify)
for (const bundle of Object.values(outBundle))
if (bundle.type === "asset" && htmlFilter(bundle.fileName) && typeof bundle.source === "string")
bundle.source = await minifyHtml(bundle.source, minify);
;
export default (userOptions = ) =>
return [createPlugin(userOptions), createMinifyHtmlPlugin(userOptions)];
// 本插件基于[https://github.com/vbenjs/vite-plugin-html]二次开发