🌟 前言

在现代前端开发中,单页应用(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

复制代码

但在 SSR 中,我们需要在服务端渲染前就获取到数据,这就带来了挑战:

何时获取数据?

如何将数据传递给客户端?

如何避免客户端重复请求?

解决方案:useAsyncData Hook

typescript

复制代码

export function useAsyncData(

key: string,

handler: () => Promise,

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

复制代码

🖥️ 服务器配置: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) {

let links = ''

modules.forEach((id) => {

const files = manifest[id]

if (files) {

files.forEach((file) => {

if (file.endsWith('.js')) {

links += ``

} else if (file.endsWith('.css')) {

links += ``

}

})

}

})

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!