SSR(server side rendering)服务端渲染,指的是将网页的内容在服务器上生成完整的HTML页面,然后再发送给浏览器进行显示。主要用于SEO的优化,这里就不过多描述了,下面找了一张图,描述了SSR的实现过程,这里主要讲客户端和服务端的实现部分,以Vue实现SSR为例

使用Express搭建一个服务端
首先安装一下express
npm install express
之后创建一个server.js文件启动一下服务,监听3000端口
import express from "express";
const server = express();
server.listen(3000, () => {
console.log("Server is running on http://localhost:3000");
});
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
到这里就可以使用node server.js指令来运行服务端了
创建Vue示例
以往创建一个Vue实例,使用的是 createApp ,现在需要使用 createSSRApp ,原因是createApp只能在浏览器环境运行,由于我们需要在服务端运行代码,而createSSRApp又同时支持在服务端和浏览器运行代码,所以这里我们使用createSSRApp
创建一个app.js文件,然后创建一个Vue实例,这里一定要通过这种工厂函数的方式来创建,因为我们需要在服务端获取Vue实例,客户端上只有一个人在使用,所以可以直接创建实例,而服务端会有多人同时获取,因此需要确保每次返回的是一个新的实例
import { createSSRApp } from "vue";
export function createApp() {
return createSSRApp({
template: `<div>{{msg}}</div>`,
data() {
return {
msg: "Hello World",
};
},
});
}
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
之后在server.js中引入app.js文件,处理之后返回给浏览器
import { renderToString } from "vue/server-renderer";
import express from "express";
import app from "./app.js";
const server = express();
server.get("/", async (req, res) => {
try {
// 转换成字符串
const html = await renderToString(app);
res.send(html);
} catch (error) {
res.status(500).send("服务器错误");
}
});
server.listen(3000, () => {
console.log("Server is running on http://localhost:3000");
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
注意
由于HTTP 响应只能是字符串,所以需要先使用Vue中提供的方法 renderToString 方法,将Vue实例转换成字符串
到这里就可以打开浏览器,在http://localhost:3000中看到返回结果了,但是会发现一个问题,浏览器拿到的就是一串字符串,而不是完整了HTML文档,这样就不利于SEO,SSR就失去了意义,这里我们可以通过创建一个HTML模板,然后将内容进行替换解决

创建HTML模板
新建一个index.html文件,然后在body中给一个特殊的标记,这里用的 vue-ssr-render ,标记不固定,主要用于后续匹配替换
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"><!-- vue-ssr-render --></div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
之后通用node.js中 fs模块 来获取这个html文件并替换内容
import { renderToString } from "vue/server-renderer";
import express from "express";
import fs from "fs";
import { createApp } from "./app.js";
const server = express();
server.get("/", async (req, res) => {
try {
const app = createApp();
// 转换成字符串
const html = await renderToString(app);
// 获取html文件
const template = fs.readFileSync("./index.html", "utf-8");
// 替换内容
res.send(template.replace("<!-- vue-ssr-render -->", html));
} catch (error) {
res.status(500).send("服务器错误");
}
});
server.listen(3000, () => {
console.log("Server is running on http://localhost:3000");
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

到这里就可以看到返回给浏览器的是一个正常的html文档了,但是又会出现另一个问题,例如我们现在给页面添加一个按钮,会发现这个按钮是不生效的
import { createSSRApp } from "vue";
export function createApp() {
return createSSRApp({
template: `<div>
<div>{{msg}}</div>
<div>{{count}}</div>
<button @click="count++">增加</button>
</div>`,
data() {
return {
msg: "Hello World",
count: 0,
};
},
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这是因为服务端返回给浏览器的只是一个静态HTML,还没有变成一个Vue应用,这时候我们就需要在客户端“激活”一下,也就是 水合 操作
水合
水合(Hydration)指的是水分子与某种物质结合,形成一个更复杂的结构或使物质变得活跃和可用,在这里静态的HTML就像一个没有活力的物质,前端的JS代码就像水分子,只有两者相结合的时候,才会使整个页面变得动态可交互。如果用代码来理解就是这样的:
<!-- 服务端渲染的静态 HTML -->
<div id="app">
<button>点击我</button> <!-- 静态的,不可点击 -->
</div>
<!-- ↓ 水合操作后 ↓ -->
<!-- 变成动态的 Vue 应用 -->
<div id="app">
<button onclick="handleClick">点击我</button> <!-- 动态的,可点击 -->
</div>
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
水合操作会对比客户端虚拟DOM和服务端渲染的HTML,确保两者一致,然后"接管"现有的DOM节点(不会重新创建),这里很重要的一点就是两者一定要一致!所以就需要提前创建一个 “同构” 应用(同一套代码在服务端和客户端都能运行),它是水合操作的前提,这也是前面为什么要使用createSSRApp的原因
我们先创建一个client.js文件,用于执行水合操作
import { createApp } from "./app.js";
const app = createApp();
app.mount("#app"); // Vue 检测到有内容 → 执行水合
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
2
3
为什么这样写就能执行水合操作?因为Vue在内部已经做了处理,传统的CSR(纯客户端渲染)在挂载的时候是一个空的容器,会进行全新的渲染,而SSR挂载的时候,Vue检测到有内容,就会自动进行水合操作
同时我们在index.html中引入client.js文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"><!-- vue-ssr-render --></div>
<!-- Import Maps -->
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<!-- 客户端激活脚本 -->
<script type="module" src="./client.js"></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这里还需要使用 importmap 来引入Vue(可以使用其它方式),因为浏览器是没有 node_modules 的,所以前面import引入的Vue只适用于服务端
由于我们在html中引入了client.js文件,所以还需要把整个文件夹设置为静态文件服务器,这样html才能访问到,在server.js中使用express设置即可
const server = express();
server.get("/", async (req, res) => {
...
});
// 必须放在server.get后面,express.static() 中间件会优先处理请求,当它发现请求的路径对应一个实际文件时,就会直接返回该文件,而不会继续执行你的路由处理程序
server.use(express.static("."));
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2
3
4
5
6
7
8
之后就可以看到页面交互正常了

脱水和注水
需要注意的是,如果客户端渲染与服务端渲染时的代码不一致,会导致水合失败,例如我们修改一下app.js,让count生成的是随机数
import { createSSRApp } from "vue";
export function createApp() {
return createSSRApp({
template: `<div>
<div>{{msg}}</div>
<div>{{count}}</div>
<button @click="count++">增加</button>
</div>`,
data() {
return {
msg: "Hello World",
count: Math.floor(Math.random() * 10),
};
},
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
会发现水合失败报错了,这是因为服务端渲染是生成的count和客户端水合时生成的count不一致导致的

其实不单单是这个例子,如果涉及到数据请求也会有这个问题,例如在服务端渲染时请求了数据,将数据塞到HTML后中返回给客户端,到了客户端渲染时是没有数据的,因为还没有水合,还没办法执行Vue的生命周期钩子函数中请求数据的代码,导致两边的数据不一致,水合失败。要解决这个问题最好的办法就是服务端获取完数据之后,同时也将这些数据返回给客户端,这样客户端拿到的时候就可以直接使用,而不用重新再去请求一次,这就是SSR中的 “脱水” 和 “注水”
- 脱水:是指服务端渲染的过程中,将渲染时使用的数据提取出来,并以一种可序列化的形式嵌入到HTML中,发送给客户端。这样客户端就可以直接使用这些数据,而不需要再次请求服务器
- 注水:是指在客户端接管服务器渲染的页面时,将脱水的状态重新加载到客户端应用中,并使用客户端的JavaScript逻辑接管页面上的交互和功能。通过注水,客户端可以避免重复请求数据,实现无缝的客户端功能接管
我们修改一下server.js中代码来实现脱水:
server.get("/", async (req, res) => {
try {
// 模拟获取的数据
const storeState = {
count: Math.floor(Math.random() * 10),
};
const app = createApp(storeState);
// 转换成字符串
const html = await renderToString(app);
// 获取html文件
const template = fs.readFileSync("./index.html", "utf-8");
const finalHtml = template
.replace("<!-- vue-ssr-render -->", html)
.replace(
"<!-- store-state -->",
`<script>window.__INITIAL_STATE__ = ${JSON.stringify(
storeState
)}</script>`
);
res.send(finalHtml);
} catch (error) {
console.log("error: ", error);
res.status(500).send("服务器错误");
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
在服务端直接将数据传递给 createApp ,然后将数据通过之前替换内容的方式注入到window中,这样客户端在拿到内容的时候就能直接在window中获取数据
在修改一下client.js来实现注水:
import { createApp } from "./app.js";
// 从服务端注入的数据获取
const initialState = window.__INITIAL_STATE__ || {};
const app = createApp(initialState);
app.mount("#app"); // 执行水合操作
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
2
3
4
5
直接从 window.INITIAL_STATE 中获取数据,传入createApp中
这样不管是在服务端或者客户端Vue实例都能直接拿到数据,且数据都是一样的:
import { createSSRApp } from "vue";
export function createApp(storeState = {}) {
return createSSRApp({
template: `<div>
<div>{{msg}}</div>
<div>{{count}}</div>
<button @click="count++">增加</button>
</div>`,
data() {
return {
msg: "Hello World",
count: storeState.count,
};
},
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
到这里就通过Vue和Express实现了一个简单的SSR,但实际情况可能会比这里更复杂一些,例如跳转路由,还有一些组件内部状态不一致导致水合失败等