vite 插件

1. vite 插件介绍

  • 服务器启动阶段 : optionsbuildStart钩子会在服务启动时被调用。

  • 请求响应阶段 : 当浏览器发起请求时,Vite 内部依次调用resolveIdloadtransform钩子。

  • 服务器关闭阶段 : Vite 会依次执行buildEndcloseBundle钩子。

  • moduleParsed: 模块解析后触发,允许你修改模块的元数据。 在开发阶段不被调用,因为 Vite 使用的是热重载和即时模块更新,不需要完整的模块解析阶段。

  • renderChunk:生成输出块时调用,允许你自定义生成的代码块。 开发阶段不调用,因为 Vite 主要依赖浏览器的即时更新,而非生成最终的打包输出。

1.1 强制插件排序

ts 复制代码
import typescript2 from 'rollup-plugin-typescript2'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    {
      ...typescript2(),
      apply: 'build',
    },
    {
      ...image(),
      enforce: 'pre',
    },
  ],
})
  • pre: 在vite核心插件之前调用这个插件
  • post: 在vite核心插件之后调用这个插件
  • 默认:在 Vite 核心插件之后调用该插件

1.2 情景应用

  • 开发 (serve) 和生产 (build) 默认都会调用
  • apply 属性指明它们仅在 'build' 或 'serve' 模式时调用:

注意:名字 vite-plugin-xxx

2. 虚拟模块

  • 虚拟模块就像是你凭空创造出来的 JS 文件,不在电脑上真实存在,但可以被其他模块像普通文件一样导入使用。

适用场景 - 动态生成配置 运行时变量注入 按需生成工具函数

2.1

js 复制代码
import type { Plugin } from 'vite'

// 定义虚拟模块ID
const virtualModuleId = 'virtual:fibonacci'
const resolvedVirtualModuleId = '\0' + virtualModuleId

export default function virtualFibPlugin(): Plugin {
  return {
    name: 'vite-plugin-virtual-fib',

    // 解析虚拟模块ID
    resolveId(id) {
      console.log(id, 'resolveId')
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId
      }
    },

    // 加载虚拟模块内容
    load(id) {
      console.log(id, 'load')
      if (id === resolvedVirtualModuleId) {
        return `
          // 斐波那契数列实现
          export function fib(n) {
            return n <= 1 ? n : fib(n - 1) + fib(n - 2)
          }

          // 记忆化版本
          export function memoFib(n, memo = {}) {
            if (n in memo) return memo[n]
            if (n <= 1) return n
            memo[n] = memoFib(n - 1, memo) + memoFib(n - 2, memo)
            return memo[n]
          }
        `
      }
    }
  }
}

注意1:加 \0 公共插件使用 virtual:插件名 格式(如 virtual:posts)

注意2: 虚拟模块在生产的时候 最好显示配置一下

js 复制代码
export default {
  plugins: [virtualPlugin()],
  build: {
    rollupOptions: {
      plugins: [virtualPlugin()] // 显式注册
    }
  }
}

🤔:不配置啥后果 虚拟模块在生产环境中的可用性取决于具体场景

  • 基本虚拟模块(仅生成静态内容):

    • 通常可以工作:如果插件只是生成简单的静态内容
    • 原因:Vite 会继承主插件数组中的插件配置
  • 复杂虚拟模块(依赖特定钩子或转换):

    • ❌ 可能失败:如果插件依赖 build 阶段的特定钩子
    • 报错表现:Cannot find module 'virtual:xxx' 或生成的内容不正确

3. vite特有钩子

  • config 在解析 Vite 配置前调用。钩子接收原始用户配置
  • configResolved 在解析 Vite 配置后调用 使用这个钩子读取和存储最终解析的配置
  • configureServer 是用于配置开发服务器的钩子
  • configurePreviewServer 用于定制预览服务器的钩子,类似configureServer 但专门用vite preview 命令启动的预览服务器。
  • transformIndexHtml 专门用来修改 index.html 文件的钩子
  • handleHotUpdate 自定义热模块替换(HMR)行为的钩子

3.1 配置处理钩子

config - 修改配置

js 复制代码
config(config, { command, mode }) {
  // command: 'serve'开发模式 | 'build'生产模式
  // mode: 'development' | 'production' | 自定义模式

  if (command === 'serve') {
    return {
      server: {
        port: 3000, // 修改开发服务器端口
        open: true  // 自动打开浏览器
      }
    }
  }
  // 生产环境配置
  return {
    build: {
      minify: 'terser'
    }
  }
}

configResolved - 配置确认

js 复制代码
const myPlugin = () => {
  let viteConfig // 用于存储配置
  return {
    name: 'my-plugin',
    // configResolved 钩子
    configResolved(resolvedConfig) {
      // 存储最终解析的配置
      viteConfig = resolvedConfig
      console.log('当前运行模式:', viteConfig.command)
    },

    // 在其他钩子中使用配置
    transform(code, id) {
      if (viteConfig.command === 'serve') {
        console.log('开发模式处理:', id)
      } else {
        console.log('生产构建处理:', id)
      }
      return code
    }
  }
}
js 复制代码
configResolved(config) {
  this.isDev = config.command === 'serve'
  this.isProduction = !this.isDev
},

load(id) {
  if (id === virtualModuleId) {
    return this.isDev
      ? `export const mode = 'development'`
      : `export const mode = 'production'`
  }
}

3.2 开发阶段钩子

  1. configureServer 开发服务器的钩子

    • 添加新的功能比如中间件 或者文件监听
js 复制代码
configureServer(server: ViteDevServer): void | (() => void)

参数 server 包含以下重要属性:

  • middlewares: Connect 中间件实例
  • httpServer: 底层 HTTP 服务器
  • watcher: 文件监听器
  • ws: WebSocket 服务器
  • transformRequest(): 用于转换模块内容

eg

js 复制代码
import type { Plugin } from 'vite'
import type { ViteDevServer } from 'vite'

export default function vitePluginTest(): Plugin {
  let devServer: ViteDevServer

  return {
    name: 'vite-plugin-test',
    configureServer(server) {
      devServer = server

      // 监听src目录下的所有文件变化(修正了watcher.add的用法)
      server.watcher.add('src/**/*')

      // 添加API接口
      server.middlewares.use('/api/data', (_, res) => {
        res.setHeader('Content-Type', 'application/json')
        res.end(
          JSON.stringify({
            data: '测试数据',
            timestamp: Date.now()
          })
        )
      })

      // 请求日志中间件
      server.middlewares.use((req, res, next) => {
        console.log(`[${new Date().toISOString()}] 请求: ${req.method} ${req.url}`)
        next()
      })

      // 监听文件变化(修正了监听逻辑)
      server.watcher.on('change', (file) => {
        console.log(`文件发生变化: ${file}`)
        if (file.startsWith('src/')) {
          console.log('src目录文件变化,刷新页面')
          server.ws.send({
            type: 'full-reload',
            path: '*'
          })
        }
      })
    },
    handleHotUpdate({ file }) {
      if (file.endsWith('.tsx')) {
        console.log('TSX文件修改,触发全量刷新')
        devServer.ws.send({
          type: 'full-reload',
          path: '*'
        })
      }
    }
  }
}

注意

js 复制代码
configureServer(server) {
  // 这个会在Vite中间件之前执行
  server.middlewares.use(...)
  
  // 返回的函数会在Vite中间件之后执行
  return () => {
    server.middlewares.use(...)
  }
}
  1. transformIndexHtml 可以定制html内容

可以 自动插入标签 修改内容

js 复制代码
 transformIndexHtml(html, ctx) {
      const tags: Array<{
        tag: string
        injectTo: 'head' | 'body' | 'head-prepend' | 'body-prepend'
        children?: string
        attrs?: Record<string, string>
      }> = []

      const isDev = !!ctx.server
      if (isDev) {
        tags.push({
          tag: 'script',
          injectTo: 'body-prepend',
          children: 'console.log("开发模式已启动")'
        })
      } else {
        tags.push({
          tag: 'meta',
          injectTo: 'head',
          attrs: { name: 'robots', content: 'index,follow' }
        })
      }

      const newHtml = html.replace(
        '<title>Vite + React + TS</title>',
        '<title>我的定制应用</title>'
      )

      // 返回符合 Vite 要求的类型
      return {
        html: newHtml,
        tags
      }
    },
  1. handleHotUpdate模块热更新
js 复制代码
    handleHotUpdate(ctx) {
      if (ctx.file.endsWith('.tsx')) {
        console.log('TSX文件修改,触发全量刷新')
        devServer.ws.send({
          type: 'full-reload',
          path: '*'
        })
      }

      // 2. 只处理项目文件,忽略node_modules
      return ctx.modules.filter((module) => !module?.id?.includes('node_modules'))
    }
graph TD A[采购食材 config] --> B[确认菜单 configResolved] B --> C[员工培训 configureServer] C --> D[开门营业 buildStart] D --> E{客人点餐} E -->|HTML订单| F[摆盘 transformIndexHtml] E -->|食材请求| G[接单→备货→烹饪 resolveId→load→transform] E -->|变更需求| H[厨房监控 handleHotUpdate] H --> I{是否打烊} I -->|是| J[清理 buildEnd → 关店 closeBundle]

4. 例子

4.1 自动引入antd组件

github.com/unplugin/un...

一个自动引入插件 可以自动为你的项目按需导入 API,无需手动编写 import 语句,自动导入,按需加载。

  • resolvers 是 unplugin-auto-import 中的一个高级配置选项,用于自定义解析自动导入的组件或工具函数的方式。

思考: 问题

  • 我们如果开发的时候 使用antd组件 可以使用这个插件 导入很多antd组件,让vite.config文件内容很多 不太美观
  • 并且antd组件中没有默认的字母开头 比如 Button 可能会造成与其他自定义组件冲突,想一下是不是可以给antd 组件加个前缀 就像AButton

所以的插件功能主要实现 自动导入全部antd组件 并且可以自定义前缀

js 复制代码
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import AutoImport from 'unplugin-auto-import/vite'
import antdResolver from './unplugin-auto-import-antd'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    react(),
    AutoImport({
      imports: [
        'react',
        {
          // antd: [
          //   'Button',
          //   'Input'
          // ]
        }
      ],
      resolvers: [
        // antdResolver({
        //   prefix: 'A', // 可选:为所有组件添加 A 前缀
        //   packageName: 'antd' // 可选:默认为 'antd'
        // }),
        {
          type: 'component',
          resolve: (name: string) => {
            console.log('resolve', name)
            const supportedComponents = ['AButton', 'Button', 'AInput', 'Table'] // 扩展这个列表

            if (supportedComponents.includes(name)) {
              return {
                from: 'antd',
                name: 'Input',
                as: `${name}` // 统一添加A前缀
              }
            }
            return undefined
          }
        }
      ],
      dts: true, // 生成类型声明文件
      eslintrc: {
        enabled: true // 生成 eslint 配置
      }
    })
  ]
})

4.2 svg直接作为组件导入

ts 复制代码
import type { Plugin } from 'vite'
import fs from 'node:fs/promises'
import { transform } from '@svgr/core'
import { transform as esbuildTransform } from 'esbuild'

interface SvgrOptions {
  defaultExport?: 'url' | 'component' // 导出类型:URL字符串 或 React组件
  svgrOptions?: Record<string, any>   // 自定义SVGR配置
}

export default function svgrPlugin(options: SvgrOptions = {}): Plugin {
  // 设置默认值:默认导出为组件,空SVGR配置
  const { defaultExport = 'component', svgrOptions = {} } = options

  return {
    name: 'vite-plugin-svgr',
    
    // transform 钩子:转换文件内容
    async transform(_, id) {
      // 只处理 .svg 文件
      if (!id.endsWith('.svg')) return
      
      try {
        // 1. 读取 SVG 文件内容
        const svg = await fs.readFile(id, 'utf-8')
        
        // 2. 使用 SVGR 将 SVG 转换为 React 组件代码
        const componentCode = await transform(
          svg, // SVG 原始内容
          {
            ...svgrOptions, // 用户自定义配置
            
            // 核心插件配置开始
            plugins: [
              '@svgr/plugin-jsx',      // 转换 SVG 为 JSX
              '@svgr/plugin-prettier'   // 格式化生成的代码
            ],
            
            // 其他重要配置
            typescript: true,          // 生成 TS 兼容代码
            jsxRuntime: 'automatic',   // 使用新版 JSX 运行时
            exportType: 'named',       // 使用命名导出
            
            // 自定义模板:控制组件输出结构
            template: ({ componentName, jsx }, { tpl }) => {
              return tpl`
                const ${componentName} = (props) => ${jsx};
                export { ${componentName} };
              `
            }
          },
          { componentName: 'ReactComponent' } // 设置组件名称
        )
        
        // 3. 清理生成的代码
        let jsCode = componentCode
          .replace(/^\/\*.*?\*\/\s*/gms, '') // 移除注释
          .replace(/\n+/g, '\n')            // 压缩空行
          .trim()
        
        // 4. 处理导出逻辑
        if (defaultExport === 'url') {
          // URL 模式:默认导出 SVG 路径
          jsCode = `
            ${jsCode}
            export default ${JSON.stringify(id)};
          `.trim()
        } else {
          // 组件模式:默认导出 React 组件
          jsCode = `
            ${jsCode}
            export default ReactComponent;
          `.trim()
        }
        
        // 5. 使用 esbuild 转换 JSX 为浏览器可执行代码
        const result = await esbuildTransform(jsCode, {
          loader: 'jsx',      // 指定为 JSX 类型
          jsx: 'automatic',   // 使用新版 JSX 转换
          sourcefile: id,     // 源文件路径(用于 sourcemap)
          format: 'esm',      // 输出 ESM 格式
          target: 'es2020',   // 目标 ES 版本
          logLevel: 'silent'  // 不输出日志
        })
        
        // 6. 返回转换后的代码
        return {
          code: result.code,
          map: result.map || null
        }
      } catch (error) {
        // 错误处理:回退到原始 SVG 路径导出
        console.error(`SVG转换失败 [${id}]:`, error)
        return {
          code: `export default ${JSON.stringify(id)};`,
          map: null
        }
      }
    }
  }
}
相关推荐
恋猫de小郭3 分钟前
Google I/O Extended :2025 Flutter 的现状与未来
android·前端·flutter
江城开朗的豌豆7 分钟前
Vue-router方法大全:让页面跳转随心所欲!
前端·javascript·vue.js
程序员爱钓鱼17 分钟前
Go语言泛型-泛型约束与实践
前端·后端·go
前端小巷子18 分钟前
web从输入网址到页面加载完成
前端·面试·浏览器
江城开朗的豌豆19 分钟前
Vue路由动态生成秘籍:让你的链接'活'起来!
前端·javascript·vue.js
晓得迷路了19 分钟前
栗子前端技术周刊第 88 期 - Apache ECharts 6.0 beta、Deno 2.4、Astro 5.11...
前端·javascript·echarts
江城开朗的豌豆25 分钟前
在写vue公用组件的时候,怎么提高可配置性
前端·javascript·vue.js
江城开朗的豌豆25 分钟前
Vue路由跳转的N种姿势,总有一种适合你!
前端·javascript·vue.js
江城开朗的豌豆26 分钟前
Vue路由玩法大揭秘:三种路由模式你Pick谁?
前端·javascript·vue.js
江城开朗的豌豆27 分钟前
Vue路由守卫全攻略:给页面访问装上'安检门'
前端·javascript·vue.js