第六章:Vite 高级特性与优化

第六章: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 支持服务端渲染和静态站点
  • 库模式用于组件库开发
  • 多页面应用支持多入口
  • 性能优化策略提升用户体验
  • 监控和分析帮助定位瓶颈

掌握这些技术,可以构建高性能、可扩展的现代应用。

相关推荐
cindershade6 天前
首屏不是 dist:用“加载账本”治理 Vite 的大包
性能优化·vite
陪我一起学编程8 天前
vue3笔记
前端·webpack·vue3·vite·脚手架·vue3笔记·组件式
向北丶23 天前
vite项目中配置alias别名
前端·vite
PedroQue9923 天前
修复 .uvue 页面路径残留问题
前端·vite
梦曦i23 天前
修复 .uvue 页面路径残留问题
前端·vite
cindershade1 个月前
首屏在替谁付费:重做 Vite 产物治理的责任边界
vite
做前端的娜娜子1 个月前
施工蓝图:vite.config.js —— 项目的指挥中心
前端·react native·vite
D_jing201 个月前
【踩坑解决】pnpm+Vite引入@koi/core报错spark-md5找不到模块(幽灵依赖深度解析)
pnpm·vite·幽灵依赖·前端踩坑
Flynt1 个月前
pnpm 12 换上了 Rust 内核,我拿项目实测了一轮构建速度
rust·vite·前端工程化
xiaoyan20151 个月前
2026最新款Electron41+React19+AntDesign电脑端后台管理系统Exe
react.js·electron·vite