Vue除了支持开发SPA应用之外,其实也是支持开发SSR应用的。 在Vue中创建SSR应用,需要调用createSSRApp函数,而不是createApp
- createApp:创建应用,直接挂载到页面上
- createSSRApp:创建应用,是在激活的模式下挂载应用
服务端用@vue/server-renderer包中的renderToString来进行渲染。

搭建SSR开发环境
需安装的依赖项:
- npm i express
- npm i --D nodemon 启动Node程序时并监听文件的变化,变化即刷新
- npm i -D webpack webpack-cli webpack-merge webpack-node-externals 排除掉node_modules中所以的模块
- npm i vue
- npmi -D vue-loader 加载.vue文件
- npmi -D babel-loader @babel/preset-env 加载JS文件,转换新语法
编写服务端代码
App.vue
js
<template>
<div class="app" style="border: 1px solid red;">
<h2>Vue App</h2>
<div> {{ count }}</div>
<button @click="addCount">+1</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(100);
const addCount = () => {
count.value++;
}
</script>
app.js
js
import { createSSRApp } from "vue";
import App from "./App.vue";
//避免夸请求状态的污染,通过函数返回app实例,保证每个请求都会返回一个app实例
export default function createApp() {
let app = createSSRApp(App);
return app;
}
思考一个问题,为什么需要写一个函数来返回app实例
在SPA中,整个生命周期中只有一个App对象实例或一个Router对象实例或一个Store对象实例都是可以的,因为每个用户在使用浏览器访问SPA应用时,应用模块都会重新初始化,这也是一种单例模式。
然而,在SSR 环境下,App应用模块通常只在服务器启动时初始化一次。同一个应用模块会在多个服务器请求之间被复用,而我们的单例状态对象也一样,也会在多个请求之间被复用,比如:
- 当某个用户对共享的单例状态进行修改,那么这个状态可能会意外地泄露给另一个在请求的用户。
- 我们把这种情况称为:跨请求状态污染。
为了避免这种跨请求状态污染,SSR的解决方案是:
- 可以在每个请求中为整个应用创建一个全新的实例,包括后面的router 和全局store等实例。
- 所以我们在创建App或路由或Store对象时都是使用一个函数来创建,保证每个请求都会创建一个全新的实例。
- 这样也会有缺点:需要消耗更多的服务器的资源。
server/index.js
js
let express = require("express");
import createApp from "../app";
import { renderToString } from "@vue/server-renderer";
let server = express();
server.get("/", async (req, res) => {
let app = createApp();
let appStringHtml = await renderToString(app);
res.send(
`
<!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>
<h1>Vue Server Side Rendering</h1>
<div id="app">${appStringHtml}</div>
</body>
</html>
`
);
});
server.listen(3000, () => {
console.log("Server is running on 3000");
});
wepack配置以及脚本文件
wepack.config.js
js
let path = require("path");
let nodeExternals = require("webpack-node-externals");
let { VueLoaderPlugin } = require("vue-loader/dist/index");
module.exports = {
target: "node",
mode: "development",
entry: "./src/server/index.js",
output: {
filename: "server_bundle.js",
path: path.resolve(__dirname, "../build/server"),
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
{
test: /\.vue$/,
loader: "vue-loader",
},
],
},
plugins: [new VueLoaderPlugin()],
resolve: {
//添加了这些拓展名,项目中的导入的拓展名文件就不用写文件的后缀
extensions: [".js", ".jsx", ".json", ".vue", ".wasm"],
},
externals: [nodeExternals()], //排除node_modules 中的包
};
js
"dev": "nodemon ./src/server/index.js",
"build:server": "webpack --config ./config/server.config.js --watch",
"start": "nodemon ./build/server/server_bundle.js"
当我们启动服务,发现已经返回了一个页面在浏览器上面,但是无法触发时间,这是因为该页面是静态的需要激活页面,也就是 Hydration

激活Hydration
首先在scr目录里面重新新建一个文件夹目录为scr/client/index.js 这里放置vue的初始化代码
js
import { createApp } from "vue";
import App from "../App.vue";
//SPA
let app = createApp(App);
app.mount("#app");
然后在配置文件中配置客户端的打包配置文件
js
let path = require("path");
let { VueLoaderPlugin } = require("vue-loader/dist/index");
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|jsx)$/,
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
{
test: /\.vue$/,
loader: "vue-loader",
},
],
},
plugins: [new VueLoaderPlugin()],
resolve: {
//添加了这些拓展名,项目中的导入的拓展名文件就不用写文件的后缀
extensions: [".js", ".jsx", ".json", ".vue", ".wasm"],
},
};
将App.vue打包为一个客户端的client_bundle.js文件,用来激活应用,使页面有交互效果
将App.vue打包为一个服务器端的server_bundle.js文件,用来在服务器端动态生成页面的HTML
在server/index.js中添加静态文件部署
js
let server = express();
server.get("/", async (req, res) => {
let app = createApp();
let appStringHtml = await renderToString(app);
//部署静态资源
server.use(express.static("build"));
res.send(
`
<!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>
<h1>Vue Server Side Rendering</h1>
<div id="app">${appStringHtml}</div>
<script src="/client/client_bundle.js"></script>
</body>
</html>
`
);
});
server_bundle.js渲染的页面+client_bundle.js文件进行Hydration
此时的文件目录
