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

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

相关推荐
kyriewen3 天前
从Webpack到Vite:我们迁移了一个10万行代码的项目,总结了这7个坑
前端·webpack·vite
大家的林语冰4 天前
尤雨溪官宣:Vite+ 全员加盟 Cloudflare,正式进军全栈开发和 AI 部署云平台!
前端·javascript·vite
明月_清风5 天前
爆破前端生态!Cloudflare 收购 Vite 背后,前端开发者会迎来什么变化?
前端·vite
Anesthesia丶8 天前
Vite + Svelte + shadcn-svelte 最小化 Demo+Vue3语法对比总结
vue·vite·svelte·shadcn-svelte
曲幽12 天前
写页面时别再把 Element Plus 整个搬进来啦!Vue3按需加载的坑我帮你踩平了
vue3·web·vite·icon·element plus·vs code·import·unplugin
Linsk15 天前
一个案例教你彻底搞明白`AbortController` 、`AbortSignal`
vite·前端工程化
ZengLiangYi16 天前
Tailwind CSS v4 + Vite:现代前端样式方案
前端·css·vite
发现一只大呆瓜16 天前
超全 Vite 性能优化指南:网络、资源、预渲染三维落地方案
前端·面试·vite
发现一只大呆瓜16 天前
Vite 兼容降级全解:语法降级、Polyfill 原理与 legacy 插件底层机制
前端·面试·vite
发现一只大呆瓜17 天前
Vite 开发预构建机制详解,搞懂 esbuild 与 Rollup 分工差异
前端·面试·vite