vite 中的插件

前言

昨天看了在B站的一个关于 vite 插件的视频(找不到了地址了,找到了补上),一直以来也没写过 vite 插件,今天来看看 vite 插件的编写,做个记录

插件

对于 vite 中的插件,应该都不陌生,在使用 vite 创建 vue3 应用时,官方会自动添加一个 vue 插件

ts 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

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

最常见的是 element-plus 中的自定义插件

可以看到,无论是官方的还是第三方的都使用了 函数 来编写插件

官方 vite 插件🔗的链接,有兴趣的可以直接点击查看详情

总的来说,插件的核心在于 钩子函数 的使用

通用钩子

以下钩子在服务器启动时被调用:

以下钩子会在每个传入模块请求时被调用:

以下钩子在服务器关闭时被调用:

vite 独有钩子

Vite 插件也可以提供钩子来服务于特定的 Vite 目标。这些钩子会被 Rollup 忽略

config

可以接收 用户传进来的 options 和 环境变量,包含正在使用的 modecommand,可以深度合并配置对象,也可以直接改变配置

ts 复制代码
(config: UserConfig, env: { mode: string, command: string }) => UserConfig | null | void 
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'
    }
  },
})

configResolved

在解析 Vite 配置后调用。使用这个钩子读取和存储最终解析的配置

在下面的例子中,由于 transform 拿不到 config 钩子后的值,可以使用 configResolved 钩子

js 复制代码
const examplePlugin = () => {
  let config 

  return {
    name: 'read-config',

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

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

configureServer

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

更多配置项请看 ViteDevServer

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

注入后置中间件

configureServer 钩子将在内部中间件被安装前调用,所以自定义的中间件将会默认会比内部中间件早运行。

如果你想注入一个在内部中间件 之后 运行的中间件,你可以从 configureServer 返回一个函数,将会在内部中间件安装后被调用:

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

handleHotUpdate

执行自定义 HMR 更新处理。钩子接收一个带有以下签名的上下文对象:

js 复制代码
interface HmrContext {
  file: string
  timestamp: number
  modules: Array<ModuleNode>
  read: () => string | Promise<string>
  server: ViteDevServer
}
  • modules 是受更改文件影响的模块数组。它是一个数组,因为单个文件可能映射到多个服务模块(例如 Vue 单文件组件)。
  • read 这是一个异步读函数,它返回文件的内容。

钩子可以选择:

  • 过滤和缩小受影响的模块列表,使 HMR 更准确。
  • 返回一个空数组,并通过向客户端发送自定义事件来执行完整的自定义 HMR 处理:
js 复制代码
handleHotUpdate({ server }) {
  server.ws.send({
    type: 'custom',
    event: 'special-update',
    data: {}
  })
  return []
}

客户端代码应该使用 HMR API 注册相应的处理器(这应该被相同插件的 transform 钩子注入)

js 复制代码
if (import.meta.hot) {
  import.meta.hot.on('special-update', (data) => {
    // 执行自定义更新
  })
}

🚅 resolveId

这个钩子属于 rollupjs ,中文文档,下文有例子

✈️ transform

这个钩子属于 rollupjs ,中文文档,下文有例子

可以被用来转换单个模块。这个钩子很重要,后面例子中会使用

插件顺序

一个 Vite 插件可以额外指定一个 enforce 属性(类似于 webpack 加载器)来调整它的应用顺序。enforce 的值可以是 prepost。解析后的插件将按照以下顺序排列:

  • 带有 enforce: 'pre' 的用户插件
  • Vite 核心插件
  • 没有 enforce 值的用户插件
  • Vite 构建用的插件
  • 带有 enforce: 'post' 的用户插件

情景应用

默认情况下插件在 开发(serve)和构建(build) 模式中都会调用。

如果插件只需要在预览或构建期间有条件地应用,请使用 apply 属性指明它们仅在 'build''serve' 模式时调用:

js 复制代码
function myPlugin() {
  return {
    name: 'build-only',
    apply: 'build' // 或 'serve'
  }
}

同时,还可以使用函数来进行更精准的控制:

js 复制代码
apply(config, { command }) {
  // 非 SSR 情况下的 build
  return command === 'build' && !config.build.ssr
}

钩子测试

config/configResolved

vite.config.ts 中定义函数 TestPlugin

ts 复制代码
export default defineConfig({
  plugins: [vue(),TestPlugin({name:"tsk"})],
})

实现 TestPlugin 这个测试插件

先看看 configconfigResolved 这两个钩子

ts 复制代码
import type { PluginOption } from "vite";

export default function TestPlugin(options:any): PluginOption {
  console.log("🚀 ~ file: replaceKeyword.ts:4 ~ Test ~ options:", options);
  return {
    name: "TestPlugin",
    enforce: "pre",
    
    // config 钩子
    config(config,env){
      console.log("🚀 ~ file: replaceKeyword.ts:8 ~ config ~ env:", env);
      console.log("🚀 ~ file: replaceKeyword.ts:8 ~ config ~ config:", config);
    },
   
   // configResolved 钩子
    configResolved(resolvedConfig) {
      console.log(
        "🚀 ~ file: replaceKeyword.ts:11 ~ configResolved ~ resolvedConfig:",
        resolvedConfig
      );
    },
  };
}

可以看出,config 中的 env 返回了 modecommand

其中 command 是值运行时机,可以选择serve 或者 build, 如果不指定,默认是开发和打包都会执行执行

如果指定属性 applybuild ,那么开发时就不会执行钩子函数

js 复制代码
 apply:"build",
 
 // 开发环境不会执行
  config(config,env){
     console.log("🚀 ~ file: replaceKeyword.ts:8 ~ config ~ env:", env);
     console.log("🚀 ~ file: replaceKeyword.ts:8 ~ config ~ config:", config);
  },

configureServer

js 复制代码
export default function TestPlugin(): PluginOption {
  return {
    name: "TestPlugin",
    enforce: "pre",
    
   configureServer(server){
    server.middlewares.use((req,res,next) => {
      console.log("🚀 ~ file: replaceKeyword.ts:11 ~ server.middlewares.use ~ req:", req);
      return next()
    }
    )
   }
  };
}

configureServer 接收一个 ViteDevServer🔗 对象,其中middlewares对象类似于 express 的中间键

🚅resolveId

ts 复制代码
resolveId(source, importer) {
      console.log(
        "🚀 ~ file: replaceKeyword.ts:8 ~ resolveId ~ importer:",
        importer
      );
      console.log(
        "🚀 ~ file: replaceKeyword.ts:8 ~ TestPlugin ~ source:",
        source
      );
    },

importer 对应的是引入的绝对路径,source 对应的是引入的文件

main.ts

ts 复制代码
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

createApp(App).mount('#app')

✈️transform(重要)

ts 复制代码
   transform(code, id) {
      console.log(
        "🚀 ~ file: replaceKeyword.ts:8 ~ transform ~ code:",
        code
      );
      console.log(
        "🚀 ~ file: replaceKeyword.ts:8 ~ TestPlugin ~ id:",
        id
      );
    },

transform 对单个文件进行了转换,id 是文件名,code 是转换后的代码字符串

我们可以利用这个进行代码转换

如果带有文件命名中 HelloWorld 的字样,就替换成 abcdefg

js 复制代码
export default function TestPlugin(): PluginOption {
  return {
    name: "TestPlugin",
    enforce: "pre",
    
    transform(code, id) {
      if (id.includes("HelloWorld")) {
        return `
            <template>
              <div>
              abcdefg
              </div>
            </template>
        `;
      }
    },
  };
}

例子

替换文本

原本效果如下图所示,我们想要把 react 替换成 react 和 vue

利用 transform 的特性,判断只要是 .vue 结尾,我们就要替换里面的 react

ts 复制代码
export default function TestPlugin(): PluginOption {
  return {
    name: "TestPlugin",
    enforce: "pre",
    transform(code, id) {
      if (id.endsWith(".vue")) {
        return code.replace(/react/g,"vue and react");
      }
    },
  };
}

也可以利用 正则表达式 进行替换,对 languageMap 进行遍历,遇见 $hi 就替换为 你好

js 复制代码
const languageMap = {
  $hi: "你好",
};

export default function TestPlugin(): PluginOption {
  return {
    name: "TestPlugin",
    enforce: "pre",
    
    transform(code, id) {
      if (id.endsWith(".vue")) {
        let tempCode = code;
        Object.entries(languageMap).forEach(([k, v]) => {
         // 对 $ 进行转义
          const regex = new RegExp(k.replace("$", "\\$"), "g");
          if (regex.test(code)) {
            tempCode = tempCode.replace(regex, v);
          }
        });
        return tempCode;
      }
    },
  };
}

最后来看看 element-plus 中的 vite 插件吧,unplugin-element-plus地址

可以看出,没有引入css文件,核心逻辑在于 transformImportStyle 方法

就以 import { ElButton, ElForm as AForm } from 'element-plus' 举例, (注意使用了 as)

ts 复制代码
const hyphenateRE = /\B([A-Z])/g
const hyphenate = (str) =>
  str.replaceAll(hyphenateRE, '-$1').toLowerCase() 
  
let x = hyphenate("ElButton") // el-button
  
const transformImportStyle =()=>{
  // ...
  
 // 先找到左括号 
 const leftBracket = statement.indexOf('{')
 
 if (leftBracket > -1) {
    // 找到标识符
    // 在我们的例子是 ElButton, ElButton,ElForm as AForm 
    const identifiers = statement.slice(leftBracket + 1, statement.indexOf('}'))
    
    // 以 逗号 进行切割
    // [ElButton,ElForm as AForm]
    const components = identifiers.split(',')
    
    // 作为导入容器
    const styleImports: string[] = []
    
    components.forEach((c)=>{
    // 去除 as 后面的值, \s 表示空白字符
    // ElForm as AForm 变为 ElForm
     const trimmed = c.replace(/\sas\s.+/, '').trim()
     // 只要 EL 开头
     if (trimmed.startsWith("El")){
      // Button Form
      const component = trimmed.slice(2)
      
      // 🚗🚗🚗🚗
      styleImports.push(
          // hyphenate 驼峰式命名转换为短横线连接然后转小写
          // ElButton 转为 el-button ElForm 转为 el-form
          `import 'element-plus/es/components/${hyphenate(
             component
            )}/style/css'`
        )
      }
    })
    return styleImports.join('\n')
  }
}

通过这一通操作,可以看到即使没有引入 css, 插件也默默帮你做了

可以看到命名就是 el-button.css

总结

平时自己写 vite 插件比较少,(可以说没有,哈哈),通过此次摸索,可以对 vite 插件有个大概的了解

相关推荐
一条晒干的咸魚1 分钟前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
花海少爷12 分钟前
第十章 JavaScript的应用课后习题
开发语言·javascript·ecmascript
Amd79416 分钟前
Nuxt.js 应用中的 webpack:compiled 事件钩子
前端·webpack·开发·编译·nuxt.js·事件·钩子
生椰拿铁You25 分钟前
09 —— Webpack搭建开发环境
前端·webpack·node.js
狸克先生36 分钟前
如何用AI写小说(二):Gradio 超简单的网页前端交互
前端·人工智能·chatgpt·交互
sinat_3842410939 分钟前
在有网络连接的机器上打包 electron 及其依赖项,在没有网络连接的机器上安装这些离线包
javascript·arcgis·electron
baiduopenmap1 小时前
百度世界2024精选公开课:基于地图智能体的导航出行AI应用创新实践
前端·人工智能·百度地图
loooseFish1 小时前
小程序webview我爱死你了 小程序webview和H5通讯
前端
小牛itbull1 小时前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress
菜牙买菜1 小时前
让安卓也能玩出Element-Plus的表格效果
前端