第六章:Vite 高级特性与优化
6.1 服务端渲染(SSR)深度实践
6.1.1 SSR 核心概念
Vite 原生支持服务端渲染,通过 vite build --ssr 构建服务端代码。SSR 的核心优势:
- SEO 友好:搜索引擎可直接抓取完整 HTML
- 首屏加载快:用户无需等待 JS 下载即可看到内容
- 更好的社交分享:Open Graph 标签可正确解析
6.1.2 完整 SSR 项目结构
ssr-project/
├── index.html # 客户端入口模板
├── server.js # 服务端入口
├── src/
│ ├── entry-client.js # 客户端入口(hydration)
│ ├── entry-server.js # 服务端入口(渲染)
│ ├── app.js # 通用应用代码
│ ├── router.js # 路由配置
│ ├── store.js # 状态管理
│ └── components/
│ └── App.vue
└── vite.config.js
6.1.3 完整 SSR 配置示例
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
build: {
// 客户端构建
rollupOptions: {
input: {
client: resolve(__dirname, 'index.html')
}
}
},
// 服务端构建配置
ssr: {
// 需要外部化的依赖
noExternal: ['@vueuse/core']
},
// 环境变量区分客户端/服务端
define: {
__SSR__: process.env.SSR === 'true'
}
})
6.1.4 通用应用代码
javascript
// src/app.js
import { createSSRApp } from 'vue'
import { createRouter } from './router'
import { createStore } from './store'
import App from './components/App.vue'
export function createApp() {
const app = createSSRApp(App)
const router = createRouter()
const store = createStore()
app.use(router)
app.use(store)
return { app, router, store }
}
6.1.5 服务端入口
javascript
// src/entry-server.js
import { renderToString } from 'vue/server-renderer'
import { createApp } from './app'
export async function render(url, manifest) {
const { app, router, store } = createApp()
// 设置路由
await router.push(url)
await router.isReady()
// 数据预取
const matchedComponents = router.currentRoute.value.matched
await Promise.all(
matchedComponents.map(component => {
if (component.asyncData) {
return component.asyncData({ store, route: router.currentRoute.value })
}
})
)
// 渲染 HTML
const html = await renderToString(app)
// 返回 HTML 和状态
return { html, state: store.state }
}
6.1.6 客户端入口(Hydration)
javascript
// src/entry-client.js
import { createApp } from './app'
const { app, router, store } = createApp()
// 注入服务端状态
if (window.__INITIAL_STATE__) {
store.state.value = window.__INITIAL_STATE__
}
// 等待路由准备就绪
router.isReady().then(() => {
app.mount('#app')
})
6.1.7 服务端渲染服务器
javascript
// server.js
import express from 'express'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const app = express()
// 中间件:静态文件服务
app.use('/assets', express.static(path.resolve(__dirname, 'dist/client/assets')))
// 动态渲染路由
app.get('*', async (req, res) => {
try {
// 读取客户端模板
const template = fs.readFileSync(
path.resolve(__dirname, 'dist/client/index.html'),
'utf-8'
)
// 动态导入服务端渲染函数
const { render } = await import('./dist/server/entry-server.js')
// 执行渲染
const { html, state } = await render(req.url)
// 注入渲染结果和状态
const finalHtml = template
.replace('<!--ssr-outlet-->', html)
.replace(
'</head>',
`<script>window.__INITIAL_STATE__=${JSON.stringify(state).replace(/</g, '\\u003c')}</script></head>`
)
res.status(200).set({ 'Content-Type': 'text/html' }).send(finalHtml)
} catch (error) {
console.error(error)
res.status(500).send('Server Error')
}
})
app.listen(3000, () => {
console.log('SSR Server running at http://localhost:3000')
})
6.1.8 数据预取模式
vue
<!-- src/components/UserProfile.vue -->
<template>
<div v-if="user">
<h1>{{ user.name }}</h1>
<p>{{ user.bio }}</p>
</div>
<div v-else>加载中...</div>
</template>
<script setup>
import { useStore } from '../store'
const store = useStore()
const props = defineProps(['userId'])
// 服务端数据预取
export async function asyncData({ store, route }) {
const userId = route.params.id
await store.dispatch('fetchUser', userId)
return { userId }
}
// 客户端数据刷新
import { onMounted } from 'vue'
onMounted(async () => {
if (!store.state.user) {
await store.dispatch('fetchUser', props.userId)
}
})
</script>
6.1.9 性能优化:流式渲染
javascript
// 使用 renderToNodeStream 实现流式渲染
import { renderToNodeStream } from 'vue/server-renderer'
app.get('*', async (req, res) => {
res.setHeader('Content-Type', 'text/html')
// 发送 HTML 头部
res.write('<!DOCTYPE html><html><head><title>SSR App</title></head><body><div id="app">')
// 流式渲染内容
const stream = renderToNodeStream(app)
stream.pipe(res, { end: false })
stream.on('end', () => {
res.write('</div><script src="/assets/client.js"></script></body></html>')
res.end()
})
})
6.1.10 Vite vs Nuxt/Next 对比
| 特性 | Vite SSR | Nuxt (Vue) | Next (React) |
|---|---|---|---|
| 框架 | Vue | Vue | React |
| 配置复杂度 | 高(需手动配置) | 低(约定优于配置) | 中 |
| 灵活性 | 极高 | 受框架约束 | 较高 |
| 学习曲线 | 陡峭 | 平缓 | 平缓 |
| 性能优化 | 手动优化 | 内置优化 | 内置优化 |
| 适用场景 | 定制化需求、深度集成 | 快速开发、标准应用 | 标准 React 应用 |
| 文件路由 | 需手动配置 | 自动生成 | App Router/Pages Router |
| 数据获取 | 手动实现 | useFetch/useAsyncData | getServerSideProps |
6.1.11 选择建议
选择 Vite SSR 当:
- 需要深度定制渲染逻辑
- 已有现有项目需迁移到 SSR
- 对框架有特殊要求(如使用特定路由库)
- 需要集成非标准后端框架
选择 Nuxt/Next 当:
- 从零开始新项目
- 需要开箱即用的 SSR 功能
- 团队熟悉约定式开发
- 不需要底层控制
6.1.12 SSR 性能优化清单
javascript
// 1. 启用组件缓存
import { createRenderer } from 'vue-server-renderer'
const renderer = createRenderer({
cache: new LRU(1000) // 缓存最近 1000 个组件
})
// 2. 使用片段缓存
<template>
<div>
<!-- 静态内容使用 v-once -->
<header v-once>
<h1>{{ title }}</h1>
</header>
<!-- 动态内容正常渲染 -->
<main>
<slot />
</main>
</div>
</template>
// 3. 延迟加载非关键组件
const HeavyComponent = defineAsyncComponent(() => import('./Heavy.vue'))
// 4. 预取数据优化
// 只预取首屏需要的数据,其他数据客户端懒加载
export async function asyncData({ route }) {
const criticalData = await fetchCritical(route.params.id)
const nonCriticalData = null // 客户端再加载
return { criticalData, nonCriticalData }
}
// 5. 使用 CDN 加速静态资源
// vite.config.js
base: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com/'
: '/'
6.1.13 常见问题与调试
Q: Hydration 不匹配警告
javascript
// 原因:服务端和客户端渲染结果不一致
// 解决:使用 onMounted 确保只在客户端执行
import { onMounted } from 'vue'
onMounted(() => {
// 只在客户端运行的代码
const userAgent = navigator.userAgent
})
Q: 服务端无法访问 window/document
javascript
// 使用条件判断
if (typeof window !== 'undefined') {
// 浏览器端代码
}
// 或使用 __SSR__ 标志
if (!__SSR__) {
// 客户端代码
}
Q: 内存泄漏
javascript
// 确保清理资源
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
// ... 配置
// 清理函数
return {
app,
cleanup: () => {
app.unmount()
}
}
}
6.2 静态站点生成(SSG)
使用 VitePress
bash
npm install -D vitepress
npx vitepress init
配置 VitePress
javascript
// .vitepress/config.js
export default {
title: '我的站点',
description: 'VitePress 示例',
themeConfig: {
nav: [
{ text: '首页', link: '/' },
{ text: '指南', link: '/guide/' }
]
}
}
6.3 库模式开发
配置库模式
javascript
// vite.config.js
export default defineConfig({
build: {
lib: {
entry: 'src/index.js',
name: 'MyLib',
formats: ['es', 'umd'],
fileName: (format) => `my-lib.${format}.js`
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue'
}
}
}
}
})
package.json 配置
json
{
"name": "my-lib",
"type": "module",
"main": "./dist/my-lib.umd.js",
"module": "./dist/my-lib.es.js",
"exports": {
".": {
"import": "./dist/my-lib.es.js",
"require": "./dist/my-lib.umd.js"
}
}
}
6.4 多页面应用(MPA)
配置多入口
javascript
// vite.config.js
import { resolve } from 'path'
export default defineConfig({
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
about: resolve(__dirname, 'about.html'),
contact: resolve(__dirname, 'contact.html')
}
}
}
})
目录结构
project/
├── index.html # 主页
├── about.html # 关于页
├── contact.html # 联系页
└── src/
├── main.js
├── about.js
└── contact.js
6.5 性能优化策略
1. 代码分割
javascript
// 动态导入
const AdminPanel = () => import('./AdminPanel.vue')
// 手动分包
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor': ['vue', 'vue-router'],
'ui': ['element-plus', '@headlessui/vue']
}
}
}
}
2. 预加载和预取
html
<!-- 预加载关键资源 -->
<link rel="preload" href="/fonts/Inter.woff2" as="font" crossorigin>
<link rel="prefetch" href="/pages/about.js">
<!-- 使用 vite-plugin-prefetch -->
import prefetch from 'vite-plugin-prefetch'
plugins: [prefetch()]
3. 图片优化
javascript
// vite-plugin-imagemin
import viteImagemin from 'vite-plugin-imagemin'
plugins: [
viteImagemin({
gifsicle: { optimizationLevel: 3 },
optipng: { optimizationLevel: 7 },
mozjpeg: { quality: 80 },
pngquant: { quality: [0.8, 0.9] }
})
]
4. 按需加载
javascript
// Element Plus 按需引入
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
]
6.6 缓存优化
浏览器缓存
javascript
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]'
}
}
}
依赖缓存
javascript
optimizeDeps: {
// 强制预构建,减少请求
include: ['vue', 'vue-router', 'axios'],
// 排除变化频繁的依赖
exclude: ['@vueuse/core']
}
6.7 压缩优化
启用压缩
javascript
// vite-plugin-compression
import compression from 'vite-plugin-compression'
plugins: [
compression({
algorithm: 'gzip',
ext: '.gz',
threshold: 10240,
deleteOriginFile: false
})
]
Brotli 压缩
javascript
compression({
algorithm: 'brotliCompress',
ext: '.br'
})
6.8 监控和分析
使用 rollup-plugin-visualizer
bash
npm install -D rollup-plugin-visualizer
javascript
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
open: true,
filename: 'stats.html',
gzipSize: true
})
]
})
Web Vitals 监控
javascript
// 在客户端收集性能指标
import { getCLS, getFID, getLCP } from 'web-vitals'
function sendToAnalytics(metric) {
console.log(metric.name, metric.value)
}
getCLS(sendToAnalytics)
getFID(sendToAnalytics)
getLCP(sendToAnalytics)
6.9 安全优化
内容安全策略(CSP)
javascript
// vite.config.js
server: {
headers: {
'Content-Security-Policy':
"default-src 'self'; script-src 'self' 'unsafe-inline'"
}
}
防止 XSS
javascript
// 使用 DOMPurify 清理 HTML
import DOMPurify from 'dompurify'
const clean = DOMPurify.sanitize(userInput)
6.10 调试技巧
源码映射
javascript
build: {
sourcemap: true // 开发环境
}
// 生产环境使用 hidden sourcemap
build: {
sourcemap: 'hidden'
}
开发调试
javascript
// 使用 debug 模块
import debug from 'debug'
const log = debug('vite:plugin')
log('插件执行')
本章小结
高级特性让 Vite 适应更复杂的场景:
- SSR/SSG 支持服务端渲染和静态站点
- 库模式用于组件库开发
- 多页面应用支持多入口
- 性能优化策略提升用户体验
- 监控和分析帮助定位瓶颈
掌握这些技术,可以构建高性能、可扩展的现代应用。