🌟 前言
在现代前端开发中,单页应用(SPA)虽然提供了流畅的用户体验,但也带来了一些问题:首屏加载慢、SEO 不友好、对搜索引擎爬虫不够友好等。而服务端渲染(SSR)技术的出现,完美地解决了这些痛点。
今天,我们就来深入探讨服务端渲染技术,从基础概念到核心原理,再到实际应用,带你全面了解 SSR 的魅力。并实现一个原生的Vue SSR项目。
🤔 什么是 SSR?
传统 CSR vs SSR
客户端渲染(CSR - Client Side Rendering):
css
浏览器请求 → 返回空白HTML → 下载JS → 执行JS → 渲染页面
服务端渲染(SSR - Server Side Rendering):
css
浏览器请求 → 服务器渲染HTML → 返回完整HTML → 客户端激活
SSR 的核心优势
- 更快的首屏加载:用户能立即看到完整的页面内容
- 更好的 SEO:搜索引擎能够直接抓取到完整的 HTML 内容
- 更好的移动端体验:减少了客户端的计算压力
- 更好的可访问性:即使 JavaScript 被禁用,页面依然可用
🏗️ Vue SSR 的核心架构
同构应用的概念
Vue SSR 采用的是"同构应用"架构,即同一套代码既能在服务端运行,也能在客户端运行:
scss
源代码
├── 服务端入口 (entry-server.ts)
├── 客户端入口 (entry-client.ts)
└── 通用应用代码 (main.ts)
双端入口设计
1. 通用应用入口 (main.ts)
typescript
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import { createRouter } from './router'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App) // 注意:使用 createSSRApp
const pinia = createPinia()
const router = createRouter()
app.use(pinia)
app.use(router)
return { app, router, pinia }
}
关键点:
- 使用
createSSRApp
而不是createApp
- 每次请求都创建新的应用实例,避免状态污染
- 返回应用实例及其依赖,供双端使用
2. 服务端入口 (entry-server.ts)
typescript
// entry-server.ts
import { renderToString } from 'vue/server-renderer'
import { createApp } from './main'
export async function render({ url, manifest, request, response }) {
const { app, router, pinia } = createApp()
// 设置服务端路由
await router.push(url)
await router.isReady()
// 渲染应用为 HTML 字符串
const html = await renderToString(app)
// 序列化状态,传递给客户端
const state = JSON.stringify(pinia.state.value)
return {
html,
state,
meta: {
title: '页面标题',
description: '页面描述'
}
}
}
核心流程:
- 创建应用实例
- 设置路由状态
- 等待异步组件和数据加载
- 渲染为 HTML 字符串
- 序列化应用状态
3. 客户端入口 (entry-client.ts)
typescript
// enter-client.s
import { createApp } from './main'
const { app, router, pinia } = createApp()
// 恢复服务端状态
if (window.__INITIAL_STATE__) {
pinia.state.value = window.__INITIAL_STATE__
}
// 等待路由准备就绪后挂载应用
router.isReady().then(() => {
app.mount('#app') // 激活静态 HTML
})
核心流程:
- 创建应用实例
- 恢复服务端传递的状态
- 等待路由准备
- 激活(hydrate)静态 HTML
🔄 数据预取:SSR 的核心挑战
问题的本质
在传统的客户端应用中,我们可以在组件挂载后再去获取数据:
vue
<script setup>
import { onMounted, ref } from 'vue'
const data = ref(null)
// 客户端:组件挂载后获取数据
onMounted(async () => {
data.value = await fetchData()
})
</script>
但在 SSR 中,我们需要在服务端渲染前就获取到数据,这就带来了挑战:
- 何时获取数据?
- 如何将数据传递给客户端?
- 如何避免客户端重复请求?
解决方案:useAsyncData Hook
typescript
export function useAsyncData<T>(
key: string,
handler: () => Promise<T>,
options?: AsyncDataOptions
) {
const data = ref(null)
const pending = ref(false)
const store = usePrefetchStore()
// 服务端预取
onServerPrefetch(async () => {
const result = await handler()
store.data[key] = result // 存储到全局状态
data.value = result
})
// 客户端激活
onBeforeMount(() => {
if (store.data[key]) {
// 使用服务端预取的数据
data.value = store.data[key]
} else {
// 服务端没有数据,客户端重新获取
refresh()
}
})
const refresh = async () => {
pending.value = true
data.value = await handler()
pending.value = false
}
return { data, pending, refresh }
}
实际使用示例
vue
<script setup>
import { useAsyncData } from '@/hooks/async-data'
import { $fetch } from '@/utils/fetch'
// 获取用户信息
const { data: userInfo, pending } = useAsyncData(
'user-info',
() => $fetch('/api/user')
)
// 获取文章列表
const { data: articles } = useAsyncData(
'articles',
() => $fetch('/api/articles')
)
</script>
<template>
<div>
<div v-if="pending">加载中...</div>
<div v-else>
<h1>欢迎,{{ userInfo?.name }}</h1>
<article v-for="article in articles" :key="article.id">
<h2>{{ article.title }}</h2>
<p>{{ article.summary }}</p>
</article>
</div>
</div>
</template>
🖥️ 服务器配置:Express + Vite
开发环境配置
javascript
import express from 'express'
import { createServer } from 'vite'
const app = express()
// 创建 Vite 开发服务器
const vite = await createServer({
server: { middlewareMode: true },
appType: 'custom'
})
// 使用 Vite 中间件
app.use(vite.middlewares)
app.use('*', async (req, res) => {
const url = req.originalUrl
// 获取 HTML 模板
let template = await readFile('./index.html', 'utf-8')
template = await vite.transformIndexHtml(url, template)
// 加载服务端入口
const { render } = await vite.ssrLoadModule('/src/entry-server.ts')
// 渲染页面
const { html, state, meta } = await render({ url })
// 注入内容
const finalHtml = template
.replace('{{%html%}}', html)
.replace('{{%state%}}', state)
.replace('{{%title%}}', meta.title)
res.set({ 'Content-Type': 'text/html' }).end(finalHtml)
})
生产环境配置
javascript
// 生产环境:使用预构建的文件
app.use('/assets', express.static('dist/client/assets'))
app.use('*', async (req, res) => {
// 读取预构建的模板
const template = await readFile('dist/client/index.html', 'utf-8')
// 导入预构建的服务端入口
const { render } = await import('./dist/server/entry-server.js')
// 读取资源清单
const manifest = JSON.parse(
await readFile('dist/client/.vite/ssr-manifest.json', 'utf-8')
)
const { html, state, preload } = await render({ url, manifest })
const finalHtml = template
.replace('{{%html%}}', html)
.replace('{{%state%}}', state)
.replace('{{%:=preload%}}', preload) // 预加载资源
res.end(finalHtml)
})
⚡ 性能优化策略
1. 资源预加载
typescript
// 生成预加载链接
function renderPreloadLinks(modules: string[], manifest: Record<string, string[]>) {
let links = ''
modules.forEach((id) => {
const files = manifest[id]
if (files) {
files.forEach((file) => {
if (file.endsWith('.js')) {
links += `<link rel="modulepreload" crossorigin href="${file}">`
} else if (file.endsWith('.css')) {
links += `<link rel="stylesheet" href="${file}">`
}
})
}
})
return links
}
🔮 未来展望
Vue SSR 技术还在不断发展,未来可能的方向包括:
- 边缘计算:在 CDN 边缘节点进行 SSR
- 流式渲染:逐步渲染页面内容
- 增量静态生成:结合 SSG 的优势
- 更好的开发工具:更完善的调试和性能分析工具
📝 总结
SSR 虽然增加了一定的复杂性,但它带来的性能提升和 SEO 优化效果是显著的。通过合理的架构设计和优化策略,我们可以构建出既快速又对搜索引擎友好的现代 Web 应用。
掌握 SSR 不仅能提升应用性能,更能让我们深入理解现代前端架构的精髓。希望这篇文章能帮助你更好地理解和应用 SSR 技术。
💡 快速开始 :如果你想快速体验 Vue SSR 开发,推荐使用
create-vue-ssr
脚手架工具:
bash# 一键创建 Vue SSR 项目 npm create vue-ssr cd my-ssr-app npm install npm run dev
📦 相关链接:
- NPM 包 :create-vue-ssr
- GitHub 仓库 :tianchangNorth/create-vue-ssr
- 欢迎贡献:项目开源,欢迎大佬们一起开发维护,提交 Issue 和 PR!