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

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

相关推荐
xiaotao1311 天前
第五章:Vite 插件开发指南
vite·前端打包
xiaotao1311 天前
第八章:实战项目案例
前端·vue.js·vite·前端打包
Irene19912 天前
TypeScript baseUrl 弃用解决(附:怎么在 Vite 中配置 resolve.alias)
typescript·vite·baseurl
辻戋2 天前
从零手写mini-vite
webpack·vite·esbuild
还是大剑师兰特4 天前
vitejs/plugin-legacy 作用与使用方法
vite·大剑师
xiaotao1315 天前
Vite 工作原理深度解析
vite·前端打包
xiaotao1315 天前
Vite 概述与核心概念
vite·前端打包
米丘6 天前
从 HTTP 到 WebSocket:深入 Vite HMR 的网络层原理
http·node.js·vite
蜡台7 天前
Vue 打包优化
前端·javascript·vue.js·vite·vue-cli