魔改版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]二次开发

逻辑概要

Transformer提效之路——一文梳理各种魔改版本Transformer

超好用的网页栅格化工具: GridGuide

vite-plugin-html 插件

快手全民特效大赛与王者荣耀双IP联合,200+优质魔表作品助力项目实现超34亿曝光

天猫魔盒电视的时间怎么调

简书中Markdown编辑器的实用用法