还没了解过 Vite 的 plugin?手写一个 plugin 带你熟悉 API

2,495 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第25天,点击查看活动详情

Plugin

如果说你没有对前端模块化有过了解,不了解 Webpackplugin,可以阅读这个章节了解 Plugin 的前世今生~

在上一代打包工具 browserify 的生涯末期,Webpack 的带着自己的代码分割和其它性能优化创建了新一代的打包工具时代,但与此同时 Webpack 使用暴露整个编译生命周期的钩子函数给予第三方插件(plugin)的设计也大大丰富了 Webpack 系的生态,而如今基于 ESM 模块标准的新一代打包工具的 Vite 也沿用了 Webpack 的插件设计,那么两者的 plugin 有什么区别呢?

答案就是没有区别,至少在暴露钩子函数这块的设计上是没有的

硬要扯点就是 Webpackplugin 写法是在 apply() 中对 compiler 的一系列 hook 做绑定,而 Vite 则是做了简化,可以直接在对应的钩子上书写逻辑,大致如下

IMG

为啥 Vite 里面没有 loader 从功能上来说,Webpackplugin 本质上是可以替代 loader 的工作的(不过会麻烦一点),可能出于某些考量,尤大就放弃多设计一个 loader 的入口了吧?

编写

开始上手!你可以在线查看源代码

202212071826163.png

Vite 的文档中建议我们开发的时候用了 Vite 钩子函数的插件统一叫 vite-plugin-[name],而相应的 plugin 就用一个 .js 文件便于表示,插件如下

export default function vitePluginPrintName() {
  return {
    name: 'transform-file',
    // ...
  }
}

其实就是一个函数,JavaScript 的一等公民,使用也很简单,在 vite.config.js 直接导入就行

import { defineConfig } from "vite";
// ...
import vitePluginPrintName from "./plugin/vite-plugin-print-name";
// ...
export default defineConfig({
  // ...
  plugins: [..., vitePluginPrintName()],
});

Vite 专属钩子

重点介绍一下 Vite 的专属钩子,一共有下面几个,一共是 6

  1. config
  2. configResolved
  3. configureServer
  4. configurePreviewServer
  5. transformIndexHtml
  6. handleHotUpdate

config 是负责和合并用户自定义配置和插件配置,而 configResolved 则代表最终配置,比如

export default function vitePluginPrintName() {
  let config = undefined;

  return {
    name: "vite-plugin-print-name",
    config: (config, env) => {},
    configResolved: (resolvedConfig) => {
      config = resolvedConfig;
    },
    transform(code, id) {
      if (config.mode === "development") {
        // 开发环境
      } else {
        // 生产环境
      }
    },
  };
}

通过一个闭包来判断 Vite 此时的环境,在 Vite 的插件示例中,有很多这样的设计

transformIndexHtml 则是负责 index.html 的转换(即根目录下的默认 html 模板),比如

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

中间件

这是两个非常有意思的钩子函数,configureServerconfigurePreviewServer 两个钩子函数是一样的作用,和 express 的中间件设计类型一致,如下

const myPlugin = () => ({
  name: 'configure-server',
  configureServer(server) {
    server.middlewares.use((req, res, next) => {
      // 自定义请求处理...
    })
  },
})

由于 Vite 采用的是 ESM 所以每个模块的调用都会请求一次,所以可以在中间件中监听模块

注意 configureServer 只会执行一次,而其中定义的中间件每次引用模块时都会执行

除了中间件以外,Vite 还提供了和客户端通信的方法

客户端与服务端间通信

configureServer 钩子函数中的参数 server 里面还有一个 ws 属性,其实就是 websocketwebpackvite 对于热更新的做法都是使用的 websocket,而 vite 对于双端通信的做法就是挂载 import.meta.hot 到全局对象,然后通过监听事件通信

// vite.config.js
export default defineConfig({
  plugins: [
    {
      // ...
      configureServer(server) {
        server.ws.send('my:greetings', { msg: 'hello' })
      },
    },
  ],
})

// client side
if (import.meta.hot) {
  import.meta.hot.on('my:greetings', (data) => {
    console.log(data.msg) // hello
  })
}

transform

transformloadresolveIdvite 中的通用函数,loadresolveId 是负责处理模块标识符的,也就是最终在磁盘 I/O 的路径,比如

D:/fe-project/vite-project/vue2-start-with-vite/node_modules/.vite/deps/vue.js?v=5417f592

而文章实现的 plugin 功能就是输出源代码中每个模块的调用顺序,即 src 目录下每个模块的调用顺序,实现如下

export default function vitePluginPrintName() {
  let config = undefined;
  
  return {
    name: "vite-plugin-print-name",
    configResolved: (resolvedConfig) => {
      config = resolvedConfig;
    },
    transform(code, id) {
      if (config.mode === "development") {
        const rootPath = process.cwd().replace(/\\/g, "/");
        if (id.indexOf(rootPath + "/src") !== -1) {
          console.log(id.replace(rootPath + "/src", ""));
        }
      }
    },
  };
}

效果如下

参考资料

  1. plugin - Webpack Doc
  2. compiler 钩子 - Webpack Doc
  3. 插件 API - Vite 官方中文文档