【笔记】Vue3项目实现SSR

前言

Vue3项目实现SSR

下载依赖

1
2
npm install express vue @vue/server-renderer
npm install -D webpack webpack-cli webpack-node-externals vue-loader babel-loader @babel/core @babel/preset-env

客户端

定义Vue3根组件

src/App.vue
1
<template></template>

定义客户端

src/client/index.js
1
2
3
4
5
const { createSSRApp } = require("vue");
const App = require("../App.vue").default;

const app = createSSRApp(App);
app.mount("#app");

定义Webpack配置

config/client/webpack.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
const path = require("path");
const { VueLoaderPlugin } = require("vue-loader");

module.exports = {
target: "web",
mode: "development",
entry: "./src/client/index.js",
output: {
filename: "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()]
}

通过Webpack打包客户端

1
webpack --config config/client/webpack.config.js

服务器

定义Vue3根组件

src/App.vue
1
<template></template>

定义服务器

  • 渲染Vue3根组件
  • 实现JS水合激活(Hydration)
src/server/app.js
1
2
3
4
5
6
const { createSSRApp } = require("vue");
const App = require("../App.vue").default;

module.exports = function createApp() {
return createSSRApp(App);
}
src/server/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
const express = require("express");
const path = require("path");
const { renderToString } = require("@vue/server-renderer");
const createApp = require("./app.js");

const server = express();
server.use(express.static(path.resolve(__dirname, "../client")));
server.get("/", async function (request, response) {
const app = createApp();
const appHTML = await renderToString(app);
response.send(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">${appHTML}</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
});
server.listen(3000);

定义Webpack配置

config/server/webpack.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
const path = require("path");
const nodeExternals = require("webpack-node-externals");
const { VueLoaderPlugin } = require("vue-loader");

module.exports = {
target: "node",
mode: "development",
entry: "./src/server/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "../../build/server"),
libraryTarget: "commonjs2"
},
module: {
rules: [
{
test: /\.js$/,
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
}
},
{
test: /\.vue$/,
loader: "vue-loader"
}
]
},
plugins: [new VueLoaderPlugin()],
externals: [
nodeExternals({
allowlist: ["vue", "@vue/server-renderer"]
})
]
}

通过Webpack打包服务器

1
webpack --config config/server/webpack.config.js

启动SSR服务器

  • 先通过Webpack依次打包客户端和服务器,再启动打包好的服务器程序
1
2
3
4
webpack --config config/client/webpack.config.js
webpack --config config/server/webpack.config.js

node build/server/bundle.js

整合路由

  • 整合路由实现多组件

下载依赖

1
npm install vue-router@4

定义路由

src/views/Home.vue
1
<template></template>
src/views/About.vue
1
<template></template>
src/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
const { createRouter } = require("vue-router");

const routes = [
{
path: "/",
component: function () {
return import("../views/Home.vue");
}
},
{
path: "/about",
component: function () {
return import("../views/About.vue");
}
}
];

export default function (history) {
return createRouter({
history: history,
routes: routes
});
}

使用路由

src/client/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
const { createSSRApp } = require("vue");
const App = require("../App.vue").default;
const createRouter = require("../router/index.js");
const { createWebHistory } = require("vue-router");

const app = createSSRApp(App);

const router = createRouter(createWebHistory());
app.use(router);

router.isReady().then(function () {
app.mount("#app");
});
src/server/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
27
28
29
30
31
32
33
const express = require("express");
const path = require("path");
const { renderToString } = require("@vue/server-renderer");
const createApp = require("./app.js");
const createRouter = require("../router/index.js");
const { createMemoryHistory } = require("vue-router");

const server = express();
server.use(express.static(path.resolve(__dirname, "../client")));
server.get("/*", async function (request, response) {
const app = createApp();

const router = createRouter(createMemoryHistory());
app.use(router);
await router.push(request.url || "/");
await router.isReady();

const appHTML = await renderToString(app);
response.send(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">${appHTML}</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
});
server.listen(3000);
src/App.vue
1
2
3
4
5
6
7
8
9
<template>
<router-link to="/">
<button>home</button>
</router-link>
<router-link to="/about">
<button>about</button>
</router-link>
<router-view></router-view>
</template>

整合Pinia

  • 整合Pinia实现状态管理

下载依赖

1
npm install pinia

定义状态

src/store/global.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { defineStore } = require("pinia");

module.exports.useGlobalStore = defineStore("global", {
state: function () {
return {
key: "default"
};
},
actions: {
fn: function () {
this.key = "value";
}
}
});

使用状态

src/client/global.js
1
2
3
4
5
6
7
8
9
10
const { createSSRApp } = require("vue");
const App = require("../App.vue").default;
const { createPinia } = require("pinia");

const app = createSSRApp(App);

const pinia = createPinia();
app.use(pinia);

app.mount("#app");
src/server/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
27
28
29
30
const express = require("express");
const path = require("path");
const { renderToString } = require("@vue/server-renderer");
const createApp = require("./app.js");
const { createPinia } = require("pinia");

const server = express();
server.use(express.static(path.resolve(__dirname, "../client")));
server.get("/", async function (request, response) {
const app = createApp();

const pinia = createPinia();
app.use(pinia);

const appHTML = await renderToString(app);
response.send(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">${appHTML}</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
});
server.listen(3000);
src/App.vue
1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { storeToRefs } from "Pinia";
import { useGlobalStore } from "./store/global.js"

const globalStore = useGlobalStore();
const { key } = storeToRefs(globalStore);
console.log(key.value); // "default"

globalStore.fn();
console.log(key.value); // "value"
</script>

完成