SSR

对比

SPA单页面应用

优点:

  1. 只需要加载一次
  2. 更好的用户体验 [切断页面不卡顿]
  3. 可轻松的构建功能丰富的 Web 应用程序

缺点:

  1. 不利于SEO,返回的是空白HTML, 爬虫无法爬取到数据
  2. 受屏渲染速度慢,项目体积比较庞大
  3. 大文件可能变得难以维护

SPA详解

  1. SPA 应用默认只返回一个空的 HTML 页面,如: Body 中只有 ``
    `
  2. 而整个应用程序的内容都是通过 JavaScript 动态加载,包括应用的程序逻辑、UI以及与服务器通信相关的所有数据。

SPA流程图:

SPA流程分析

CRS原理

SSG静态站点生成

优点:

  1. 有利于SEO
  2. 访问速度快

缺点:

  1. 不利于展示实时性内容,实时性的更适合SSR
  2. 如果站点内容更新了,那必须 重新再次构建和部署

SSR服务器端渲染

优点

  1. 更快的首屏渲染速度
  2. 更好的SEO

缺点

  1. 服务器成本高
  2. 增加一定的开发成本

SSR流程:

SSR流程

Vue3+SSR

Node-server搭建

控制台安装相关依赖

1
2
3
4
1. npm init -y
2. npm i express
3. npm i -D nodemon
4. npm i -D webpack webpack-cli webpack-node-externals

src / server / index.js 基本服务器,启动服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let express = require("express")

const server = express();

server.get("/", (req, res) => {
res.send(
`Hello Node server`
)
})

server.listen(3000, () => {
console.log("start node server on 3000~");
})
// node ./src/server/index.js (可修改启动脚本: package.json: "dev": "node ./src/server/index.js")

src / config / server.config.js 配置打包结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let path = require('path')
let nodeExternals = require("webpack-node-externals")

module.exports = {
target: 'node',
mode: 'development',
entry: './src/server/index.js',
output: {
filename: "server_bundle.js",
path: path.resolve(__dirname, '../build/server')
},
externals: [nodeExternals()] // 排除node_module下的某些包
}
// "build:server": "webpack --config ./config/webpack.config.js --watch"

vue3 + ssr

控制台安装相关依赖

1
2
3
4
5
6
7
1. npm i express
2. npm i -D nodemon
3. npm i vue
4. npm i -D vue-loader
5. npm i -D babel-loader @babel/preset-env
6. npm i -D webpack webpack-cli
7. npm i -D webpack-merge webpack-node-externals

src / config / client.config.js 配置打包结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
let path = require("path");
let { VueLoaderPlugin } = require("vue-loader/dist/index.js");
let { DefinePlugin } = require("webpack");

module.exports = {
target: "web",
mode: "development",
entry: "./src/client/index.js",
output: {
filename: "client_bundle.js",
path: path.resolve(__dirname, "../build/client"),
},
module: {
rules: [
{
test: /\.js$/,
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
{
test: /\.vue$/,
loader: "vue-loader",
},
],
},
plugins: [
new VueLoaderPlugin(),
new DefinePlugin({
__VUE_OPTIONS_API__: false,
__VUE_PROD_DEVTOOLS__: false,
}),
],
resolve: {
// 添加这些扩展名,项目中导入包就不需要编写文件名后缀
extensions: [".js", ".json", ".wasm", ".jsx", ".vue"],
},
};

水合

  1. 需要在 src/client/index.jscreateApp 然后挂载到 app 上,这个 appsrc/server/index.jsres.send(``````````)div 所挂载的 app
  2. res.send() 是后端返回给前端的渲染页面, 其中 appStringHtml 是处理后端 createSSRApp 所创建出来的 app
  3. <script src="/client/client_bundle.js"></script> 是将 client 打包的结果与 server 进行水合。

跨请求状态污染

  1. 由于是 SSR 后端进行渲染,所以我们会在后端请求的代码中,针对每一个浏览器请求的 App 都进行重新创建一个全新的实例。
  2. 在创建App或路由或Store都是采用函数的方式来创建的,防止不同域名下的浏览器访问到的 app 是同一个,从而导致请求状态污染。

优化

1
1. npm i webpack-merge -D //将config文件夹中的server和client公共配置放在base中

config / base.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
let { VueLoaderPlugin } = require("vue-loader/dist/index.js");

module.exports = {
mode: "development",
module: {
rules: [
{
test: /\.js$/,
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
{
test: /\.vue$/,
loader: "vue-loader",
},
],
},
plugins: [new VueLoaderPlugin()],
resolve: {
// 添加这些扩展名,项目中导入包就不需要编写文件名后缀
extensions: [".js", ".json", ".wasm", ".jsx", ".vue"],
},
};

// 在client | server.config.js 中使用
// 合并公共配置
let { merge } = require("webpack-merge");
let baseConfig = require("./base.config");
module.exports = merge(baseConfig, {该文件特有配置})

多页面SSR-Router

1
1. npm install vue-router --save  //实现多页面SSR

router / index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { createRouter } from "vue-router";

const routes = [
{
path: "/",
component: () => import("../views/home.vue"),
},
{
path: "/about",
component: () => import("../views/about.vue"),
},
];

/**
*为防止全局污染,把它放在一个匿名函数中,防止函数名冲突
* @export
* @param {*} history 动态穿递的路由参数(不确定是客户端调用还是服务器端调用)
* @return {*}
*/
export default function (history) {
return createRouter({
history,
routes,
});
}

水合Hydration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 在 src/server/index.js
import createRouter from "../router"
// 在node中使用这种路由模式
import { createMemoryHistory } from "vue-router";
// app 安装路由插件
let router = createRouter(createMemoryHistory())
app.use(router)
await router.push(req.url || '/')
await router.isReady() // 等待路由加载完成在渲染页面

// 在 src/client/index.js
import createRouter from "../router";
import { createWebHistory } from "vue-router";
// 注册路由
let router = createRouter(createWebHistory());
app.use(router);
// 等待路由加载完成后再挂载
router.isReady().then(() => {
app.mount("#app");
});

效果展示:

多页面后端返回渲染页面结果

Pinia

安装相关依赖

1
1. npm i pinia --save

注意:

  1. 避免跨请求状态污染,需要在每个请求中都创建一个全新的pinia
  2. 客户端和服务器端都需要各自创建pinia
  3. 页面中pinia使用方法与Vue一致

src / store / index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { defineStore } from "pinia";

// 防止跨域请求污染
export const useHomeStore = defineStore("home", {
state() {
return {
count,
};
},
actions: {
increment() {
this.count++;
},
decrement() {
this.count--;
},

// 也支持异步请求
},
});

水合Hydration:

1
2
3
4
5
6
7
8
9
10
11
12
// src/server/index.js 
// 使用pinia
import { createPinia } from 'pinia'
// app 安装pinia
let pinia = createPinia()
app.use(pinia)
// src/client/index.js
// 使用pinia
import { createPinia } from 'pinia'
// app 安装pinia
let pinia = createPinia()
app.use(pinia)

SSR
http://example.com/2023/12/24/SSR/
作者
Caoqin
发布于
2023年12月24日
许可协议