likes
comments
collection
share

十八.vite之插件实现篇

作者站长头像
站长
· 阅读数 35

前言

在上一篇中我们简单了解了一下Vite插件的基本概念和规范,这篇我们开始对vite插件进行实战,对插件钩子的基本用法进行一定的了解。

源代码放在vue3-basic-admin,该项目是一款开源开箱即用的中后台管理系统。基于 Vue3ViteElement-PlusTypeScriptPinia 等主流技术开发,内置许多开箱即用的组件,能快速构建中后台管理系统。

准备

如还没看过Vite的插件基本概念,可查看上一篇文章和项目实战项目:

为了方便插件开发,这里就先做简单点,在项目根目录建立build文件夹,里面存放一些我们自定义的插件。

# build/test.js
export function testPlugin(){
    return {
        //插件名字
        name:'vite-plugin-test',
        options(options) {

        },
        buildStart(options){
            console.log(options)
        },
    }
}

使用插件:

# vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {testPlugin} from "./build/test"

export default defineConfig({
  plugins: [vue(),testPlugin()],
})

resolveId钩子

resolveId 钩子可以获取文件路径,在该钩子下可以对文件路径进行重写或其他操作。例如:通过该钩子来实现alias操作。

import path from "path"

export function testPlugin(){
    return {
        //插件名字
        name:'vite-plugin-test',
        resolveId(id) {
           if (id.startsWith('@')) {
              return id.replace('@',path.resolve(process.cwd(), 'src'))
           }
        },
    }
}

load钩子

load钩子可拦截文件读取,模块引入读取操作,例如我想拦截某个组件读取,这样写后,对应的组件里面的内容加载时就会被替换成code的内容。

export function testPlugin(){
    return {
        //插件名字
        name:'vite-plugin-test',
        load(id) {
            if (id.includes('HelloWorld.vue')) {
              return {
                code: `<template>1111</template>`,
                map: '' 
              }
            }
         }
    }
}

我们还可以通过resolveId钩子+load钩子去实现加载虚拟模块功能,例如:页面中引入了一个不存在的模块

import test from "virtual:test-modules"

我们可以通过resolveId钩子+load钩子去实现这个虚拟模块。

这里提一下,Rollup定义的规范,如果插件是以\0开头,则会跳过解析

const moduleName="virtual:test-modules"

export function testPlugin(){
    return {
        //插件名字
        name:'vite-plugin-test',
        resolveId(id) { 
           if (id === moduleName) {
              return `\0${id}`
           } 
        },
        load(id) {
            if (id===`\0${moduleName}`) {
              return {
                code: `const data = '虚拟test-modlues常量'; \n export default data; `,
                map: '' 
              }
            }
         }
    }
}
import test from "virtual:test-modules"
console.log(test)  //虚拟test-modlues常量

transform钩子

transform钩子可以转换单个模块对应的代码,和load钩子不同的是,他有两个参数,codeid,这里的id和上面一样,这个code就是他对应的代码。例如我们这里通过这个钩子去实现setup添加name功能。

主要实现逻辑:通过 transform钩子拿到后缀为.vue的文件,然后拖过模板解析工具解析该文件是否存在setup,如果存在且setup上存在name,则给code再添加一个script标签,这样即可页面上仅需setup上添加name,其他的交给插件去处理。

题外话,这里要用到一个工具:@vue/compiler-sfc,这个工具主要是去编译vue文件,它对外暴露了:compileTemplatecompileStyle, compileScriptparse等方法,其中 parse 方法主要是解析vue文件,解析出来的文件如下:

十八.vite之插件实现篇

然后compileTemplatecompileStyle, compileScript分别是处理templatestylescript的工具,将对应的语法转换成浏览器能识别的代码。

export function testPlugin(){
    return {
        //插件名字
        name:'vite-plugin-test',
        enforce: "pre",
        transform(code,id){
           // 过滤文件格式
           if (/\.vue$/.test(id) || /\.vue\?.*type=script.*/.test(id)) {
                // 解析文件
                const { descriptor } = parse(code);
                // 判断是否存在setup标签
                if (!descriptor.script && descriptor.scriptSetup 
                    && !descriptor.scriptSetup.attrs?.extendIgnore
                 ) {
                    
                    const result = compileScript(descriptor, { id });
                    // 拿到setup上的name属性
                    const name = result.attrs.name;
                    const lang = result.attrs.lang;
                    const inheritAttrs = result.attrs.inheritAttrs;
                    //如果name存在,则创建一个script标签,然后添加到code上
                    if (name || inheritAttrs) {
                        const template = `<script${lang ? ` lang="${lang}"` : ""}>
                        import { defineComponent } from 'vue'
                        export default defineComponent({
                          ${name ? `name: "${name}",` : ""}
                          ${inheritAttrs ? `inheritAttrs: ${inheritAttrs !== "false"},` : ""}
                        })
                        </script>\n`;
                        code += template;
                    }
                }
            }
            return code;
        }
    }
}

transformIndexHtml钩子

transformIndexHtml钩子主要是操作index.html文件,它会返回当前的html文件,这里可以通过该钩子实现一个简单的 修改index.htmltitle的插件。

export function testPlugin({title=''}){
    return {
        //插件名字
        name:'vite-plugin-test',
        enforce: "pre",
        transformIndexHtml(html){
            return html.replace(/<title>(.*?)<\/title>/,`<title>${title}</title>`)
        }
    }
}

使用:

# vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {testPlugin} from "./build/test"

export default defineConfig({
  plugins: [vue(),testPlugin({title:'我是测试插件名字'})],
})

closeBundle钩子

closeBundle 钩子是打包后最后执行的钩子,他和buildEnd的区别是,它是打包代码生成后触发的钩子。通过该钩子我们可以写一个统计打包用时的插件。

import picocolors from "picocolors";
import dayjs from "dayjs";
import duration from 'dayjs/plugin/duration' // 按需加载插件
dayjs.extend(duration) // 使用插件
const { green, blue, bold } = picocolors;

export function testPlugin(){
    let config;
    let startTime;
    let endTime;
    
    return {
        //插件名字
        name:'vite-plugin-test',
        //获取最终vite配置
        configResolved(resolvedConfig) {
            config = resolvedConfig;
        },
        //打包开始钩子
        buildStart(){
            console.log(bold(green(`👏欢迎使用系统,现在正全力为您${config.command === "build" ? "打包" : "编译"}`)));
             if (config.command === "build") {
               //如果当前是build 环境
                startTime = dayjs(new Date());
            }
        },
        closeBundle(){
         if (config.command === "build") {
                endTime = dayjs(new Date());
                console.log(bold(green(`恭喜打包完成🎉(总用时${dayjs.duration(endTime.diff(startTime)).format("mm分ss秒")})`)));
             }
        }
    }
}

十八.vite之插件实现篇

总结

以上就是Vite插件的一些简单实战,还是挺有意思的,学会了后可以自己写一些插件,然后发布到npm上,这样vite的生态会越来越强大,如有问题可以评论区留言,大家一起共同进步。

其他文章

转载自:https://juejin.cn/post/7210278786592292920
评论
请登录