Vue服务端渲染(SSR)的实现细节
Vue 服务端渲染(SSR)的基本概念
什么是服务端渲染
在传统的前端开发中,页面通常是在浏览器端通过 JavaScript 动态渲染生成的。这意味着浏览器需要先下载 HTML 骨架,然后再加载 JavaScript 脚本,解析并执行脚本后才能呈现出完整的页面内容。而服务端渲染(Server - Side Rendering,简称 SSR)则是在服务器端将 Vue 组件渲染为 HTML 字符串,然后将完整的 HTML 页面发送到浏览器。这样,浏览器在收到页面时已经是一个完全渲染好的 DOM 结构,无需等待额外的 JavaScript 执行来生成页面内容,大大提高了首屏加载速度,对于 SEO 也更为友好,因为搜索引擎爬虫可以直接获取到完整的页面内容。
SSR 与客户端渲染的对比
- 首屏加载速度:客户端渲染需要先加载 HTML 骨架和 JavaScript 脚本,然后在浏览器端执行脚本渲染页面,这个过程会导致首屏加载时间较长。而 SSR 由于在服务器端已经生成了完整的 HTML,浏览器直接接收并展示,首屏加载速度更快。
- SEO 友好度:搜索引擎爬虫在抓取页面时,更倾向于解析完整的 HTML 内容。客户端渲染的页面在爬虫抓取时可能只能获取到初始的 HTML 骨架,无法获取到动态渲染的内容,不利于 SEO。SSR 生成的页面包含完整的内容,对 SEO 更为友好。
- 交互体验:客户端渲染在脚本加载并执行后,可以实现非常流畅的交互体验,因为所有的逻辑都在客户端处理。而 SSR 在首次加载时虽然速度快,但后续的交互可能需要更多的处理,例如重新渲染部分组件,这可能会导致一些性能问题,不过通过合理的优化可以改善。
Vue SSR 的实现原理
构建流程
- 客户端构建:与常规的 Vue 项目构建类似,通过 webpack 等工具将 Vue 组件、JavaScript、CSS 等资源打包成适合在浏览器端运行的文件。这个过程主要是将 Vue 组件编译成浏览器可执行的 JavaScript 代码,并处理样式等资源。
- 服务端构建:同样使用 webpack 进行构建,但配置与客户端构建有所不同。服务端构建会将 Vue 组件渲染为 Node.js 模块,这个模块可以在服务器端运行并生成 HTML 字符串。在服务端构建中,webpack 会将 Vue 组件中的模板、样式等资源进行处理,使其能够在 Node.js 环境中正确渲染。
渲染过程
- 服务器端渲染:当客户端请求一个页面时,服务器接收到请求后,会加载通过服务端构建生成的 Vue 应用模块。然后,服务器使用 Vue 的渲染函数将组件渲染为 HTML 字符串。在这个过程中,Vue 会按照组件的生命周期钩子函数顺序执行相关逻辑,例如
created
、mounted
等钩子函数中的数据获取操作,确保在渲染前数据已经准备好。 - 客户端激活:服务器将渲染好的 HTML 发送到客户端后,客户端需要将这个静态的 HTML 激活为一个可交互的 Vue 应用。这一步通过重新创建 Vue 实例,并将服务器端渲染生成的 HTML 作为初始状态进行挂载来实现。客户端会复用服务器端渲染的 DOM 结构,然后在其上绑定事件监听器等,使其具备交互能力。
环境准备
安装 Node.js
Vue SSR 需要在 Node.js 环境下运行,因此首先要确保系统中安装了 Node.js。可以从 Node.js 官方网站(https://nodejs.org/)下载并安装适合系统的版本。安装完成后,通过在命令行中输入 node -v
来验证是否安装成功,如果输出版本号,则说明安装正确。
初始化 Vue 项目
- 使用 Vue CLI 创建一个新的 Vue 项目。在命令行中执行以下命令:
vue create my - ssr - project
按照提示选择项目的配置,例如选择 Vue 2.x 还是 Vue 3.x 等。这里以 Vue 2.x 为例进行讲解。 2. 进入项目目录:
cd my - ssr - project
安装依赖
- 安装 Vue SSR 相关依赖。在项目目录下执行:
npm install vue - server - renderer vue - router express
vue - server - renderer
:用于在服务器端渲染 Vue 组件为 HTML。vue - router
:用于处理路由,在 SSR 中同样需要管理页面路由。express
:一个流行的 Node.js web 服务器框架,用于搭建服务器接收客户端请求并返回渲染后的页面。
服务端渲染的配置
服务器端构建配置
- 在项目根目录下创建
webpack.server.js
文件,用于配置服务端的 webpack 构建。以下是一个基本的配置示例:
const path = require('path');
const VueSSRServerPlugin = require('vue - server - renderer/server - plugin');
module.exports = {
target:'server',
entry: './src/entry - server.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename:'server - bundle.js',
libraryTarget: 'commonjs2'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue - loader'
},
{
test: /\.js$/,
loader: 'babel - loader',
exclude: /node_modules/
}
]
},
resolve: {
alias: {
'@': path.resolve(__dirname,'src')
}
},
plugins: [
new VueSSRServerPlugin()
]
};
target:'server'
:指定这是一个针对服务器端的构建。entry: './src/entry - server.js'
:指定服务端的入口文件,后续会详细介绍入口文件的内容。output
配置:指定输出路径和文件名,libraryTarget: 'commonjs2'
确保输出的模块可以在 Node.js 环境中使用。module.rules
:配置了对.vue
和.js
文件的加载器,vue - loader
用于处理 Vue 组件,babel - loader
用于将 ES6+ 代码转换为 Node.js 可执行的代码。resolve.alias
:设置别名,方便在项目中引用src
目录下的文件。new VueSSRServerPlugin()
:这个插件会生成一个可以在服务器端运行的 bundle 文件。
客户端构建配置
在项目根目录下创建 webpack.client.js
文件,用于配置客户端的 webpack 构建。基本配置如下:
const path = require('path');
const VueSSRClientPlugin = require('vue - server - renderer/client - plugin');
module.exports = {
target: 'web',
entry: './src/entry - client.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'client - bundle.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue - loader'
},
{
test: /\.js$/,
loader: 'babel - loader',
exclude: /node_modules/
}
]
},
resolve: {
alias: {
'@': path.resolve(__dirname,'src')
}
},
plugins: [
new VueSSRClientPlugin()
]
};
target: 'web'
:指定这是一个针对浏览器端的构建。entry: './src/entry - client.js'
:指定客户端的入口文件。output
配置:指定输出路径和文件名,publicPath: '/'
设置资源的公共路径。- 同样配置了
module.rules
、resolve.alias
和new VueSSRClientPlugin()
。VueSSRClientPlugin
会生成一些辅助文件,用于客户端激活。
配置 Babel
在项目根目录下创建 .babelrc
文件,用于配置 Babel 转码规则。以下是一个基本的配置:
{
"presets": [
[
"@babel/preset - env",
{
"targets": {
"node": "current"
}
}
]
]
}
这个配置使用 @babel/preset - env
预设,将 ES6+ 代码转换为适合当前 Node.js 版本运行的代码。
编写入口文件
服务端入口文件(entry - server.js)
在 src
目录下创建 entry - server.js
文件,内容如下:
import Vue from 'vue';
import App from './App.vue';
import router from './router';
export default context => {
return new Promise((resolve, reject) => {
const app = new Vue({
router,
render: h => h(App)
});
resolve(app);
});
};
这个文件导出一个函数,该函数接收一个 context
参数(通常用于传递请求相关的信息)。函数返回一个 Promise,在 Promise 中创建一个 Vue 实例,并将其 resolve。router
和 App
组件被引入并用于创建 Vue 实例,render
函数指定了根组件为 App
。
客户端入口文件(entry - client.js)
在 src
目录下创建 entry - client.js
文件,内容如下:
import Vue from 'vue';
import App from './App.vue';
import router from './router';
const app = new Vue({
router,
render: h => h(App)
});
// 激活客户端
if (window.__INITIAL_STATE__) {
app.$store.replaceState(window.__INITIAL_STATE__);
}
app.$mount('#app');
客户端入口文件首先创建一个 Vue 实例,与服务端入口类似。然后,如果存在 window.__INITIAL_STATE__
(这个状态是在服务端渲染时注入到 HTML 中的),则将其替换到 Vuex 的状态中。最后,将 Vue 实例挂载到页面的 #app
元素上。
路由配置
在 src/router
目录下创建 index.js
文件,用于配置路由。以下是一个简单的示例:
import Vue from 'vue';
import Router from 'vue - router';
import Home from '@/components/Home.vue';
import About from '@/components/About.vue';
Vue.use(Router);
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
});
这里使用 vue - router
配置了两个路由,/
对应 Home
组件,/about
对应 About
组件。mode: 'history'
使用 HTML5 的 History API 来实现无哈希的路由。
服务器端实现
创建 Express 服务器
在项目根目录下创建 server.js
文件,用于搭建 Express 服务器并处理 SSR。内容如下:
const express = require('express');
const { createBundleRenderer } = require('vue - server - renderer');
const serverBundle = require('./dist/server - bundle.js');
const clientManifest = require('./dist/client - manifest.json');
const app = express();
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template: require('fs').readFileSync(path.join(__dirname, 'index.template.html'), 'utf - 8'),
clientManifest
});
app.use(express.static(path.join(__dirname, 'dist')));
app.get('*', (req, res) => {
const context = {
url: req.url
};
renderer.renderToString(context, (err, html) => {
if (err) {
if (err.code === 404) {
res.status(404).end('Page Not Found');
} else {
res.status(500).end('Internal Server Error');
}
} else {
res.end(html);
}
});
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
- 引入
express
、vue - server - renderer
等模块,并加载服务端 bundle 文件和客户端 manifest 文件。 - 使用
createBundleRenderer
创建一个渲染器,runInNewContext: false
表示在当前 Node.js 上下文环境中运行,template
配置了 HTML 模板文件,clientManifest
用于客户端激活。 - 使用
express.static
中间件来处理静态资源,将dist
目录作为静态资源目录。 - 在
app.get('*')
中,为每个请求创建一个context
对象,包含请求的 URL。然后使用渲染器的renderToString
方法将 Vue 应用渲染为 HTML 字符串。如果渲染过程中出现错误,根据错误类型返回相应的 HTTP 状态码和错误信息;如果渲染成功,则将 HTML 返回给客户端。 - 最后,服务器监听指定端口,默认是 3000 端口。
HTML 模板
在项目根目录下创建 index.template.html
文件,作为服务器端渲染的 HTML 模板。内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF - 8">
<meta name="viewport" content="width=device - width, initial - scale=1.0">
<title>Vue SSR Example</title>
<!-- 引入客户端构建生成的 CSS 等资源 -->
<% for (var i in htmlWebpackPlugin.files.css) { %>
<link href="<%= htmlWebpackPlugin.files.css[i] %>" rel="stylesheet">
<% } %>
</head>
<body>
<div id="app"><%= appHtml %></div>
<!-- 注入服务端渲染生成的状态 -->
<script>
window.__INITIAL_STATE__ = <%= JSON.stringify(initialState) %>
</script>
<!-- 引入客户端构建生成的 JavaScript 资源 -->
<% for (var i in htmlWebpackPlugin.files.js) { %>
<script src="<%= htmlWebpackPlugin.files.js[i] %>"></script>
<% } %>
</body>
</html>
这个模板文件使用了 EJS 模板语法(可以通过安装 html - webpack - plugin
并在 webpack 配置中使用来支持)。<%= appHtml %>
会被服务端渲染生成的 HTML 内容替换,window.__INITIAL_STATE__
用于注入服务端渲染时的初始状态,以便客户端激活时使用。模板中还引入了客户端构建生成的 CSS 和 JavaScript 资源。
数据获取与状态管理
在 SSR 中获取数据
- 生命周期钩子函数:在 Vue 组件中,可以利用
created
或mounted
钩子函数来获取数据。例如,在Home
组件中:
export default {
data() {
return {
posts: []
};
},
created() {
// 模拟数据请求
fetch('https://api.example.com/posts')
.then(response => response.json())
.then(data => {
this.posts = data;
});
}
};
在服务端渲染时,created
钩子函数中的数据请求会在服务器端执行,确保在渲染前数据已经准备好。
- 异步组件数据获取:对于异步组件,可以使用
asyncData
方法来获取数据。首先安装@nuxtjs/async - data
插件(虽然它是 Nuxt.js 相关的,但在普通 Vue SSR 项目中也可借鉴)。然后在异步组件中:
import { asyncData } from '@nuxtjs/async - data';
export default {
data() {
return {
user: null
};
},
asyncData({ $axios }) {
return $axios.get('https://api.example.com/user')
.then(response => {
return {
user: response.data
};
});
}
};
asyncData
方法会在组件渲染前执行,并且可以在服务端和客户端共享数据获取逻辑。
使用 Vuex 进行状态管理
- 安装 Vuex:在项目目录下执行
npm install vuex
。 - 创建 Vuex store:在
src/store
目录下创建index.js
文件,内容如下:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++;
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment');
}, 1000);
}
}
});
export default store;
- 在 SSR 中使用 Vuex:在服务端入口文件
entry - server.js
中:
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
export default context => {
return new Promise((resolve, reject) => {
const app = new Vue({
router,
store,
render: h => h(App)
});
resolve(app);
});
};
在客户端入口文件 entry - client.js
中:
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
const app = new Vue({
router,
store,
render: h => h(App)
});
if (window.__INITIAL_STATE__) {
app.$store.replaceState(window.__INITIAL_STATE__);
}
app.$mount('#app');
在服务端渲染时,Vuex 的状态会被填充,并且可以通过 window.__INITIAL_STATE__
传递给客户端,客户端在激活时可以复用这些状态,确保服务端和客户端状态的一致性。
优化与注意事项
性能优化
- 代码拆分:在客户端和服务端构建中,通过配置 webpack 的代码拆分功能,将代码按照路由、功能模块等进行拆分,减少初始加载的文件大小。例如,在
webpack.client.js
中可以使用splitChunks
配置:
module.exports = {
//...其他配置
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
- 缓存:在服务器端,可以对一些不经常变化的数据进行缓存,例如在获取数据的 API 调用时,可以使用
node - cache
等库进行缓存。这样在相同请求时,可以直接从缓存中获取数据,减少数据获取的时间。 - 预渲染:对于一些静态页面,可以使用预渲染工具(如
prerender - spa - plugin
)在构建时生成静态 HTML 文件。这样,这些页面可以直接作为静态资源被服务器返回,进一步提高加载速度。
注意事项
- 生命周期钩子函数的差异:在服务端渲染时,一些生命周期钩子函数(如
mounted
)可能不会像在客户端那样完全执行,因为服务端渲染主要关注的是生成 HTML。因此,需要确保在created
等钩子函数中完成数据获取等关键操作。 - 第三方库的兼容性:部分第三方库可能不兼容服务端渲染环境,例如一些依赖于浏览器全局对象(如
window
、document
)的库。在使用第三方库时,需要检查其是否支持 SSR,或者通过一些方法(如条件加载)使其在服务端和客户端都能正常工作。 - 状态管理的一致性:确保服务端和客户端的 Vuex 状态一致非常重要。在服务端渲染时填充的状态,需要正确地传递给客户端并在客户端激活时复用,否则可能会出现页面显示不一致的问题。
通过以上详细的步骤和技术细节,我们可以实现一个完整的 Vue 服务端渲染应用,充分发挥 SSR 的优势,提升应用的性能和用户体验。在实际开发中,还需要根据项目的具体需求和场景进行进一步的优化和调整。