vite实现插件

发布时间 2023-11-17 17:35:05作者: 柯基与佩奇

准备

为了方便插件开发,这里就先做简单点,在项目根目录建立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 文件,解析出来的文件如下:

image.png

然后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秒")})`
            )
          )
        );
      }
    },
  };
}

image.png