0


【魔改版vite-plugin-html】超好用的vite HTML模板插件!

使用示例

// vite.config.jsimport createHtmlPlugin from'./vite-plugin-html.js'exportdefaultasync()=>{// 前置处理const pages=[{// 默认的filename是template的文件名,即此处为index.htmltemplate:'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:{// 替换模板的内容}}},]returndefineConfig({// 其他配置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已经提供的方法createFilterimport{ render }from'ejs';import{ parse }from'node-html-parser';import{ minify }from'html-minifier-terser';import path from'pathe';const fs=require('fs');functioncreatePlugin({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:newRegExp('^\\/$'),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:newRegExp(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:newRegExp('^\\/(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)returnnext();const headers = rqst.headers;if(typeof headers.accept!=='string'||!["text/html","application/xhtml+xml"].some(accept=>headers.accept.includes(accept)))returnnext();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';returnnext();}if(typeof rewrite.to==='string'){
          rqst.url=rewrite.to;returnnext();}// 遗留内容,貌似没什么用if(proxyKeys.some(k=>parsedUrl.pathname.startsWith(path.resolve(baseUrl,k)))){
          rqst.url=parsedUrl.pathname.replace(baseUrl,'/');returnnext();}// 调用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)returnnull;if(typeof rewrite.to==='string')return rewrite.to;return path.resolve(viteConfig.root,rewrite.to.filename);},// rollup钩子,根据文件地址读取文件内容load(id){if(typeof id!=='string')returnnull;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',asynctransform(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=awaitrender(
          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"]);functiongetOptions(minify){return{collapseWhitespace: minify,keepClosingSlash: minify,removeComments: minify,removeRedundantAttributes: minify,removeScriptTypeAttributes: minify,removeStyleLinkTypeAttributes: minify,useShortDoctype: minify,minifyCSS: minify
  };}asyncfunctionminifyHtml(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);}returnawaitminify(html, minifyOptions);}functioncreateMinifyHtmlPlugin({minify =true}={}){return{name:"vite:minify-html",enforce:"post",asyncgenerateBundle(_, outBundle){if(minify){for(const bundle of Object.values(outBundle)){if(bundle.type ==="asset"&&htmlFilter(bundle.fileName)&&typeof bundle.source ==="string"){
            bundle.source =awaitminifyHtml(bundle.source, minify);}}}}};}exportdefault(userOptions ={})=>{return[createPlugin(userOptions),createMinifyHtmlPlugin(userOptions)];}// 本插件基于[https://github.com/vbenjs/vite-plugin-html]二次开发

逻辑概要

#mermaid-svg-cAQk6DgHpj3BFWQs {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-cAQk6DgHpj3BFWQs .error-icon{fill:#552222;}#mermaid-svg-cAQk6DgHpj3BFWQs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cAQk6DgHpj3BFWQs .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-cAQk6DgHpj3BFWQs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cAQk6DgHpj3BFWQs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cAQk6DgHpj3BFWQs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cAQk6DgHpj3BFWQs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cAQk6DgHpj3BFWQs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cAQk6DgHpj3BFWQs .marker.cross{stroke:#333333;}#mermaid-svg-cAQk6DgHpj3BFWQs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cAQk6DgHpj3BFWQs .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cAQk6DgHpj3BFWQs .cluster-label text{fill:#333;}#mermaid-svg-cAQk6DgHpj3BFWQs .cluster-label span{color:#333;}#mermaid-svg-cAQk6DgHpj3BFWQs .label text,#mermaid-svg-cAQk6DgHpj3BFWQs span{fill:#333;color:#333;}#mermaid-svg-cAQk6DgHpj3BFWQs .node rect,#mermaid-svg-cAQk6DgHpj3BFWQs .node circle,#mermaid-svg-cAQk6DgHpj3BFWQs .node ellipse,#mermaid-svg-cAQk6DgHpj3BFWQs .node polygon,#mermaid-svg-cAQk6DgHpj3BFWQs .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cAQk6DgHpj3BFWQs .node .label{text-align:center;}#mermaid-svg-cAQk6DgHpj3BFWQs .node.clickable{cursor:pointer;}#mermaid-svg-cAQk6DgHpj3BFWQs .arrowheadPath{fill:#333333;}#mermaid-svg-cAQk6DgHpj3BFWQs .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cAQk6DgHpj3BFWQs .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cAQk6DgHpj3BFWQs .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-cAQk6DgHpj3BFWQs .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-cAQk6DgHpj3BFWQs .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cAQk6DgHpj3BFWQs .cluster text{fill:#333;}#mermaid-svg-cAQk6DgHpj3BFWQs .cluster span{color:#333;}#mermaid-svg-cAQk6DgHpj3BFWQs div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cAQk6DgHpj3BFWQs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}
dev

主动调用

response.end

build

钩子

钩子

config
构建重写映射

configureServer
通过server中间件劫持请求

resolveId匹配重写
重定向到虚拟文件

中间件回调
匹配重写

load根据虚拟文件
读取并返回模板html

读取模板html

transformIndexHtml
替换HTML插槽

响应浏览器

写在最后

这是我第一次写vite插件,但不是第一次改源码。作为非科班出身,缺乏基础功底始终是个硬伤,因此几乎每次遇到难题都是逆向解剖的方式层层抽剥,很艰难,很费时,但是每次都能有很多“精准的”收获。如果你也不是科班出身,并且时常会有一些缺少技能支撑的想法,别焦虑,请相信,眼前的问题最终都会一个一个解决的。

我是个人开发者,全栈程序员。后端语言PHP,喜欢用Codeigniter框架、swoole-cli。前端vue框架、微信小程序,欢迎交流!

标签: html 前端 vue.js

本文转载自: https://blog.csdn.net/warmbook/article/details/128767878
版权归原作者 warmbook 所有, 如有侵权,请联系我们删除。

“【魔改版vite-plugin-html】超好用的vite HTML模板插件!”的评论:

还没有评论