Skip to content

Vite 插件开发指南

前言

Vite 插件是为 Vite 构建工具编写的扩展,本质是在特定的时间点(生命周期钩子)去做相对应的事,可以在构建过程中修改默认行为、添加新功能或执行自定义操作。例如:

  • 自定义开发服务器
  • 左右构建过程
  • 代码转化
  • 特殊文件类型处理
  • 优化输出结果
  • ...

基于 vite 是基于 Rollup 实现的,它可以兼容一些 Rollup 插件,也可以使用 Rollup 通用的钩子之外,vite 自身还有特有的钩子

开发须知

使用 vite 创建项目

先创建

bash
pnpm create vite
pnpm create vite

进入目录,在创建例子

bash
cd projectName
pnpm create vite
cd projectName
pnpm create vite

使用vite创建项目

整理目录

分为插件源码、和例子源码 依赖相同可以共用一个 package.json 文件和共享项目配置(ts|eslint|gitignore)

添加、修改文件内容并执行

  • 安装 pnpm i vite-plugin-inspect -D
  • 修改 main.ts 内容,创建插件实例
  • 给 example 添加 vite.config.ts 文件,并导入 main.ts 创建的插件并使用
  • 修改 package.json scripts 命令

添加、修改文件内容并执行

ts
import type { PluginOption } from "vite";

const vitePluginTemplate = (): PluginOption => {
  return {
    name: "vite-plugin-template",
    // apply: "build" // 指定应用场景 build | serve
    buildStart(options) {
      console.log("开始构建", options);
    }
  };
};

export { vitePluginTemplate };
export default vitePluginTemplate;
import type { PluginOption } from "vite";

const vitePluginTemplate = (): PluginOption => {
  return {
    name: "vite-plugin-template",
    // apply: "build" // 指定应用场景 build | serve
    buildStart(options) {
      console.log("开始构建", options);
    }
  };
};

export { vitePluginTemplate };
export default vitePluginTemplate;
ts
import { defineConfig } from "vite";
import Inspect from "vite-plugin-inspect";
import vitePluginTemplate from "../src/main";
import path from "path";

export default defineConfig({
  input: "./example-module",
  plugins: [Inspect(), vitePluginTemplate()], // 项目中引入我们开发的插件
  resolve: {
    alias: {
      "@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
    }
  }
});
import { defineConfig } from "vite";
import Inspect from "vite-plugin-inspect";
import vitePluginTemplate from "../src/main";
import path from "path";

export default defineConfig({
  input: "./example-module",
  plugins: [Inspect(), vitePluginTemplate()], // 项目中引入我们开发的插件
  resolve: {
    alias: {
      "@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
    }
  }
});
json
{
  "name": "zebr-vite-plugin-template",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev:example": "vite dev example",
    "build:example": "vite build example",
    "preview:example": "vite preview example"
  },
  "devDependencies": {
    "typescript": "^5.2.2",
    "vite": "^5.1.6",
    "vite-plugin-inspect": "^0.8.3"
  }
}
{
  "name": "zebr-vite-plugin-template",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev:example": "vite dev example",
    "build:example": "vite build example",
    "preview:example": "vite preview example"
  },
  "devDependencies": {
    "typescript": "^5.2.2",
    "vite": "^5.1.6",
    "vite-plugin-inspect": "^0.8.3"
  }
}
点击查看代码

不想用 vite 创建也可以手动搭建

可以单独创建一个目录 xxx

初始化 package.json pnpm init

安装依赖 pnpm i vite typescript

创建 index.js 或 index.ts 文件来写插件代码

只要在 vite 项目中的 vite.config 就能导入使用

创建例子目录 xxx/example,方便测试

xxx/example/index.html 中引入 xxx/example/src/index.js

xxx/example/vite.config.ts 导入 xxx/index.js 使用

Rollup 通用钩子 🪝

  1. options 在服务器启动时被调用
  2. buildStart在服务器启动时被调用
  3. resolveId在每个传入模块请求时被调用
  4. load在每个传入模块请求时被调用
  5. transform在每个传入模块请求时被调用
  6. buildEnd在服务器关闭时被调用
  7. closeBundle在服务器关闭时被调用
  8. moduleParsed在开发中是 不会 被调用的,因为 Vite 为了性能会避免完整的 AST 解析。
ts
const vitePluginTemplate = () => {
  return {
    name: "vite-plugin-template",
    // rollup 通用钩子
    options() {
      console.log("1.options: 在服务器启动时被调用");
    },
    buildStart() {
      console.log("2.buildStart: 在服务器启动时被调用");
    },
    resolveId() {
      console.log("3.resolveId: 在每个传入模块请求时被调用");
    },
    load() {
      console.log("4.load: 在每个传入模块请求时被调用");
    },
    transform() {
      console.log("5.transform: 在每个传入模块请求时被调用");
    },
    buildEnd() {
      console.log("6.buildEnd: 在服务器关闭时被调用");
    },
    closeBundle() {
      console.log("7.closeBundle: 在服务器关闭时被调用");
    },
    moduleParsed() {
      console.log(
        "8.moduleParsed: 在开发中是 不会 被调用的,因为 Vite 为了性能会避免完整的 AST 解析。"
      );
    }
  };
};
export { vitePluginTemplate };
export default vitePluginTemplate;
const vitePluginTemplate = () => {
  return {
    name: "vite-plugin-template",
    // rollup 通用钩子
    options() {
      console.log("1.options: 在服务器启动时被调用");
    },
    buildStart() {
      console.log("2.buildStart: 在服务器启动时被调用");
    },
    resolveId() {
      console.log("3.resolveId: 在每个传入模块请求时被调用");
    },
    load() {
      console.log("4.load: 在每个传入模块请求时被调用");
    },
    transform() {
      console.log("5.transform: 在每个传入模块请求时被调用");
    },
    buildEnd() {
      console.log("6.buildEnd: 在服务器关闭时被调用");
    },
    closeBundle() {
      console.log("7.closeBundle: 在服务器关闭时被调用");
    },
    moduleParsed() {
      console.log(
        "8.moduleParsed: 在开发中是 不会 被调用的,因为 Vite 为了性能会避免完整的 AST 解析。"
      );
    }
  };
};
export { vitePluginTemplate };
export default vitePluginTemplate;
点击查看代码

vite 独有的钩子 🪝

config

(config: UserConfig, env: { mode: string, command: string, ssrBuild: boolean }) => UserConfig | null | void

参数类型描述
configUserConfig 用户 vite.config 配置
env{ mode: string, command: string, ssrBuild: boolean }运行环境变量

解析 vite 配置前调用,常用于根据环境修改 vite 配置,有两种修改方法:

  • 赋值一个匿名函数,返回对象进行合并
  • 直接修改 config 参数,并返回 config
js
// 返回部分配置(推荐)
const partialConfigPlugin = () => ({
  name: "return-partial",
  config: () => ({
    resolve: {
      alias: {
        foo: "bar"
      }
    }
  })
});

// 直接改变配置(应仅在合并不起作用时使用)
const mutateConfigPlugin = () => ({
  name: "mutate-config",
  config(config, { command }) {
    if (command === "build") {
      config.root = "foo";
    }
  }
});
// 返回部分配置(推荐)
const partialConfigPlugin = () => ({
  name: "return-partial",
  config: () => ({
    resolve: {
      alias: {
        foo: "bar"
      }
    }
  })
});

// 直接改变配置(应仅在合并不起作用时使用)
const mutateConfigPlugin = () => ({
  name: "mutate-config",
  config(config, { command }) {
    if (command === "build") {
      config.root = "foo";
    }
  }
});

configResolved

(config: ResolvedConfig) => void | Promise<void>

参数类型描述
configResolvedConfig 最终 vite 配置

解析完 vite 配置时调用,常用于获取、存储最终的配置,后续其他钩子根据配置进行操作时,他很有用。

js
const examplePlugin = () => {
  let config; // 1.创建存储变量

  return {
    name: "read-config",

    configResolved(resolvedConfig) {
      // 2.存储最终解析的配置
      config = resolvedConfig;
    },

    // 3.在其他钩子中使用存储的配置
    transform(code, id) {
      if (config.command === "serve") {
        // dev: 由开发服务器调用的插件
      } else {
        // build: 由 Rollup 调用的插件
      }
    }
  };
};
const examplePlugin = () => {
  let config; // 1.创建存储变量

  return {
    name: "read-config",

    configResolved(resolvedConfig) {
      // 2.存储最终解析的配置
      config = resolvedConfig;
    },

    // 3.在其他钩子中使用存储的配置
    transform(code, id) {
      if (config.command === "serve") {
        // dev: 由开发服务器调用的插件
      } else {
        // build: 由 Rollup 调用的插件
      }
    }
  };
};

configureServer

(server: ViteDevServer) => (() => void) | void | Promise<(() => void) | void>

参数类型描述
serverViteDevServer 开发服务器实例

是用于配置开发服务器的钩子。最常见的用例是在内部 connect 应用程序中添加自定义中间件:

此外请看:ViteDevServer

js
const myPlugin = () => {
  let _serve = null;

  return {
    name: "configure-server",
    configureServer(server) {
      // 在某些情况下,其他插件钩子可能需要访问开发服务器实例(例如访问 websocket 服务器、文件系统监视程序或模块图)。这个钩子也可以用来存储服务器实例以供其他钩子访问:
      console.log("configureServer: 是用于配置开发服务器的钩子");
      _serve = server;
      return () => {
        server.middlewares.use((req, res, next) => {
          // 自定义请求处理...
          const startTime = Date.now();
          res.on("finish", () => {
            const endTime = Date.now();
            const duration = endTime - startTime;
            console.log(`Request ${req.url} took ${duration}ms to process.`); // Request /index.html took 12ms to process.
          });
          next();
        });
      };
    },
    transform(code, id) {
      if (_serve) {
        // 使用 _serve...
      }
    }
  };
};
const myPlugin = () => {
  let _serve = null;

  return {
    name: "configure-server",
    configureServer(server) {
      // 在某些情况下,其他插件钩子可能需要访问开发服务器实例(例如访问 websocket 服务器、文件系统监视程序或模块图)。这个钩子也可以用来存储服务器实例以供其他钩子访问:
      console.log("configureServer: 是用于配置开发服务器的钩子");
      _serve = server;
      return () => {
        server.middlewares.use((req, res, next) => {
          // 自定义请求处理...
          const startTime = Date.now();
          res.on("finish", () => {
            const endTime = Date.now();
            const duration = endTime - startTime;
            console.log(`Request ${req.url} took ${duration}ms to process.`); // Request /index.html took 12ms to process.
          });
          next();
        });
      };
    },
    transform(code, id) {
      if (_serve) {
        // 使用 _serve...
      }
    }
  };
};

TIP

插入中间件在其他中间件安装前调用,如果想要在其他中间件安装后调用,请返回一个函数。

注意 configureServer 在运行生产版本时不会被调用,所以其他钩子需要防范它缺失。

configurePreviewServer

(server: PreviewServer) => (() => void) | void | Promise<(() => void) | void>

参数类型描述
serverViteDevServer 预览服务器实例

与 configureServer 相同,但用于预览服务器。也是最前面调用,最后调用也是返回一个函数封装调用。

此外参见:PreviewServerForHook

js
const myPlugin = () => ({
  name: "configure-server",
  configureServer(server) {
    // 返回一个在内部中间件安装后
    // 被调用的后置钩子
    return () => {
      server.middlewares.use((req, res, next) => {
        // 自定义请求处理...
      });
    };
  }
});
const myPlugin = () => ({
  name: "configure-server",
  configureServer(server) {
    // 返回一个在内部中间件安装后
    // 被调用的后置钩子
    return () => {
      server.middlewares.use((req, res, next) => {
        // 自定义请求处理...
      });
    };
  }
});

transformIndexHtml

专门处理 index.html 的钩子,常用于 获取 html 文件、修改 html 内容,标题、icon 等...

js
const htmlPlugin = () => {
  return {
    name: "html-transform",
    transformIndexHtml(html) {
      return html.replace(/<title>(.*?)<\/title>/, `<title>Title replaced!</title>`);
    }
  };
};
const htmlPlugin = () => {
  return {
    name: "html-transform",
    transformIndexHtml(html) {
      return html.replace(/<title>(.*?)<\/title>/, `<title>Title replaced!</title>`);
    }
  };
};

handleHotUpdate

hmr 热更新时调用,可以查看更新的文件和影响对应的模块

js
handleHotUpdate({ server }) {
  console.log("handleHotUpdate: 执行自定义 HMR 更新处理, 更新文件为:", payload.file);
  // payload.file =>  /Users/linsen/code/vite-plugin-template/example/src/index.ts

  // 如果需要与客户端通信,也可使用 `server.ws.send` 来支持 Vite <5.1 版本>
  server.ws.send({
    type: 'custom',
    event: 'special-update',
    data: {}
  })
  return []
}
handleHotUpdate({ server }) {
  console.log("handleHotUpdate: 执行自定义 HMR 更新处理, 更新文件为:", payload.file);
  // payload.file =>  /Users/linsen/code/vite-plugin-template/example/src/index.ts

  // 如果需要与客户端通信,也可使用 `server.ws.send` 来支持 Vite <5.1 版本>
  server.ws.send({
    type: 'custom',
    event: 'special-update',
    data: {}
  })
  return []
}
js
console.log("update code 2"); // 修改保存触发更新

// 与开发服务器通信
if (import.meta.hot) {
  import.meta.hot.on("special-update", (data) => {
    // 执行自定义更新
    console.log(data, "special-update");
  });
  import.meta.hot.on("my:ack", (data) => {
    console.log(data.msg); // hello
  });
  import.meta.hot.send("my:from-client", { msg: "Hey!" });
}
console.log("update code 2"); // 修改保存触发更新

// 与开发服务器通信
if (import.meta.hot) {
  import.meta.hot.on("special-update", (data) => {
    // 执行自定义更新
    console.log(data, "special-update");
  });
  import.meta.hot.on("my:ack", (data) => {
    console.log(data.msg); // hello
  });
  import.meta.hot.send("my:from-client", { msg: "Hey!" });
}