Vite 高级指南

1. 插件开发 API(#%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91-api) 2. SSR(服务端渲染)(#ssr%EF%BC%88%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%B8%B2%E6%9F%93%EF%BC%89) 3. 中间件模式(#%E4%B8%AD%E9%97%B4%E4%BB%B6%E6%A8%A1%E5%BC%8F) 4. JavaScript API(#javascript-api) 5. 性能优化(#%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96) 6.

分享

官方文档:https://vite.dev/guide/
适用版本:Vite 6.x(2026-05-08 核实)

目录

  1. 插件开发 API
  2. SSR(服务端渲染)
  3. 中间件模式
  4. JavaScript API
  5. 性能优化
  6. 环境变量进阶与多环境部署
  7. Preview Options 完整配置
  8. Worker Options 完整配置
  9. 打包与部署
  10. Monorepo 配置
  11. 高级插件示例:自定义 Mock 插件
  12. 踩坑与最佳实践清单

插件开发 API

插件结构

Vite 插件使用工厂函数模式,函数接收用户选项,返回一个 Plugin 对象(或 Plugin 数组)。

命名规范:

  • Vite 专用插件:vite-plugin-xxx
  • Rollup 兼容插件:rollup-plugin-xxx
  • 框架专用插件:vite-plugin-vue-xxx

Plugin 对象顶层控制字段:

参数名 类型 默认值 说明
name string 必填 插件名称,用于错误信息和日志
enforce 'pre' | 'post' | undefined undefined 插件执行顺序。pre 在核心插件之前,post 在核心插件之后
apply 'build' | 'serve' | function 两者都运行 控制插件在何时生效。传入函数可实现更精细的条件判断

执行顺序(从先到后):

  1. enforce: 'pre' 的插件
  2. 无 enforce 的插件(核心 Vite 插件穿插其中)
  3. enforce: 'post' 的插件

基本插件骨架:

// vite-plugin-example/index.js
export default function vitePluginExample(options = {}) {
  const { prefix = 'example' } = options

  return {
    name: 'vite-plugin-example',
    enforce: 'pre',          // 可选
    apply: 'build',          // 可选,仅构建时启用

    // ... 钩子函数
  }
}

apply 使用函数进行条件判断:

export default function myPlugin() {
  return {
    name: 'vite-plugin-conditional',
    apply(config, { command, mode }) {
      // 仅在生产构建且 mode 为 staging 时启用
      return command === 'build' && mode === 'staging'
    },
  }
}

通用钩子(与 Rollup 共享)

这些钩子与 Rollup 插件 API 完全兼容,在 Vite 中的行为与 Rollup 一致。

钩子总览:

钩子 触发时机 异步支持 说明
options 服务器/构建启动前 修改 Rollup 选项
buildStart 每次构建开始 初始化构建状态
resolveId 模块导入解析 自定义模块 ID 解析逻辑
load 模块加载 自定义模块内容读取
transform 模块内容转换 转换模块源代码
buildEnd 构建结束 构建完成或出错时触发
closeBundle bundle 关闭 清理资源

options(options)

参数:

参数名 类型 说明
options RollupOptions 当前 Rollup 配置对象

返回值: RollupOptions | null。返回修改后的配置对象或 null(不修改)。

options(options) {
  // 追加外部依赖
  return {
    ...options,
    external: [...(options.external || []), 'some-lib'],
  }
}

buildStart(options)

参数:

参数名 类型 说明
options NormalizedInputOptions 已规范化的 Rollup 输入选项

返回值: void

buildStart(options) {
  console.log('构建开始,入口文件:', options.input)
  // 初始化缓存、计时器等
  this.cache = new Map()
}

resolveId(source, importer, options)

参数:

参数名 类型 默认值 说明
source string 必填 被导入的模块标识符(原始字符串)
importer string | undefined 必填 发起导入的模块路径。入口模块时为 undefined
options.isEntry boolean 是否为入口模块
options.assertions Record<string, string> 导入断言(import assertions)

返回值: string | false | null | { id, external?, moduleSideEffects?, resolvedBy? }

  • 返回字符串:作为解析后的模块 ID
  • 返回 false:将该模块标记为外部模块(external)
  • 返回 null:交给其他插件或默认解析逻辑处理
resolveId(source, importer) {
  if (source === 'virtual:config') {
    // 虚拟模块,返回特殊 ID
    return '\0virtual:config'
  }
  return null // 其他模块交给默认处理
}

load(id)

参数:

参数名 类型 说明
id string 模块 ID(由 resolveId 返回的值)

返回值: string | null | { code, map?, moduleSideEffects?, syntheticNamedExports? }

load(id) {
  if (id === '\0virtual:config') {
    return `export const config = ${JSON.stringify({ version: '1.0.0' })}`
  }
  return null
}

transform(code, id)

参数:

参数名 类型 说明
code string 模块源代码
id string 模块 ID

返回值: string | null | { code, map? }

transform(code, id) {
  if (!id.endsWith('.vue')) return null

  // 对 .vue 文件进行处理
  const transformed = code.replace(/console\.log\(.*?\)/g, '')
  return {
    code: transformed,
    map: null, // 若有 source map 请提供
  }
}

buildEnd(error?)

参数:

参数名 类型 默认值 说明
error Error | undefined undefined 若构建出错,则为错误对象
buildEnd(error) {
  if (error) {
    console.error('构建失败:', error.message)
  } else {
    console.log('构建成功')
  }
}

closeBundle()

无参数。用于清理资源、关闭数据库连接等。在 buildEnd 之后触发。

async closeBundle() {
  await db.close()
  console.log('资源清理完毕')
}

Vite 特有钩子

这些钩子仅在 Vite 中有效,Rollup 构建时会被忽略。

钩子总览:

钩子 异步 顺序 说明
config 串行 修改 Vite 配置(深度合并方式)
configResolved 并行 读取最终配置,只读,不可修改
configureServer 串行 配置开发服务器,添加自定义中间件
configurePreviewServer 串行 配置预览服务器
transformIndexHtml 串行 转换 index.html
handleHotUpdate 串行 自定义 HMR 更新处理逻辑

config(config, env)

参数:

参数名 类型 说明
config UserConfig 用户配置(合并前)
env.command 'build' | 'serve' 当前运行的命令
env.mode string 当前 mode(development/production/staging 等)

返回值: Partial<UserConfig> | null。返回的对象将与现有配置深度合并。

config(config, { command, mode }) {
  if (command === 'serve') {
    return {
      define: {
        __DEV__: true,
      },
    }
  }
  return {
    build: {
      sourcemap: mode !== 'production',
    },
  }
}

configResolved(config)

参数:

参数名 类型 说明
config ResolvedConfig 最终合并解析后的完整配置,只读
let resolvedConfig

configResolved(config) {
  // 保存配置引用供其他钩子使用
  resolvedConfig = config
  console.log('最终 base:', config.base)
}

configureServer(server)

参数:

参数名 类型 说明
server ViteDevServer 开发服务器实例

返回值: (() => void) | void。若返回函数,该函数将在内置中间件安装完毕后执行(可用于在内置中间件之后插入中间件)。

configureServer(server) {
  // 在内置中间件之前添加
  server.middlewares.use('/api/hello', (req, res) => {
    res.end(JSON.stringify({ message: 'hello from plugin' }))
  })

  // 若要在内置中间件之后添加,返回一个函数
  return () => {
    server.middlewares.use((req, res, next) => {
      // 后置中间件
      next()
    })
  }
}

configurePreviewServer(server)

参数:

参数名 类型 说明
server PreviewServer 预览服务器实例

与 configureServer 用法相同,但作用于 vite preview 命令启动的预览服务器。

transformIndexHtml(html, ctx)

参数:

参数名 类型 说明
html string 原始 HTML 内容
ctx.path string HTML 文件路径
ctx.filename string HTML 文件绝对路径
ctx.server ViteDevServer | undefined 开发模式下的服务器实例
ctx.bundle OutputBundle | undefined 构建模式下的 bundle 信息
ctx.chunk OutputChunk | undefined 与当前 HTML 对应的 chunk

返回值: string | HtmlTagDescriptor[] | { html, tags }

transformIndexHtml(html, ctx) {
  // 方式一:直接返回修改后的 HTML 字符串
  return html.replace(
    '<title>App</title>',
    `<title>${process.env.npm_package_name}</title>`
  )
}

// 方式二:返回标签描述符数组(推荐,更安全)
transformIndexHtml() {
  return [
    {
      tag: 'meta',
      attrs: { name: 'build-time', content: new Date().toISOString() },
      injectTo: 'head',
    },
    {
      tag: 'script',
      attrs: { src: '/analytics.js', defer: true },
      injectTo: 'body',
    },
  ]
}

HtmlTagDescriptor 字段:

字段 类型 说明
tag string HTML 标签名
attrs Record<string, string | boolean> 标签属性
children string | HtmlTagDescriptor[] 子内容
injectTo 'head' | 'body' | 'head-prepend' | 'body-prepend' 注入位置

handleHotUpdate(ctx)

参数(ctx 对象):

参数名 类型 说明
file string 发生变化的文件绝对路径
timestamp number 变化发生的时间戳
modules ModuleNode[] 受此变化影响的模块列表
read () => string | Promise 读取文件内容的函数(带缓存)
server ViteDevServer 开发服务器实例

返回值: ModuleNode[] | void。返回需要热更新的模块列表;返回空数组则不触发 HMR;返回 undefined 则使用默认逻辑。

async handleHotUpdate({ file, modules, server }) {
  if (file.endsWith('.json')) {
    // 自定义:JSON 文件变化时,通知客户端刷新数据
    server.ws.send({
      type: 'custom',
      event: 'json-update',
      data: { file },
    })
    return [] // 阻止默认 HMR
  }

  // 过滤掉不需要热更新的模块
  return modules.filter(m => !m.url.includes('vendor'))
}

虚拟模块

虚拟模块允许插件向构建系统提供不存在于磁盘上的模块内容。

约定:

  • 模块 ID 以 virtual: 前缀开头(面向用户的 ID)
  • resolveId 返回时,使用 \0 前缀(Rollup 约定,防止其他插件误处理)
// vite-plugin-virtual-config.js
export default function vitePluginVirtualConfig(appConfig = {}) {
  const VIRTUAL_ID = 'virtual:app-config'
  const RESOLVED_ID = '\0virtual:app-config'

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

    resolveId(id) {
      if (id === VIRTUAL_ID) {
        return RESOLVED_ID
      }
    },

    load(id) {
      if (id === RESOLVED_ID) {
        return `
          export const config = ${JSON.stringify(appConfig)};
          export default config;
        `
      }
    },
  }
}

使用方式:

// vite.config.js
import vitePluginVirtualConfig from './vite-plugin-virtual-config'

export default {
  plugins: [
    vitePluginVirtualConfig({
      apiBaseUrl: 'https://api.example.com',
      version: '2.0.0',
    }),
  ],
}
// src/app.js
import { config } from 'virtual:app-config'

console.log(config.apiBaseUrl) // 'https://api.example.com'

TypeScript 类型声明:

// src/env.d.ts
declare module 'virtual:app-config' {
  export const config: {
    apiBaseUrl: string
    version: string
  }
  export default config
}

插件通信(Client-Server)

Vite 提供了开发服务器与浏览器客户端之间通过 WebSocket 通信的机制。

服务端发送事件

// 插件中(服务端)
configureServer(server) {
  server.watcher.on('change', (file) => {
    server.ws.send({
      type: 'custom',
      event: 'file-changed',
      data: { file, timestamp: Date.now() },
    })
  })
}

server.ws.send 参数:

参数名 类型 说明
type 'custom' 类型固定为 'custom'(内置类型由 Vite 管理)
event string 自定义事件名称
data any 传递给客户端的数据(需可 JSON 序列化)

客户端监听事件

// 客户端代码(会被 tree-shaking 掉,仅开发时有效)
if (import.meta.hot) {
  import.meta.hot.on('file-changed', (data) => {
    console.log('文件发生变化:', data.file)
    // 执行自定义更新逻辑
  })
}

客户端发送事件

// 客户端发送
if (import.meta.hot) {
  import.meta.hot.send('client-event', { payload: 'hello server' })
}

服务端监听客户端事件

configureServer(server) {
  server.ws.on('client-event', (data, client) => {
    console.log('收到客户端消息:', data.payload)
    // client 是发送消息的 WebSocket 连接
  })
}

TypeScript 扩展 CustomEventMap

// src/env.d.ts
/// <reference types="vite/client" />

interface ImportMetaHot {
  on(event: 'file-changed', cb: (data: { file: string; timestamp: number }) => void): void
  on(event: 'build-complete', cb: (data: { duration: number }) => void): void
}

或使用推荐的 CustomEventMap 扩展方式:

declare module 'vite/types/customEvent' {
  interface CustomEventMap {
    'file-changed': { file: string; timestamp: number }
    'build-complete': { duration: number }
    'client-event': { payload: string }
  }
}

完整插件示例:vite-plugin-auto-routes

自动扫描 pages/ 目录,生成 Vue Router 路由配置的完整插件:

// vite-plugin-auto-routes/index.js
import fs from 'node:fs'
import path from 'node:path'

const VIRTUAL_ID = 'virtual:auto-routes'
const RESOLVED_ID = '\0virtual:auto-routes'

/**
 * 递归扫描目录,生成路由配置
 * @param {string} dir - 扫描目录绝对路径
 * @param {string} base - 路由 base path
 * @returns {Array} 路由配置数组
 */
function scanRoutes(dir, base = '') {
  const routes = []

  if (!fs.existsSync(dir)) return routes

  const entries = fs.readdirSync(dir, { withFileTypes: true })

  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name)

    if (entry.isDirectory()) {
      // 递归处理子目录
      const children = scanRoutes(fullPath, `${base}/${entry.name}`)
      if (children.length > 0) {
        routes.push({
          path: `${base}/${entry.name}`,
          children,
        })
      }
    } else if (entry.isFile() && /\.(vue|jsx|tsx)$/.test(entry.name)) {
      const name = entry.name.replace(/\.(vue|jsx|tsx)$/, '')
      const routePath = name === 'index'
        ? base || '/'
        : `${base}/${name}`

      // 处理动态路由:[id].vue -> :id
      const normalizedPath = routePath.replace(/\[(\w+)\]/g, ':$1')

      routes.push({
        path: normalizedPath,
        component: fullPath,
        name: normalizedPath.replace(/\//g, '-').replace(/^-/, '') || 'home',
      })
    }
  }

  return routes
}

/**
 * 将路由配置转换为可执行的模块代码
 */
function generateRouteCode(routes) {
  const imports = []
  const routeObjects = []

  function processRoutes(routeList, level = 0) {
    return routeList.map((route) => {
      if (route.component) {
        const importName = `Page_${imports.length}`
        imports.push(
          `const ${importName} = () => import(${JSON.stringify(route.component)})`
        )
        return `{
          path: ${JSON.stringify(route.path)},
          name: ${JSON.stringify(route.name)},
          component: ${importName},
        }`
      } else {
        const children = processRoutes(route.children || [])
        return `{
          path: ${JSON.stringify(route.path)},
          children: [${children.join(',')}],
        }`
      }
    })
  }

  const routeStrings = processRoutes(routes)

  return `
${imports.join('\n')}

export const routes = [
  ${routeStrings.join(',\n  ')}
]

export default routes
  `.trim()
}

export default function vitePluginAutoRoutes(options = {}) {
  const {
    pagesDir = 'src/pages',
    extensions = ['vue', 'jsx', 'tsx'],
  } = options

  let pagesAbsPath
  let viteConfig

  return {
    name: 'vite-plugin-auto-routes',
    enforce: 'pre',

    configResolved(config) {
      viteConfig = config
      pagesAbsPath = path.resolve(config.root, pagesDir)
    },

    resolveId(id) {
      if (id === VIRTUAL_ID) {
        return RESOLVED_ID
      }
    },

    load(id) {
      if (id !== RESOLVED_ID) return null

      const routes = scanRoutes(pagesAbsPath)
      return generateRouteCode(routes)
    },

    configureServer(server) {
      // 监听 pages 目录变化,使虚拟模块失效
      server.watcher.add(pagesAbsPath)
      server.watcher.on('add', invalidate)
      server.watcher.on('unlink', invalidate)
      server.watcher.on('addDir', invalidate)
      server.watcher.on('unlinkDir', invalidate)

      function invalidate(file) {
        if (!file.startsWith(pagesAbsPath)) return

        const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
        if (mod) {
          server.moduleGraph.invalidateModule(mod)
          server.ws.send({ type: 'full-reload' })
        }
      }
    },
  }
}

使用方式:

// vite.config.js
import autoRoutes from './vite-plugin-auto-routes'

export default {
  plugins: [
    autoRoutes({ pagesDir: 'src/pages' }),
  ],
}
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from 'virtual:auto-routes'

export const router = createRouter({
  history: createWebHistory(),
  routes,
})

SSR(服务端渲染)

SSR 项目结构

project/
├── src/
│   ├── main.js          # 通用代码,导出 createApp 工厂函数
│   ├── entry-client.js  # 客户端入口:挂载到 DOM
│   ├── entry-server.js  # 服务端入口:返回渲染结果
│   └── App.vue
├── server.js            # Express 服务器
├── index.html           # HTML 模板
└── vite.config.js

src/main.js(通用代码):

import { createApp } from 'vue'
import App from './App.vue'
import { createRouter } from './router'

// 每次请求都需要创建全新的实例,避免状态污染
export function createVueApp() {
  const app = createApp(App)
  const router = createRouter()
  app.use(router)
  return { app, router }
}

src/entry-client.js:

import { createVueApp } from './main'

const { app, router } = createVueApp()

// 等待路由就绪后再挂载,确保 hydration 正确
router.isReady().then(() => {
  app.mount('#app')
})

src/entry-server.js:

import { renderToString } from 'vue/server-renderer'
import { createVueApp } from './main'

export async function render(url, manifest) {
  const { app, router } = createVueApp()

  await router.push(url)
  await router.isReady()

  const ctx = {}
  const html = await renderToString(app, ctx)

  // 提取预加载链接(需要 manifest)
  const preloadLinks = manifest
    ? renderPreloadLinks(ctx.modules, manifest)
    : ''

  return { html, preloadLinks }
}

function renderPreloadLinks(modules, manifest) {
  let links = ''
  const seen = new Set()
  modules.forEach((id) => {
    const files = manifest[id]
    if (files) {
      files.forEach((file) => {
        if (!seen.has(file)) {
          seen.add(file)
          const ext = file.split('.').pop()
          if (ext === 'js') {
            links += `<link rel="modulepreload" crossorigin href="${file}">`
          } else if (ext === 'css') {
            links += `<link rel="stylesheet" href="${file}">`
          }
        }
      })
    }
  })
  return links
}

开发环境 SSR 配置

server.js(开发 + 生产合一):

import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import express from 'express'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const isProd = process.env.NODE_ENV === 'production'

async function createExpressServer() {
  const app = express()

  let vite
  let template
  let render

  if (!isProd) {
    // 开发模式:使用 Vite 中间件
    const { createServer } = await import('vite')
    vite = await createServer({
      root: __dirname,
      appType: 'custom',          // 不使用 Vite 的 HTML 处理逻辑
      server: {
        middlewareMode: true,     // 中间件模式,不启动内置 HTTP 服务器
      },
    })

    // 挂载 Vite 中间件(处理模块热更新等)
    app.use(vite.middlewares)
  } else {
    // 生产模式:服务静态文件
    app.use(express.static(path.resolve(__dirname, 'dist/client'), {
      index: false, // 不自动提供 index.html,由 SSR 处理
    }))
  }

  app.use('*', async (req, res) => {
    const url = req.originalUrl

    try {
      if (!isProd) {
        // 开发模式:每次请求都读取最新的 HTML 和模块
        template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8')
        template = await vite.transformIndexHtml(url, template)
        // ssrLoadModule 自动处理 ESM 和热更新
        const serverEntry = await vite.ssrLoadModule('/src/entry-server.js')
        render = serverEntry.render
      } else {
        // 生产模式:缓存 template 和 render
        if (!template) {
          template = fs.readFileSync(
            path.resolve(__dirname, 'dist/client/index.html'),
            'utf-8'
          )
        }
        if (!render) {
          render = (await import('./dist/server/entry-server.js')).render
        }
      }

      const manifest = isProd
        ? JSON.parse(fs.readFileSync(
            path.resolve(__dirname, 'dist/client/.vite/ssr-manifest.json'),
            'utf-8'
          ))
        : {}

      const { html: appHtml, preloadLinks } = await render(url, manifest)

      const finalHtml = template
        .replace('<!--preload-links-->', preloadLinks)
        .replace('<!--app-html-->', appHtml)

      res.status(200).set({ 'Content-Type': 'text/html' }).end(finalHtml)
    } catch (e) {
      // 让 Vite 修复错误堆栈(映射回源码)
      if (vite) vite.ssrFixStacktrace(e)
      console.error(e)
      res.status(500).end(e.message)
    }
  })

  return app
}

createExpressServer().then((app) => {
  app.listen(3000, () => {
    console.log('SSR 服务器启动:http://localhost:3000')
  })
})

生产环境 SSR 构建

package.json scripts:

{
  "scripts": {
    "dev": "node server.js",
    "build": "npm run build:client && npm run build:server",
    "build:client": "vite build --outDir dist/client",
    "build:server": "vite build --outDir dist/server --ssr src/entry-server.js",
    "preview": "cross-env NODE_ENV=production node server.js"
  }
}

vite.config.js(SSR 项目):

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    // SSR manifest 用于生成 preload links
    ssrManifest: true,
  },
  ssr: {
    // 强制将这些包打包进 SSR bundle(不外部化)
    noExternal: ['some-lib-with-css', /^@my-org\//],
  },
})

SSR Options 参数表

参数名 类型 默认值 说明
ssr.target 'node' | 'webworker' 'node' SSR 构建目标环境
ssr.format 'esm' | 'cjs' 'esm' SSR 产物格式
ssr.noExternal string | RegExp | (string | RegExp)[] | true 强制将指定依赖打包进 SSR bundle。true 表示打包所有依赖
ssr.external string[] | true 强制将指定依赖外部化(不打包)。true 表示外部化所有依赖
ssr.resolve.conditions string[] 解析依赖的 package.json exports 条件,叠加到默认条件上
ssr.resolve.externalConditions string[] ['node'] 解析外部化 SSR 依赖时使用的条件
ssr.optimizeDeps.enabled boolean 是否为 SSR 启用依赖优化

中间件模式

中间件模式允许 Vite 作为现有 HTTP 服务器的中间件,而非独立服务器。

server.middlewareMode 配置

参数名 类型 默认值 说明
server.middlewareMode boolean false 启用中间件模式,Vite 不创建 HTTP 服务器
appType 'spa' | 'mpa' | 'custom' 'spa' 指定应用类型。custom 表示完全自定义 HTML 处理

与 Express 集成

import express from 'express'
import { createServer as createViteServer } from 'vite'

const app = express()

const vite = await createViteServer({
  server: { middlewareMode: true },
  appType: 'custom',
})

// 挂载 Vite 中间件(必须在自定义路由之前)
app.use(vite.middlewares)

// 自定义 API 路由
app.get('/api/data', (req, res) => {
  res.json({ data: 'from server' })
})

// 兜底:所有非 API 路由交给 Vite/SSR 处理
app.use('*', async (req, res) => {
  // ... SSR 逻辑
})

app.listen(3000)

与 Koa 集成

import Koa from 'koa'
import { createServer as createViteServer } from 'vite'
import { createReadStream } from 'node:fs'
import { resolve } from 'node:path'

const app = new Koa()

const vite = await createViteServer({
  server: { middlewareMode: true },
  appType: 'custom',
})

// 将 Connect 风格的中间件转换为 Koa 中间件
app.use(async (ctx, next) => {
  await new Promise((resolve, reject) => {
    vite.middlewares(ctx.req, ctx.res, (err) => {
      if (err) reject(err)
      else resolve()
    })
  })
  await next()
})

// Koa 自定义路由
app.use(async (ctx) => {
  if (ctx.path.startsWith('/api')) {
    ctx.body = { message: 'API response' }
    return
  }

  // SSR 处理
  const template = await vite.transformIndexHtml(
    ctx.url,
    '<html><body><!--app--></body></html>'
  )
  ctx.type = 'text/html'
  ctx.body = template
})

app.listen(3000, () => {
  console.log('Koa + Vite 服务器启动:http://localhost:3000')
})

JavaScript API

createServer(inlineConfig)

以编程方式创建 Vite 开发服务器。

参数:

参数名 类型 默认值 说明
inlineConfig InlineConfig {} Vite 配置(与配置文件深度合并)

返回值: Promise<ViteDevServer>

ViteDevServer 完整 API

属性/方法 类型 说明
config ResolvedConfig 最终解析后的 Vite 配置
httpServer http.Server | null Node.js HTTP 服务器实例。中间件模式下为 null
watcher FSWatcher Chokidar 文件监听器实例
ws WebSocketServer WebSocket 服务器,用于 HMR 通信
moduleGraph ModuleGraph 模块依赖图,追踪模块间的导入关系
pluginContainer PluginContainer 插件容器,可调用插件钩子
transformRequest(url, options?) Promise<TransformResult | null> 对指定 URL 的模块执行 Vite 管道(解析、加载、转换)
ssrLoadModule(url, options?) Promise<Record<string, any>> 加载 SSR 模块(Node.js 环境中执行)
ssrFixStacktrace(e) void 修复 SSR 错误的堆栈信息,映射回源码位置
listen(port?, isRestart?) Promise 启动 HTTP 服务器监听
close() Promise 关闭服务器,停止文件监听和 WebSocket
restart(forceOptimize?) Promise 重启服务器。forceOptimize=true 强制重新优化依赖
printUrls() void 打印服务器地址信息
bindCLIShortcuts(options?) void 绑定 CLI 快捷键(q 退出,r 重启等)

编程方式使用示例:

import { createServer } from 'vite'

const vite = await createServer({
  root: process.cwd(),
  server: {
    port: 5173,
    host: true,
  },
})

await vite.listen()
vite.printUrls()

// 手动触发模块转换
const result = await vite.transformRequest('/src/main.js')
console.log(result?.code)

// 关闭服务器
process.on('SIGINT', async () => {
  await vite.close()
  process.exit(0)
})

build(inlineConfig)

以编程方式执行 Vite 构建。

参数:

参数名 类型 默认值 说明
inlineConfig InlineConfig {} Vite 配置(与配置文件深度合并)

返回值: Promise<RollupOutput | RollupOutput[] | RollupWatcher>

import { build } from 'vite'

// 基础构建
const result = await build({
  root: process.cwd(),
  build: {
    outDir: 'dist',
  },
})

// result 是 RollupOutput 数组,包含所有输出文件信息
console.log('输出文件:', result.output.map(f => f.fileName))

// watch 模式(返回 RollupWatcher)
const watcher = await build({
  build: {
    watch: {},
  },
})

watcher.on('event', (event) => {
  if (event.code === 'BUNDLE_END') {
    console.log('构建完成,耗时:', event.duration, 'ms')
  }
})

preview(inlineConfig)

以编程方式启动预览服务器(服务 build 产物)。

参数:

参数名 类型 说明
inlineConfig InlineConfig Vite 配置

返回值: Promise<PreviewServer>

import { preview } from 'vite'

const previewServer = await preview({
  preview: {
    port: 4173,
    open: true,
  },
})

previewServer.printUrls()

resolveConfig(inlineConfig, command, mode)

解析 Vite 配置(合并配置文件与 inlineConfig),不启动服务器。

参数:

参数名 类型 说明
inlineConfig InlineConfig 内联配置
command 'build' | 'serve' 模拟的命令
mode string 模拟的 mode

返回值: Promise<ResolvedConfig>

import { resolveConfig } from 'vite'

const config = await resolveConfig({}, 'build', 'production')
console.log('解析后的 base:', config.base)
console.log('所有插件:', config.plugins.map(p => p.name))

loadConfigFromFile(configEnv, configFile?)

加载 Vite 配置文件。

参数:

参数名 类型 默认值 说明
configEnv.command 'build' | 'serve' 必填 当前命令
configEnv.mode string 必填 当前 mode
configFile string 自动查找 配置文件路径,默认自动查找 vite.config.*

返回值: Promise<{ path, config, dependencies } | null>

import { loadConfigFromFile } from 'vite'

const result = await loadConfigFromFile(
  { command: 'build', mode: 'production' },
  './vite.config.ts'
)

if (result) {
  console.log('配置文件路径:', result.path)
  console.log('配置内容:', result.config)
  console.log('依赖的文件:', result.dependencies)
}

性能优化

开发服务器优化

server.warmup 预热关键文件

在服务器启动时提前转换关键模块,减少首次访问延迟:

export default {
  server: {
    warmup: {
      // 预热客户端模块(路由入口、核心组件等)
      clientFiles: [
        './src/main.js',
        './src/components/Layout.vue',
        './src/router/index.js',
      ],
      // 预热 SSR 模块
      ssrFiles: [
        './src/entry-server.js',
      ],
    },
  },
}
参数名 类型 说明
server.warmup.clientFiles string[] 预热的客户端模块路径(支持 glob)
server.warmup.ssrFiles string[] 预热的 SSR 模块路径(支持 glob)

optimizeDeps 减少首次启动扫描

export default {
  optimizeDeps: {
    // 明确指定需要预构建的依赖(避免 Vite 全量扫描)
    include: [
      'vue',
      'vue-router',
      'pinia',
      'axios',
      'lodash-es',
      // 对于深层导入也要声明
      'some-lib > nested-dep',
    ],
    // 排除不需要预构建的依赖
    exclude: ['@vite/client', '@vite/env'],
    // 对需要转换的依赖进行 esbuild 配置
    esbuildOptions: {
      target: 'esnext',
    },
    // 强制重新构建(调试时使用)
    force: false,
  },
}

optimizeDeps 参数表:

参数名 类型 默认值 说明
optimizeDeps.entries string | string[] 指定扫描入口,默认扫描 index.html
optimizeDeps.exclude string[] [] 排除预构建的依赖列表
optimizeDeps.include string[] [] 强制预构建的依赖列表(即使被 exclude 排除)
optimizeDeps.esbuildOptions EsbuildOptions 传递给 esbuild 的选项
optimizeDeps.force boolean false 忽略缓存,强制重新预构建
optimizeDeps.holdUntilCrawlEnd boolean true 是否等待爬取结束再发送预构建模块

构建产物优化

代码分割策略(manualChunks)

export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // Vue 相关库单独打包
          if (id.includes('/node_modules/vue') ||
              id.includes('/node_modules/@vue')) {
            return 'vue-vendor'
          }

          // UI 组件库单独打包
          if (id.includes('/node_modules/element-plus') ||
              id.includes('/node_modules/@element-plus')) {
            return 'element-plus'
          }

          // 工具库单独打包
          if (id.includes('/node_modules/lodash-es') ||
              id.includes('/node_modules/dayjs')) {
            return 'utils'
          }

          // 其余第三方依赖合并为 vendor
          if (id.includes('/node_modules/')) {
            return 'vendor'
          }

          // 业务代码按模块分割
          if (id.includes('/src/views/admin/')) {
            return 'admin'
          }
        },
      },
    },
  },
}

对象形式(静态声明):

export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui': ['element-plus'],
          'utils': ['lodash-es', 'dayjs', 'axios'],
        },
      },
    },
  },
}

build.rollupOptions.output 参数表

参数名 类型 默认值 说明
chunkFileNames string | function 'assets/[name]-[hash].js' 非入口 chunk 的文件名模板
entryFileNames string | function 'assets/[name]-[hash].js' 入口 chunk 的文件名模板
assetFileNames string | function 'assets/[name]-[hash][extname]' 静态资源文件名模板
manualChunks Record<string, string[]> | function 自定义 chunk 分割策略
format 'es' | 'cjs' | 'umd' | 'iife' | 'system' 'es' 输出格式
globals Record<string, string> UMD/IIFE 格式中外部依赖的全局变量名
inlineDynamicImports boolean false 是否将动态导入内联(不分割 chunk)
sourcemap boolean | 'inline' | 'hidden' false 是否生成 sourcemap

文件名模板可用占位符:

占位符 说明
[name] chunk 名称
[hash] 基于内容的 hash
[format] 输出格式
[extname] 文件扩展名(含点号)

build 核心参数表

参数名 类型 默认值 说明
build.target string | string[] 'modules' 浏览器兼容目标,传 esbuild target 格式
build.outDir string 'dist' 构建输出目录
build.assetsDir string 'assets' 静态资源子目录名
build.assetsInlineLimit number 4096 小于此字节的资源转为 base64 内联
build.cssCodeSplit boolean true 启用 CSS 代码分割
build.sourcemap boolean | 'inline' | 'hidden' false 生成 sourcemap
build.rollupOptions RollupOptions {} 传递给 Rollup 的配置
build.lib LibraryOptions 构建库模式配置
build.ssr boolean | string 启用 SSR 构建,传字符串时指定入口
build.ssrManifest boolean | string false 生成 SSR manifest 文件
build.reportCompressedSize boolean true 在终端输出 gzip 压缩后的大小
build.chunkSizeWarningLimit number 500 chunk 大小警告阈值(KB)
build.minify boolean | 'terser' | 'esbuild' 'esbuild' 压缩工具选择
build.terserOptions TerserOptions Terser 压缩选项(minify: 'terser' 时有效)
build.emptyOutDir boolean true 构建前清空输出目录
build.copyPublicDir boolean true 是否将 public 目录文件复制到 outDir

Tree Shaking 优化

在 package.json 中声明无副作用:

{
  "name": "my-lib",
  "sideEffects": false
}

或精确声明有副作用的文件:

{
  "sideEffects": [
    "*.css",
    "./src/polyfills.js"
  ]
}

避免 import * 的写法:

// 不推荐:整个 lodash 都会被打包
import _ from 'lodash'
import * as _ from 'lodash'

// 推荐:只打包使用的函数
import { debounce, throttle } from 'lodash-es'

// 推荐:按需导入(lodash-es 支持 tree shaking)
import debounce from 'lodash-es/debounce'

在 vite.config.js 中配合 optimizeDeps:

export default {
  optimizeDeps: {
    include: ['lodash-es'],
  },
}

大包分析

使用 rollup-plugin-visualizer:

npm install -D rollup-plugin-visualizer
import { defineConfig } from 'vite'
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    visualizer({
      open: true,           // 构建后自动打开浏览器
      filename: 'stats.html', // 分析报告文件名
      gzipSize: true,       // 显示 gzip 后的大小
      brotliSize: true,     // 显示 brotli 后的大小
      template: 'treemap',  // 可选 'sunburst', 'network', 'raw-data', 'list'
    }),
  ],
  build: {
    // 调整 chunk 大小警告阈值
    chunkSizeWarningLimit: 1000, // KB
    // 关闭压缩大小报告(大型项目可加快构建速度)
    reportCompressedSize: false,
  },
})

环境变量进阶与多环境部署

loadEnv() 在配置文件中使用

loadEnv 允许在 vite.config.js 中读取 .env 文件中的变量:

import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig(({ command, mode }) => {
  // 第三个参数为前缀过滤,'' 表示加载所有变量(包括 VITE_ 前缀和无前缀)
  const env = loadEnv(mode, process.cwd(), '')

  return {
    plugins: [vue()],

    define: {
      // 将 Node.js 环境变量注入到客户端代码
      __APP_VERSION__: JSON.stringify(env.npm_package_version),
      __BUILD_TIME__: JSON.stringify(new Date().toISOString()),
    },

    server: {
      proxy: {
        '/api': {
          target: env.VITE_API_BASE_URL || 'http://localhost:8080',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, ''),
        },
      },
    },

    build: {
      sourcemap: env.VITE_SOURCEMAP === 'true',
    },
  }
})

loadEnv 参数:

参数名 类型 默认值 说明
mode string 必填 环境名(development/production/staging 等)
envDir string 必填 .env 文件所在目录,通常为 process.cwd()
prefixes string | string[] 'VITE_' 变量前缀过滤,传 '' 加载所有变量

多环境配置示例

目录结构:

.env                    # 所有环境共享的基础变量
.env.development        # 本地开发
.env.staging            # 测试环境
.env.production         # 生产环境

.env(基础):

VITE_APP_NAME=My App
VITE_APP_VERSION=1.0.0

.env.development:

VITE_API_BASE_URL=http://localhost:8080
VITE_SOURCEMAP=true
VITE_MOCK_ENABLED=true
VITE_LOG_LEVEL=debug

.env.staging:

VITE_API_BASE_URL=https://staging-api.example.com
VITE_SOURCEMAP=true
VITE_MOCK_ENABLED=false
VITE_LOG_LEVEL=info

.env.production:

VITE_API_BASE_URL=https://api.example.com
VITE_SOURCEMAP=false
VITE_MOCK_ENABLED=false
VITE_LOG_LEVEL=error

package.json scripts:

{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "build:staging": "vite build --mode staging",
    "build:production": "vite build --mode production",
    "preview": "vite preview"
  }
}

TypeScript ImportMetaEnv 扩展:

// src/env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  // 基础(Vite 内置)
  readonly MODE: string
  readonly BASE_URL: string
  readonly PROD: boolean
  readonly DEV: boolean
  readonly SSR: boolean

  // 自定义变量
  readonly VITE_APP_NAME: string
  readonly VITE_APP_VERSION: string
  readonly VITE_API_BASE_URL: string
  readonly VITE_SOURCEMAP: string
  readonly VITE_MOCK_ENABLED: string
  readonly VITE_LOG_LEVEL: 'debug' | 'info' | 'warn' | 'error'
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

在代码中使用:

// 有类型提示和自动补全
const apiUrl = import.meta.env.VITE_API_BASE_URL
const isMockEnabled = import.meta.env.VITE_MOCK_ENABLED === 'true'
const appName = import.meta.env.VITE_APP_NAME

Preview Options 完整配置

preview 选项用于配置 vite preview 命令启动的预览服务器(用于本地预览构建产物)。

参数名 类型 默认值 说明
preview.host string | boolean 'localhost' 监听的主机名。true 或 '0.0.0.0' 监听所有地址
preview.allowedHosts string[] | true 允许访问的主机名列表。true 表示允许所有主机
preview.port number 4173 监听端口号
preview.strictPort boolean false 端口占用时是否直接退出(false 则自动递增)
preview.https https.ServerOptions 启用 HTTPS,传入证书配置
preview.open boolean | string false 启动后自动打开浏览器。字符串时指定打开的 URL 路径
preview.proxy Record<string, ProxyOptions> 代理配置,格式与 server.proxy 相同
preview.cors boolean | CorsOptions 配置 CORS
preview.headers OutgoingHttpHeaders 指定响应头
export default {
  preview: {
    host: '0.0.0.0',
    port: 4173,
    strictPort: true,
    https: {
      key: fs.readFileSync('./certs/key.pem'),
      cert: fs.readFileSync('./certs/cert.pem'),
    },
    open: true,
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
      },
    },
    cors: true,
    headers: {
      'X-Custom-Header': 'my-value',
      'Cache-Control': 'no-cache',
    },
  },
}

Worker Options 完整配置

worker 选项用于配置 Web Worker 的打包方式。

参数名 类型 默认值 说明
worker.format 'es' | 'iife' 'iife' Worker bundle 的输出格式。iife 兼容性更好,es 支持 import
worker.plugins () => Plugin[] 应用于 Worker bundle 的 Vite 插件(工厂函数形式)
worker.rollupOptions RollupOptions Worker bundle 的 Rollup 配置
export default {
  worker: {
    // 使用 ES 模块格式(支持在 Worker 中使用 import)
    format: 'es',

    // Worker 专用插件(使用工厂函数避免实例共享)
    plugins: () => [myWorkerPlugin()],

    rollupOptions: {
      output: {
        // Worker 产物命名规则
        entryFileNames: 'workers/[name]-[hash].js',
      },
    },
  },
}

在代码中使用 Worker:

// 使用 ?worker 后缀导入
import MyWorker from './my.worker.js?worker'

const worker = new MyWorker()
worker.postMessage({ type: 'start', data: [1, 2, 3] })
worker.onmessage = (e) => {
  console.log('Worker 结果:', e.data)
}

// 内联 Worker(打包进主 bundle)
import InlineWorker from './my.worker.js?worker&inline'

// SharedWorker
import SharedWorkerClass from './shared.worker.js?sharedworker'
const sharedWorker = new SharedWorkerClass()

Worker 文件示例(my.worker.js):

// Worker 内不能访问 window、document 等 DOM API
self.onmessage = (e) => {
  const { type, data } = e.data

  if (type === 'start') {
    // 执行计算密集型任务
    const result = data.reduce((sum, n) => sum + n, 0)
    self.postMessage({ result })
  }
}

打包与部署

Docker 多阶段构建

# Stage 1:构建阶段
FROM node:22-alpine AS build

WORKDIR /app

# 先复制依赖文件,利用 Docker 层缓存
COPY package.json package-lock.json ./
RUN npm ci --prefer-offline

# 复制源代码并构建
COPY . .
RUN npm run build

# Stage 2:生产阶段(仅包含静态文件)
FROM nginx:1.25-alpine AS production

# 复制构建产物
COPY --from=build /app/dist /usr/share/nginx/html

# 复制自定义 Nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

nginx.conf(SPA 路由 + 性能优化):

server {
    listen 80;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

    # 开启 gzip 压缩
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied expired no-cache no-store private auth;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/javascript
        application/json
        application/xml+rss
        image/svg+xml;

    # 静态资源长期缓存(Vite 产物带 hash,可永久缓存)
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # index.html 不缓存(确保用户获取最新版本)
    location / {
        try_files $uri $uri/ /index.html;
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
        add_header Expires "0";
    }

    # 安全响应头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}

.dockerignore:

node_modules
dist
.git
.gitignore
*.md
.env*

构建和运行:

docker build -t my-vite-app .
docker run -p 80:80 my-vite-app

GitHub Actions CI/CD

部署到 GitHub Pages:

# .github/workflows/deploy.yml
name: Deploy to GitHub Pages

on:
  push:
    branches: [main]
  workflow_dispatch:

# 设置 GITHUB_TOKEN 权限
permissions:
  contents: read
  pages: write
  id-token: write

# 防止并发部署
concurrency:
  group: pages
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build
        env:
          VITE_API_BASE_URL: ${{ secrets.VITE_API_BASE_URL }}
          NODE_ENV: production

      - name: Setup Pages
        uses: actions/configure-pages@v4

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: dist

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

部署到 VPS(通过 SSH):

# .github/workflows/deploy-vps.yml
name: Deploy to VPS

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      - name: Install and Build
        run: |
          npm ci
          npm run build
        env:
          VITE_API_BASE_URL: ${{ secrets.VITE_API_BASE_URL }}

      - name: Deploy to VPS via SCP
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          source: 'dist/*'
          target: '/var/www/my-app'
          strip_components: 1

      - name: Restart Nginx
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          script: nginx -s reload

CDN 部署(base 配置)

部署到子路径:

export default {
  base: '/my-app/',  // 所有资源路径前缀
}

部署到 CDN:

export default defineConfig(({ command }) => ({
  base: command === 'build'
    ? 'https://cdn.example.com/assets/my-app/'
    : '/',
}))

动态 base(运行时注入):

// vite.config.js
export default {
  experimental: {
    renderBuiltUrl(filename, { hostId, hostType, type }) {
      if (type === 'asset') {
        return `https://cdn.example.com/${filename}`
      }
      // 返回 undefined 使用默认处理
    },
  },
}

Monorepo 配置

目录结构

my-monorepo/
├── package.json          # 根 package.json(workspaces 配置)
├── pnpm-workspace.yaml   # pnpm workspaces 配置(若用 pnpm)
├── packages/
│   ├── ui/               # 组件库
│   │   ├── package.json
│   │   ├── vite.config.js
│   │   └── src/
│   ├── app/              # 主应用
│   │   ├── package.json
│   │   ├── vite.config.js
│   │   └── src/
│   └── shared/           # 共享工具代码
│       ├── package.json
│       └── src/
└── node_modules/

根 package.json(npm workspaces):

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}

pnpm-workspace.yaml:

packages:
  - 'packages/*'

resolve.dedupe 解决 Monorepo 依赖问题

在 Monorepo 中,不同包可能各自安装了同一依赖的不同副本,导致问题(如 Vue 实例不唯一)。

// packages/app/vite.config.js
import { defineConfig } from 'vite'
import { resolve } from 'path'
import vue from '@vitejs/plugin-vue'

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

  resolve: {
    // 强制使用根 node_modules 中的单一副本
    dedupe: ['vue', 'vue-router', 'pinia', 'react', 'react-dom'],

    // 若使用 pnpm,symlink 指向真实路径,设为 false 避免路径解析问题
    preserveSymlinks: false,

    alias: {
      // 将 workspace 包别名指向源码(避免引用编译后版本)
      '@my-org/ui': resolve(__dirname, '../ui/src'),
      '@my-org/shared': resolve(__dirname, '../shared/src'),
    },
  },

  optimizeDeps: {
    // workspace 包的源码中如果有未预构建的依赖,需要显式包含
    include: [
      'vue',
      'pinia',
      // 对于 workspace 包中的深层依赖
      '@my-org/ui > some-dep',
    ],
    // 排除 workspace 包本身(它们会被 Vite 直接处理)
    exclude: ['@my-org/ui', '@my-org/shared'],
  },
})

完整 Monorepo vite.config.js 示例

packages/ui/vite.config.js(库模式):

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

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

  build: {
    // 库模式构建
    lib: {
      entry: resolve(__dirname, 'src/index.js'),
      name: 'MyUI',
      formats: ['es', 'cjs'],
      fileName: (format) => `my-ui.${format}.js`,
    },
    rollupOptions: {
      // 外部化 peer dependencies
      external: ['vue'],
      output: {
        globals: { vue: 'Vue' },
        // ES 模块保留目录结构(方便 tree shaking)
        preserveModules: true,
        preserveModulesRoot: 'src',
      },
    },
    // 生成类型声明
    emptyOutDir: true,
  },
})

packages/app/vite.config.js(应用模式):

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

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

  resolve: {
    dedupe: ['vue', 'vue-router', 'pinia'],
    alias: {
      '@': resolve(__dirname, 'src'),
      '@my-org/ui': resolve(__dirname, '../ui/src'),
      '@my-org/shared': resolve(__dirname, '../shared/src'),
    },
  },

  optimizeDeps: {
    include: [
      'vue',
      'vue-router',
      'pinia',
      // 处理 workspace 包内的 CommonJS 依赖
      '@my-org/ui > some-cjs-lib',
    ],
  },

  server: {
    fs: {
      // 允许访问 packages/ 目录(跨 workspace 引用时需要)
      allow: ['../..'],
    },
  },
})

高级插件示例:自定义 Mock 插件

完整可运行的 vite-plugin-mock 实现:

// vite-plugin-mock/index.js
import fs from 'node:fs'
import path from 'node:path'
import { createRequire } from 'node:module'

const require = createRequire(import.meta.url)

/**
 * 解析请求体
 */
async function parseBody(req) {
  return new Promise((resolve) => {
    let body = ''
    req.on('data', (chunk) => { body += chunk.toString() })
    req.on('end', () => {
      try {
        resolve(body ? JSON.parse(body) : {})
      } catch {
        resolve({})
      }
    })
  })
}

/**
 * 从 mock 目录加载所有 mock 文件
 */
function loadMockFiles(mockDir) {
  const mocks = []

  if (!fs.existsSync(mockDir)) return mocks

  const files = fs.readdirSync(mockDir).filter(f => f.endsWith('.js'))

  for (const file of files) {
    const filePath = path.join(mockDir, file)
    try {
      // 清除 require 缓存,支持热重载
      delete require.cache[filePath]
      const mockModule = require(filePath)
      const mockList = Array.isArray(mockModule) ? mockModule : [mockModule]
      mocks.push(...mockList)
    } catch (e) {
      console.error(`[vite-plugin-mock] 加载 mock 文件失败:${file}`, e.message)
    }
  }

  return mocks
}

/**
 * 匹配 mock 路由
 */
function matchMock(mocks, url, method) {
  return mocks.find((mock) => {
    const methodMatch = !mock.method || mock.method.toUpperCase() === method.toUpperCase()
    const urlMatch = typeof mock.url === 'string'
      ? mock.url === url.split('?')[0]
      : mock.url instanceof RegExp
        ? mock.url.test(url)
        : false
    return methodMatch && urlMatch
  })
}

export default function vitePluginMock(options = {}) {
  const {
    mockDir = 'mock',      // mock 文件目录
    enable = true,         // 是否启用
    logger = true,         // 是否打印日志
  } = options

  return {
    name: 'vite-plugin-mock',
    apply: 'serve',        // 仅在开发服务器中启用

    configureServer(server) {
      if (!enable) return

      const mockAbsDir = path.resolve(process.cwd(), mockDir)
      let mocks = loadMockFiles(mockAbsDir)

      // 监听 mock 目录变化,自动重新加载
      server.watcher.add(mockAbsDir)
      server.watcher.on('change', (file) => {
        if (file.startsWith(mockAbsDir)) {
          mocks = loadMockFiles(mockAbsDir)
          if (logger) {
            console.log(`[vite-plugin-mock] mock 文件已更新:${path.basename(file)}`)
          }
        }
      })

      // 在内置中间件之前插入 mock 中间件
      server.middlewares.use(async (req, res, next) => {
        const matched = matchMock(mocks, req.url, req.method)

        if (!matched) return next()

        if (logger) {
          console.log(`[vite-plugin-mock] ${req.method} ${req.url}`)
        }

        // 支持延迟响应
        if (matched.delay && matched.delay > 0) {
          await new Promise(resolve => setTimeout(resolve, matched.delay))
        }

        const body = await parseBody(req)

        // 支持函数形式的响应(动态响应)
        let responseData
        if (typeof matched.response === 'function') {
          responseData = await matched.response({
            url: req.url,
            method: req.method,
            body,
            headers: req.headers,
            query: Object.fromEntries(new URL(req.url, 'http://localhost').searchParams),
          })
        } else {
          responseData = matched.response
        }

        const statusCode = matched.statusCode || 200

        res.writeHead(statusCode, {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*',
          ...(matched.headers || {}),
        })

        res.end(JSON.stringify(responseData))
      })
    },
  }
}

Mock 文件示例(mock/user.js):

// mock/user.js
module.exports = [
  {
    url: '/api/users',
    method: 'GET',
    delay: 200, // 模拟 200ms 延迟
    response: ({ query }) => {
      const page = parseInt(query.page) || 1
      const pageSize = parseInt(query.pageSize) || 10

      const users = Array.from({ length: pageSize }, (_, i) => ({
        id: (page - 1) * pageSize + i + 1,
        name: `用户 ${(page - 1) * pageSize + i + 1}`,
        email: `user${i + 1}@example.com`,
        role: i % 3 === 0 ? 'admin' : 'user',
      }))

      return {
        code: 0,
        message: 'success',
        data: {
          list: users,
          total: 100,
          page,
          pageSize,
        },
      }
    },
  },

  {
    url: '/api/users/login',
    method: 'POST',
    delay: 500,
    response: ({ body }) => {
      const { username, password } = body

      if (username === 'admin' && password === '123456') {
        return {
          code: 0,
          message: '登录成功',
          data: {
            token: 'mock-jwt-token-' + Date.now(),
            userInfo: { id: 1, name: 'Admin', role: 'admin' },
          },
        }
      }

      return {
        code: 401,
        message: '用户名或密码错误',
        data: null,
      }
    },
  },

  {
    url: /^\/api\/users\/\d+$/,  // 正则匹配
    method: 'GET',
    response: ({ url }) => {
      const id = url.split('/').pop()
      return {
        code: 0,
        data: { id: parseInt(id), name: `用户 ${id}`, email: `user${id}@example.com` },
      }
    },
  },
]

vite.config.js 使用:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import mockPlugin from './vite-plugin-mock'

export default defineConfig(({ command }) => ({
  plugins: [
    vue(),
    mockPlugin({
      mockDir: 'mock',
      enable: command === 'serve', // 仅开发环境启用
      logger: true,
    }),
  ],
}))

踩坑与最佳实践清单

SSR Hydration 不匹配问题

问题描述: 服务端渲染的 HTML 与客户端 Vue 重新渲染的结果不一致,导致 hydration 失败,出现闪烁或警告。

常见原因及解决方案:

// 错误:在服务端和客户端使用不一致的随机值
const id = Math.random() // 每次调用结果不同

// 错误:直接使用 Date.now()
const timestamp = Date.now()

// 正确:使用可预测的、基于请求的数据
// 在 SSR 中通过 provide/inject 传递,确保客户端复水时一致

// 错误:服务端访问了 window 对象
if (window !== undefined) { ... }

// 正确:使用平台判断
if (typeof window !== 'undefined') { ... }
// 或在 onMounted 中处理(仅在客户端运行)
onMounted(() => {
  // 这里的代码只在客户端执行
})

build.target 和 define 组合使用时的注意事项

问题:build.target 低于某些语法支持版本时,define 替换后的代码可能无法正确压缩。

// 问题示例
export default {
  build: {
    target: 'es2015', // 目标 ES2015
  },
  define: {
    // 这里替换的是字面量,esbuild 会进行常量折叠
    __DEV__: false,
    // 注意:不要 define 对象或数组,只 define 基本类型
    __CONFIG__: JSON.stringify({ key: 'value' }), // 正确
  },
}

// 陷阱:define 的值在替换时是直接字符串替换,不是 AST 转换
// 错误:这会替换成 undefined(字符串)
define: {
  'process.env.NODE_ENV': undefined, // 错误
}

// 正确:始终用 JSON.stringify 包裹字符串值
define: {
  'process.env.NODE_ENV': JSON.stringify('production'), // 正确
}

manualChunks 循环依赖导致 chunk 爆炸

问题: 错误的 manualChunks 配置可能导致模块被重复打包到多个 chunk 中,造成产物体积暴增。

// 危险:如果 A 依赖 B,B 依赖 A,强行拆分会导致循环
// 正确做法:使用函数形式,让 Rollup 帮助处理依赖关系

// 推荐:先分析依赖关系,再配置(用 visualizer 先可视化)
build: {
  rollupOptions: {
    output: {
      manualChunks(id) {
        // 不要把相互依赖的模块拆分到不同 chunk
        if (id.includes('/node_modules/')) {
          // 安全做法:按大的包名分组,不要过细拆分
          const match = id.match(/node_modules\/([^/]+)/)
          if (match) {
            const pkg = match[1]
            // 只对体积大的包单独分割
            if (['lodash-es', 'echarts', 'monaco-editor'].includes(pkg)) {
              return pkg
            }
            return 'vendor'
          }
        }
      },
    },
  },
}

插件 enforce 顺序问题

执行顺序总览:

enforce: 'pre' 插件
  → Vite 核心插件(HTML、alias 等)
    → 无 enforce 插件
      → Vite 构建插件
        → enforce: 'post' 插件

常见误区:

// 错误:在无 enforce 插件中尝试处理原始 import alias
// 此时 alias 已被 Vite 核心插件处理,拿到的已经是解析后的路径

// 正确:若需要在 alias 解析前处理,使用 enforce: 'pre'
{
  name: 'my-resolver',
  enforce: 'pre',
  resolveId(source) {
    // 此时 source 是原始导入字符串,alias 尚未替换
  }
}

// transform 插件的顺序同样受 enforce 影响
// 若需要在 Vue/React 编译前处理源码,使用 enforce: 'pre'
// 若需要在编译后处理,使用 enforce: 'post'

server.fs.deny 拦截 .env 文件

默认行为: Vite 默认会拦截对 .env 文件的直接 HTTP 请求,防止敏感信息泄露。

// 注意:Vite 默认拒绝访问 .env 文件
// 如果你的 API 代理路径包含 '.env' 字符串,会被误拦截

// 错误的代理配置(路径含 .env)
server: {
  proxy: {
    '/api/.env-config': 'http://backend.com', // 会被 Vite 拦截!
  }
}

// 查看默认 fs.deny 规则:
server: {
  fs: {
    deny: ['.env', '.env.*', '*.{crt,pem}'] // Vite 默认值
  }
}

// 若确实需要允许某类文件(谨慎操作):
server: {
  fs: {
    // 移除 .env 拦截(不推荐)
    deny: ['*.{crt,pem}'],
  }
}

Worker 中不能使用 window

// 错误:Worker 中没有 window 对象
self.onmessage = (e) => {
  window.location.href // ReferenceError: window is not defined
  document.querySelector('.app') // ReferenceError: document is not defined
  localStorage.getItem('key') // Worker 中不可用
}

// 正确:Worker 中使用 self 代替 window,且只能访问受限 API
self.onmessage = (e) => {
  // 可用:self, postMessage, fetch, XMLHttpRequest, WebSocket
  // 可用:setTimeout, setInterval, Promise, crypto
  // 可用:IndexedDB(通过 self.indexedDB)
  // 不可用:window, document, localStorage, sessionStorage, alert

  fetch('/api/data').then(r => r.json()).then(data => {
    self.postMessage(data)
  })
}

其他最佳实践

避免在 vite.config.js 中进行大量文件系统操作:

// 不推荐:在配置中使用大量 glob(每次启动都会执行)
import { glob } from 'glob'
const entries = glob.sync('src/pages/**/*.vue') // 每次启动都扫描

// 推荐:将扫描逻辑放到插件的 buildStart 钩子中(仅构建时执行一次)
// 或放到 configureServer 中(仅开发时执行)

正确使用 this.emitFile 生成额外文件:

// 在插件中生成额外的输出文件
{
  name: 'my-plugin',
  generateBundle(options, bundle) {
    // 生成额外文件
    this.emitFile({
      type: 'asset',
      fileName: 'manifest.json',
      source: JSON.stringify({ version: '1.0.0' }),
    })
  }
}

CSS Modules 命名冲突:

// vite.config.js
export default {
  css: {
    modules: {
      // 开发模式:保留原始类名方便调试
      generateScopedName: process.env.NODE_ENV === 'development'
        ? '[name]__[local]'
        : '[hash:base64:8]',
      // 避免不同模块的类名哈希冲突
      hashPrefix: 'my-app',
    },
  },
}

避免在生产构建中包含开发工具:

// 正确做法:使用 import.meta.env.DEV 进行条件导入
if (import.meta.env.DEV) {
  // 这段代码和导入会在生产构建中被 tree-shaking 移除
  const { setupDevtools } = await import('./devtools')
  setupDevtools(app)
}

最佳实践

插件的 enforce 字段明确执行时机:插件不声明 enforce 时默认在 Vite 核心插件之后运行。需要在 resolve 之前介入(如路径别名插件)用 enforce: 'pre';需要在 build 输出之后处理(如 HTML 注入)用 enforce: 'post'。混淆时机是插件 Bug 最常见的根源。

SSR 插件用 apply 字段区分 serve/build:部分插件(如热更新辅助)只在 serve 阶段有意义;另一些(如 CDN 替换)只在 build 阶段需要。用 apply: 'serve'apply: 'build' 缩小插件作用范围,避免 SSR 构建时加载无用插件。

resolveId 返回虚拟模块时加 \0 前缀:Vite/Rollup 约定以 \0 开头的 ID 为虚拟模块,不会被其他插件(如 node_modules 解析)拦截处理,避免路径冲突。

resolveId(id) {
  if (id === 'virtual:my-module') return '\0virtual:my-module'
},
load(id) {
  if (id === '\0virtual:my-module') return `export const msg = 'hello'`
},

createServermiddlewareMode 用于嵌入现有 HTTP 框架:将 Vite 作为中间件集成到 Express/Fastify 时,设置 server: { middlewareMode: true },不让 Vite 启动自己的 HTTP 服务器,避免端口冲突和双重请求处理。

环境变量文件按优先级顺序覆盖.env.production.local > .env.production > .env.local > .env,本地覆盖文件(.local)加入 .gitignore,不提交到仓库,团队共享变量放 .env.production


常见陷阱

陷阱:SSR 构建产物在 Node.js 运行时报 ReferenceError: window is not defined

现象: SSR 模式下启动服务端渲染,某个第三方库在导入时直接访问 window/document,构建通过但运行时崩溃。
原因: Vite SSR 构建输出的是 Node.js 可执行的 CommonJS/ESM 包,但部分前端库假设浏览器环境,在模块顶层读取 window
解决:vite.config.ts 中把问题包加入 ssr.noExternal(强制打包并让 Vite 注入 shim),或在动态 import 中延迟加载该库,或在服务端环境提供 global.window = {} 占位。

陷阱:自定义插件的 transform 在生产构建中不触发

现象: 开发服务器下插件生效,vite build 后功能缺失。
原因: 部分开发者在 transform 钩子中写了提前返回逻辑(如 if (!this.server) return),或插件依赖开发服务器特有的 API(如 server.moduleGraph),导致生产构建路径跳过了转换。
解决:configResolved 钩子中保存 config,区分 command === 'serve''build' 两条路径,确保 transform 在两种模式下都执行必要逻辑。

陷阱:build.rollupOptions.output.manualChunks 导致循环依赖警告

现象: 配置 manualChunks 将某些模块合并后,Rollup 输出大量 circular dependency 警告,或运行时出现变量未定义错误。
原因: manualChunks 强制将模块分组,若分组破坏了 Rollup 自动确定的模块加载顺序,会引入循环引用。
解决: 优先使用 manualChunks 的函数形式而非对象形式,让 Rollup 仍能分析依赖关系;避免把有相互依赖的模块拆到不同 chunk;出现问题时先移除 manualChunks 验证是否是它引起的。


参见

Vite初级指南
Vite中级指南
TypeScript完全指南

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog