SSR初体验
基础搭建
安装依赖
 
 先开启一个服务器
let express = require("express");
let server = express();
server.get("/", (req, res) => {
	res.send(
		`
			Hello Node Server
		`
	);
});
server.listen(3000, () => {
	console.log("start node server on 3000");
});
 
配置wp.config.js
let path = require("path");
let nodeExternals = require("webpack-node-externals");
module.exports = {
	target: "node", //fs path就不会打包了
	mode: "development",
	entry: "./src/server/index.js",
	output: {
		filename: "server_bundle.js",
		path: path.resolve(__dirname, "../build/server"),
	},
	externals: [nodeExternals()], //排除掉node_module中的包
};
 
引入vue

 创建App.vue
<template>
	<div class="app" style="border: 1px solid red">
		<h2>Vue3 app</h2>
		<div>{{ count }}</div>
		<button @click="addCounter">+1</button>
	</div>
</template>
<script setup>
	import { ref } from "vue";
	const count = ref(100);
	function addCounter() {
		count.value++;
	}
</script>
 
app.js
import { createSSRApp } from "vue";
import App from "./App.vue";
//这里为什么写一个函数来返回app实例
//通过函数返回app实例 可以保证每个请求都会返回一个新的app实例 避免跨请求状态的污染
export default function createApp() {
	let app = createSSRApp(App);
	return app;
}
 
index.js引入vue
let express = require("express");
let server = express();
import createApp from "../app";
import { renderToString } from "@vue/server-renderer";
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 http-equiv="X-UA-Compatible" content="IE=edge" />
				<meta name="viewport" content="width=device-width, initial-scale=1.0" />
				<title>Document</title>
			</head>
			<body>
				<h1>Vue Serve Side Render</h1>
				<div id="app">
					${appStringHtml}
				</div>
			</body>
		</html>
		`
	);
});
server.listen(3000, () => {
	console.log("start node server on 3000");
});
 
修改server.config.js
let path = require("path");
let nodeExternals = require("webpack-node-externals");
let { VueLoaderPlugin } = require("vue-loader/dist/index");
module.exports = {
	target: "node", //fs path就不会打包了
	mode: "development",
	entry: "./src/server/index.js",
	output: {
		filename: "server_bundle.js",
		path: path.resolve(__dirname, "../build/server"),
	},
	module: {
		rules: [
			{
				test: /.\js$/,
				loader: "babel-loader",
				options: {
					presets: ["@babel/preset-env"],
				},
			},
			{
				test: /\.vue$/,
				loader: "vue-loader",
			},
		],
	},
	plugins: [new VueLoaderPlugin()], //对vue文件的打包
	resolve: {
		//添加了这些扩展明名之后 项目中导包如下的扩展名文件就不需要编写文件的后缀
		extensions: [".js", ".json", ".wasm", ".jsx", ".vue"],
	},
	externals: [nodeExternals()], //排除掉node_module中的包
};
 
npm run build:server 打包
 npm run start 开启
 访问http://localhost:3000/
 
但此时是静态页面 无法交互 需要激活
hydration
import { createApp } from "vue";
import App from "../App.vue";
let app = createApp(App);
app.mount("#app");
 
client.config.js
let path = require("path");
let { VueLoaderPlugin } = require("vue-loader");
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,
		}),
	], //对vue文件的打包
	resolve: {
		//添加了这些扩展明名之后 项目中导包如下的扩展名文件就不需要编写文件的后缀
		extensions: [".js", ".json", ".wasm", ".jsx", ".vue"],
	},
};
 
index.js
let express = require("express");
let server = express();
import createApp from "../app";
import { renderToString } from "@vue/server-renderer";
//部署静态资源
server.use(express.static("build"));
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 http-equiv="X-UA-Compatible" content="IE=edge" />
				<meta name="viewport" content="width=device-width, initial-scale=1.0" />
				<title>Document</title>
			</head>
			<body>
				<h1>Vue Serve Side Render</h1>
				<div id="app">
					${appStringHtml}
				</div>
				<script src="/client/client_bundle.js"></script>
			</body>
		</html>
		`
	);
});
server.listen(3000, () => {
	console.log("start node server on 3000");
});
 

 npm run build:server 打包
 npm run build:client打包
 npm run start 开启
 访问http://localhost:3000/ 可以交互

结合vue-router

router/index.js
import { createRouter } from "vue-router";
const routes = [
	{
		path: "/",
		component: () => import("../views/home.vue"),
	},
	{
		path: "/about",
		component: () => import("../views/about.vue"),
	},
];
export default function (history) {
	return createRouter({
		history,
		routes,
	});
}
 
App.vue
<template>
	<div class="app" style="border: 1px solid red">
		<h2>Vue3 app</h2>
		<div>{{ count }}</div>
		<button @click="addCounter">+1</button>
		<div>
			<router-link to="/">
				<button>home</button>
			</router-link>
			<router-link to="/about">
				<button>about</button>
			</router-link>
		</div>
		<!-- 路由的占位 -->
		<router-view></router-view>
	</div>
</template>
<script setup>
	import { ref } from "vue";
	const count = ref(100);
	function addCounter() {
		count.value++;
	}
</script>
 
views/home.vue
<template>
	<div class="app" style="border: 1px solid green; margin: 10px">
		<h2>home</h2>
		<div>{{ count }}</div>
		<button @click="addCounter">+1</button>
	</div>
</template>
<script setup>
	import { ref } from "vue";
	const count = ref(200);
	function addCounter() {
		count.value++;
	}
</script>
 
client/index.js
import { createApp } from "vue";
import App from "../App.vue";
import createRouter from "../router";
import { createWebHistory } from "vue-router";
let app = createApp(App);
//安装路由插件
let router = createRouter(createWebHistory());
app.use(router);
router.isReady().then(() => {
	app.mount("#app");
});
 
server.js
let express = require("express");
let server = express();
import createApp from "../app";
import { renderToString } from "@vue/server-renderer";
import createRouter from "../router";
// 内存路由 -> node用
import { createMemoryHistory } from "vue-router";
//部署静态资源
server.use(express.static("build"));
server.get("/*", async (req, res) => {
	let app = createApp();
	//app 安装路由插件
	let router = createRouter(createMemoryHistory());
	app.use(router);
	await router.push(req.url || "/"); // / or /about 等待页面跳转好
	await router.isReady(); // 等待(异步)路由加载完成 再渲染页面
	let appStringHtml = await renderToString(app);
	res.send(
		`
		<!DOCTYPE html>
		<html lang="en">
			<head>
				<meta charset="UTF-8" />
				<meta http-equiv="X-UA-Compatible" content="IE=edge" />
				<meta name="viewport" content="width=device-width, initial-scale=1.0" />
				<title>Document</title>
			</head>
			<body>
				<h1>Vue Serve Side Render</h1>
				<div id="app">
					${appStringHtml}
				</div>
				<script src="/client/client_bundle.js"></script>
			</body>
		</html>
		`
	);
});
server.listen(3000, () => {
	console.log("start node server on 3000");
});
 

结合pinia

 store/home.js
import { defineStore } from "pinia";
export const useHomeStore = defineStore("home", {
	state() {
		return {
			count: 1000,
		};
	},
	actions: {
		increment() {
			this.count++;
		},
		decrement() {
			this.count--;
		},
		// async fetchHomeData() {
		// 	let res = await axios.get();
		// 	this.homeInfo = res.data;
		// },
	},
});
 
client/index.js
import { createApp } from "vue";
import App from "../App.vue";
import createRouter from "../router";
import { createWebHistory } from "vue-router";
import { createPinia } from "pinia";
let app = createApp(App);
//安装路由插件
let router = createRouter(createWebHistory());
let pinia = createPinia();
app.use(router);
app.use(pinia);
router.isReady().then(() => {
	app.mount("#app");
});
 
server/index.js
let express = require("express");
let server = express();
import createApp from "../app";
import { renderToString } from "@vue/server-renderer";
import createRouter from "../router";
// 内存路由 -> node用
import { createMemoryHistory } from "vue-router";
import { createPinia } from "pinia";
//部署静态资源
server.use(express.static("build"));
server.get("/*", async (req, res) => {
	let app = createApp();
	//app 安装路由插件
	let router = createRouter(createMemoryHistory());
	app.use(router);
	await router.push(req.url || "/"); // / or /about 等待页面跳转好
	await router.isReady(); // 等待(异步)路由加载完成 再渲染页面
	//app 安装pinia插件
	let pinia = createPinia();
	app.use(pinia);
	let appStringHtml = await renderToString(app);
	res.send(
		`
		<!DOCTYPE html>
		<html lang="en">
			<head>
				<meta charset="UTF-8" />
				<meta http-equiv="X-UA-Compatible" content="IE=edge" />
				<meta name="viewport" content="width=device-width, initial-scale=1.0" />
				<title>Document</title>
			</head>
			<body>
				<h1>Vue Serve Side Render</h1>
				<div id="app">
					${appStringHtml}
				</div>
				<script src="/client/client_bundle.js"></script>
			</body>
		</html>
		`
	);
});
server.listen(3000, () => {
	console.log("start node server on 3000");
});
 
views/home.vue
<template>
	<div class="app" style="border: 1px solid green; margin: 10px">
		<h2>home</h2>
		<div>{{ count }}</div>
		<button @click="addCounter">+1</button>
	</div>
</template>
<script setup>
	import { storeToRefs } from "pinia";
	import { useHomeStore } from "../store/home";
	let homeStore = useHomeStore();
	let { count } = storeToRefs(homeStore);
	function addCounter() {
		count.value++;
	}
</script>
 
views/about.vue
<template>
	<div class="app" style="border: 1px solid blue; margin: 10px">
		<h2>about</h2>
		<div>{{ count }}</div>
		<button @click="addCounter">+1</button>
	</div>
</template>
<script setup>
	import { storeToRefs } from "pinia";
	import { useHomeStore } from "../store/home";
	let homeStore = useHomeStore();
	let { count } = storeToRefs(homeStore);
	function addCounter() {
		count.value++;
	}
</script>
 




















