跳轉到內容

Vite 插件開發與構建優化進階 2026 | 高性能前端工程化實戰

Vite 插件開發與構建優化

Vite 的插件系統是其生態繁榮的核心。理解插件開發機制不僅能讓你自定義構建流程,還能深入理解 Vite 的底層工作原理。本文將從插件開發到構建優化,系統講解 Vite 進階實戰技巧。


一、Vite 插件機制

1.1 插件的本質

Vite 插件本質上是一個Rollup 插件的擴展,在開發模式和生產構建中分別處理:

  • 開發模式:Vite 使用自定義插件容器,按需編譯,支持 HMR
  • 生產構建:直接使用 Rollup 插件鏈進行打包
typescript
import { Plugin } from 'vite'

const myPlugin: Plugin = {
  name: 'my-plugin',  // 必須有唯一名稱

  // --- Rollup 兼容鉤子 ---
  options(opts) { /* ... */ },
  buildStart(options) { /* ... */ },
  resolveId(source, importer) { /* ... */ },
  load(id) { /* ... */ },
  transform(code, id) { /* ... */ },
  buildEnd() { /* ... */ },
  generateBundle(options, bundle) { /* ... */ },

  // --- Vite 獨有鉤子 ---
  config(config) { /* ... */ },
  configResolved(resolvedConfig) { /* ... */ },
  configureServer(server) { /* ... */ },
  transformIndexHtml(html) { /* ... */ },
  handleHotUpdate(ctx) { /* ... */ },
}

1.2 鉤子執行順序

config → configResolved → options → buildStart
  → resolveId → load → transform(每模塊循環)
→ buildEnd → generateBundle → writeBundle

Vite 獨有鉤子只在開發模式生效,生產構建使用 Rollup 原生鉤子。

1.3 插件應用條件

typescript
const myPlugin: Plugin = {
  name: 'my-plugin',

  // 僅在開發模式應用
  apply: 'serve',

  // 僅處理 .vue 文件
  enforce: 'pre',  // pre | post | 默認中間

  transform(code, id) {
    if (!id.endsWith('.vue')) return
    // 處理 .vue 文件
  }
}

enforce 執行順序:

  • pre:最先執行(如 vue 插件解析 SFC)
  • 默認:中間執行
  • post:最後執行(如壓縮、分析)

二、自定義插件實戰

2.1 虛擬模塊插件

虛擬模塊允許你創建不對應真實文件的模塊:

typescript
function virtualEnvPlugin(): Plugin {
  const virtualModuleId = 'virtual:env-config'
  const resolvedVirtualModuleId = '\0' + virtualModuleId

  return {
    name: 'vite-plugin-virtual-env',

    resolveId(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId
      }
    },

    load(id) {
      if (id === resolvedVirtualModuleId) {
        // 運行時動態生成模塊內容
        const config = {
          apiUrl: process.env.API_URL || 'http://localhost:3000',
          version: process.env.npm_package_version || '0.0.0',
          buildTime: new Date().toISOString()
        }
        return `export default ${JSON.stringify(config, null, 2)}`
      }
    }
  }
}

// 使用
// import envConfig from 'virtual:env-config'
// console.log(envConfig.apiUrl)

2.2 HTML 注入插件

index.html 中注入自定義內容:

typescript
function injectAnalyticsPlugin(gaId: string): Plugin {
  return {
    name: 'vite-plugin-inject-analytics',

    transformIndexHtml: {
      enforce: 'post',
      transform(html) {
        return html.replace(
          '</head>',
          `<script async src="https://www.googletagmanager.com/gtag/js?id=${gaId}"></script>
<script>
  window.dataLayer = window.dataLayer || []
  function gtag(){dataLayer.push(arguments)}
  gtag('js', new Date())
  gtag('config', '${gaId}')
</script>
</head>`
        )
      }
    }
  }
}

2.3 Markdown 編譯插件

將 Markdown 文件編譯為 Vue 組件:

typescript
import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkHtml from 'remark-html'

function markdownPlugin(): Plugin {
  return {
    name: 'vite-plugin-markdown',

    async transform(code, id) {
      if (!id.endsWith('.md')) return

      const result = await unified()
        .use(remarkParse)
        .use(remarkHtml)
        .process(code)

      const html = String(result)

      // 返回 Vue SFC 格式
      return {
        code: `<template><div class="markdown">${html}</div></template>`,
        map: null
      }
    }
  }
}

2.4 自定義 HMR 插件

typescript
function customHMRPlugin(): Plugin {
  return {
    name: 'vite-plugin-custom-hmr',

    handleHotUpdate({ file, server, modules }) {
      // 當數據文件變更時,觸發特定模塊更新
      if (file.endsWith('.data.json')) {
        console.log(`數據文件更新:${file}`)

        // 找到使用此數據的模塊
        const importers = [...modules.values()]
          .flatMap(m => [...m.importers])

        // 觸發全頁面刷新
        server.ws.send({
          type: 'full-reload',
          path: '*'
        })

        // 返回空數組阻止默認 HMR
        return []
      }
    }
  }
}

2.5 構建時代碼生成插件

typescript
function generateRoutesPlugin(): Plugin {
  return {
    name: 'vite-plugin-generate-routes',

    async buildStart() {
      // 掃描 pages 目錄,自動生成路由
      const pages = await glob('src/pages/**/*.{vue,tsx}')
      const routes = pages.map(page => {
        const path = page
          .replace('src/pages', '')
          .replace(/\.(vue|tsx)$/, '')
          .replace(/\/index$/, '')
          .toLowerCase()

        return `{ path: '${path || '/'}', component: () => import('/${page}') }`
      })

      const code = `export const routes = [${routes.join(',')}]`

      this.emitFile({
        type: 'asset',
        fileName: 'generated/routes.ts',
        source: code
      })
    }
  }
}

三、HMR 熱更新原理

3.1 HMR 工作流程

1. 文件變更
2. Vite 監聽到變更,編譯該模塊
3. 通過 WebSocket 發送 update 通知
4. 瀏覽器收到通知,請求更新後的模塊
5. 運行 HMR callback,替換舊模塊
6. 如果沒有 HMR boundary,觸發全頁面刷新

3.2 自定義 HMR API

typescript
// 在業務代碼中使用
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // 模塊自接受更新
    if (newModule) {
      console.log('模塊已更新', newModule)
    }
  })

  import.meta.hot.accept('./dependency', (newDep) => {
    // 接受依賴更新
    console.log('依賴已更新', newDep)
  })

  import.meta.hot.dispose((data) => {
    // 清理舊模塊的副作用
    // data 會在新模塊加載時傳遞
    data.count = count
  })

  import.meta.hot.on('custom-event', (payload) => {
    // 監聽自定義事件
    console.log('自定義事件', payload)
  })
}

四、構建優化實戰

4.1 代碼分割策略

typescript
// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // 手動分包
        manualChunks: {
          // 將 Vue 全家桶單獨打包
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          // UI 庫單獨打包
          'ui-vendor': ['element-plus'],
          // 工具庫單獨打包
          'utils': ['lodash-es', 'dayjs', 'axios'],
        },
        // 或使用函數形式(更靈活)
        manualChunks(id) {
          // node_modules 中的包按目錄分組
          if (id.includes('node_modules')) {
            if (id.includes('element-plus')) return 'ui-vendor'
            if (id.includes('lodash')) return 'utils'
            return 'vendor'
          }
        },
        // 文件命名
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js',
        assetFileNames: '[ext]/[name]-[hash].[ext]'
      }
    }
  }
})

4.2 Tree Shaking 優化

確保 Tree Shaking 生效的關鍵條件:

  1. 使用 ESM:確保依賴提供 ESM 格式
  2. sideEffects 配置
json
// package.json
{
  "sideEffects": false,
  "sideEffects": ["*.css", "*.scss"]
}
  1. 按需導入
typescript
// ❌ 導入整個庫(無法 Tree Shake)
import _ from 'lodash'

// ✅ 按需導入
import debounce from 'lodash/debounce'
import { debounce } from 'lodash-es'
  1. 檢查 Tree Shaking 效果
bash
# 分析包內容
npx vite-bundle-visualizer

# 或使用 rollup-plugin-visualizer
npm install -D rollup-plugin-visualizer
typescript
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
      filename: 'stats.html'
    })
  ]
})

4.3 預構建優化

Vite 會自動預構建 CommonJS 依賴為 ESM,可以自定義優化:

typescript
export default defineConfig({
  optimizeDeps: {
    // 預包含的依賴(避免頁面加載時發現新依賴導致重新預構建)
    include: [
      'vue',
      'vue-router',
      'pinia',
      'axios',
      'lodash-es'
    ],
    // 排除不需要預構建的包
    exclude: [
      '@iconify/json',  // 大量數據的包
    ],
    // 強制預構建(依賴更新後)
    force: false
  }
})

4.4 依賴外置與 CDN

typescript
export default defineConfig({
  build: {
    rollupOptions: {
      external: ['vue', 'vue-router'],
      output: {
        globals: {
          vue: 'Vue',
          'vue-router': 'VueRouter'
        }
      }
    }
  },

  // 在 HTML 中注入 CDN 腳本
  plugins: [
    {
      name: 'html-cdn',
      transformIndexHtml: {
        enforce: 'pre',
        transform(html) {
          return html.replace(
            '</head>',
            `<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@4/dist/vue-router.global.prod.js"></script>
</head>`
          )
        }
      }
    }
  ]
})

4.5 圖片與資源優化

typescript
export default defineConfig({
  build: {
    // 小於 4KB 的資源內聯為 base64
    assetsInlineLimit: 4096,

    // CSS 代碼分割
    cssCodeSplit: true,

    // 構建後壓縮選項
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,    // 移除 console
        drop_debugger: true    // 移除 debugger
      }
    },

    // chunk 大小警告閾值
    chunkSizeWarningLimit: 1000,

    // sourcemap
    sourcemap: false,

    // Rollup 配置
    rollupOptions: {
      output: {
        // 限制 chunk 大小(超過 500KB 自動分割)
        experimentalMinChunkSize: 500_000
      }
    }
  }
})

五、開發體驗優化

5.1 開發服務器配置

typescript
export default defineConfig({
  server: {
    host: '0.0.0.0',
    port: 3000,
    open: true,

    // 代理配置
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
        configure: (proxy) => {
          proxy.on('error', (err) => console.log('proxy error', err))
        }
      },
      '/ws': {
        target: 'ws://localhost:8080',
        ws: true
      }
    },

    // CORS
    cors: true,

    // HTTPS
    https: false
  }
})

5.2 別名與路徑映射

typescript
import { resolve } from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@components': resolve(__dirname, 'src/components'),
      '@utils': resolve(__dirname, 'src/utils'),
      '@assets': resolve(__dirname, 'src/assets')
    },
    extensions: ['.ts', '.tsx', '.js', '.jsx', '.vue', '.json']
  }
})

配合 tsconfig.json:

json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"]
    }
  }
}

六、性能分析工具

6.1 構建性能分析

typescript
import { Plugin } from 'vite'

function buildTimingPlugin(): Plugin {
  let startTime: number

  return {
    name: 'build-timing',
    buildStart() {
      startTime = Date.now()
      console.log('構建開始...')
    },
    buildEnd() {
      const duration = Date.now() - startTime
      console.log(`構建耗時:${(duration / 1000).toFixed(2)}s`)
    },
    generateBundle() {
      const duration = Date.now() - startTime
      console.log(`生成包耗時:${(duration / 1000).toFixed(2)}s`)
    }
  }
}

6.2 常用分析工具

bash
# 包體積分析
npx vite-bundle-visualizer

# Rollup 分析
npx rollup --config --environment BUILD:analysis

# Speed Measure(開發模式性能分析)
# 使用 vite-plugin-inspect
npm install -D vite-plugin-inspect
typescript
import Inspect from 'vite-plugin-inspect'

export default defineConfig({
  plugins: [Inspect()]
})
// 訪問 http://localhost:3000/__inspect/ 查看

七、部署優化

7.1 多環境構建

typescript
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '')

  return {
    define: {
      __APP_VERSION__: JSON.stringify(env.npm_package_version),
      __BUILD_TIME__: JSON.stringify(new Date().toISOString())
    },
    build: {
      outDir: `dist-${mode}`,
      rollupOptions: {
        output: {
          manualChunks: mode === 'production' ? {
            vendor: ['vue', 'vue-router', 'pinia']
          } : undefined
        }
      }
    }
  }
})

7.2 構建產物分析

bash
# 構建後分析
pnpm build
ls -la dist/assets/

# 檢查 gzip 後大小
gzip -c dist/assets/*.js | wc -c

# Brotli 大小
brotli dist/assets/*.js

7.3 gzip 壓縮插件

typescript
import viteCompression from 'vite-plugin-compression'

export default defineConfig({
  plugins: [
    viteCompression({
      algorithm: 'gzip',
      threshold: 10240,  // 大於 10KB 才壓縮
      deleteOriginFile: false
    }),
    viteCompression({
      algorithm: 'brotliCompress',
      threshold: 10240
    })
  ]
})

八、總結

  • ✅ 理解 Vite 插件機制(Rollup 兼容 + Vite 獨有鉤子)
  • ✅ 實戰自定義插件(虛擬模塊、HTML注入、HMR、代碼生成)
  • ✅ 掌握 HMR 熱更新原理與 API
  • ✅ 構建優化:代碼分割、Tree Shaking、預構建、CDN 外置
  • ✅ 開發體驗優化:代理、別名、多環境
  • ✅ 性能分析與部署優化

Vite 的插件系統是其靈活性的核心,掌握插件開發讓你能夠應對任何定製化構建需求。


相關閱讀:

最後更新於: