vue ssr 实例
目录结构

新建项目,npm init -y
package.json:
{ "name": "vue-ssr2", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js", //构建客户端 "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js",//构建服务端 "build": "rimraf dist && npm run build:client && npm run build:server", "start": "cross-env NODE_ENV=production node server.js", //构建之后运行 "dev": "rimraf dist && cross-env NODE_ENV=dev node server.js" //实时构建运行 }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.17.10", "@babel/plugin-transform-runtime": "^7.17.10", "@babel/preset-env": "^7.17.10", "axios": "^0.27.2", "babel-loader": "^8.2.5", "chokidar": "^3.5.3", "cross-env": "^7.0.3", "file-loader": "^6.2.0", "friendly-errors-webpack-plugin": "^1.7.0", "rimraf": "^3.0.2", "url-loader": "^4.1.1", "vue": "^2.6.14", "vue-loader": "^15.7.0", "vue-router": "^3.5.1", "vue-server-renderer": "^2.6.14", "vue-template-compiler": "^2.6.14", "vuex": "^3.6.2", "webpack": "^4.41.5", "webpack-cli": "^3.3.10", "webpack-dev-middleware": "^5.3.1", "webpack-merge": "^5.8.0", "webpack-node-externals": "^3.0.0", "css-loader": "5", "express": "^4.18.1", "vue-meta": "^2.4.0" }, "dependencies": { } }
路由router
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../pages/Home.vue";
Vue.use(VueRouter);
const routes = [
{
path: "/",
name: "home",
component: Home,
},
{
path: "/about",
name: "about",
component: () =>
import(/* webpackChunkName: "about" */ "../pages/About.vue"),
},
{
path: "/post",
name: "post",
component: () =>
import(/* webpackChunkName: "about" */ "../pages/Post.vue"),
},
{
path: "*",
name: "error",
component: () => import("../pages/error.vue"),
},
];
// 修改后的写法
export default function createRouter() {
return new VueRouter({
mode: "history", // 一定要history
base: process.env.BASE_URL,
routes,
});
}
store
import axios from "axios"; import Vue from "vue"; import Vuex from "vuex"; Vue.use(Vuex); export default function createStore() { return new Vuex.Store({ state: () => ({ posts: [], }), actions: { // 在服务端渲染期间务必让 action 返回一个 Promise async getPosts({ commit }) { const { data } = await axios.get("http://localhost:3002/postdata");//获取post.js接口数据 commit("setPosts", data.data); }, }, mutations: { setPosts(state, posts) { state.posts = posts; }, }, }); }
app.js入口
import Vue from "vue"; import App from "./app.vue"; import createRouter from "./router"; import Meta from "vue-meta"; import createdStore from "./store"; Vue.use(Meta); Vue.mixin({ metaInfo: { titleTemplate: "%s - ssr", }, }); //导出一个工厂函数 export function createApp() { const router = new createRouter(); const store = new createdStore(); const app = new Vue({ router, store, render: (h) => h(App), }); return { app, router, store }; }
入口
客户端entry-client.js
import { createApp } from "./app";
const { app, router, store } = createApp();
//服务端渲染的数据通过<script>标签放入元素,vuex接管
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
//路由异步加载完成之后挂载
router.onReady(() => {
app.$mount("#app");
});
服务端entry-server.js
import { resolve } from "../build/webpack.base.config";
import { createApp } from "./app";
// context实际上就是server.js里面传参,后面会说到server.js
export default async (context) => {
const { app, router, store } = createApp();
//处理路由,数据等
//拿到meta信息
const meta = app.$meta();
//设置服务端router位置
router.push(context.url);
context.meta = meta;
//等routr将异步加载路由钩子解析完
await new Promise(router.onReady.bind(router));
context.rendered = () => {
context.state = store.state;
};
return app;
};
构建
webpack.base.config.js
const path = require("path");
const FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
const reolve = (file) => path.resolve(__dirname, file);
const isProd = process.env.NOOD_ENV === "production";
module.exports = {
mode: isProd ? "production" : "development",
// 出口
output: {
path: reolve("../dist/"),
publicPath: "/dist/",
filename: "[name].js",
},
resolve: {
extensions: [".js", ".vue", ".json"],
alias: {
vue$: "vue/dist/vue.esm.js",
"@": reolve("../src/"),
},
},
devtool: isProd ? "source-map" : "cheap-module-eval-source-map",
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: "url-loader",
options: {
limit: 8129,
},
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: "url-loader",
options: {
limit: 10000,
},
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: "url-loader",
},
{
test: /\.vue$/,
loader: "vue-loader",
},
{
test: /\.css$/,
use: ["vue-style-loader", "css-loader"],
},
],
},
devServer: {
proxy: {},
},
plugins: [new VueLoaderPlugin(), new FriendlyErrorsWebpackPlugin()],//友好日志信息
};
webpack.client.config.js
const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.base.config");
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
module.exports = merge(baseConfig, {
entry: {
app: "./src/entry-client.js",
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
plugins: [["@babel/plugin-transform-runtime"]],
},
},
},
],
},
optimization: {
splitChunks: {
name: "mainfest",
minChunks: Infinity,
},
},
plugins: [new VueSSRClientPlugin()], //构建server的json文件
});
webpack.server.config.js
const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.base.config");
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin");
const nodeExternals = require("webpack-node-externals");
module.exports = merge(baseConfig, {
entry: "./src/entry-server.js",
target: "node",//此标签一定要有
output: {
filename: "server-bundle.js",
libraryTarget: "commonjs2",
},
externals: [
nodeExternals({
allowlist: [/\.css$/],
}),
],
plugins: [new VueSSRServerPlugin()],//构建client的json文件
});
动态打包构建
stup-dev-server.js
const fs = require("fs");
const path = require("path");
const chokidar = require("chokidar");
const webpack = require("webpack");
const middleware = require("webpack-dev-middleware");
const resolve = (filename) => path.resolve(__dirname, filename);
let num = 0;
module.exports = (server, callback) => {
let ready;
const onReader = new Promise((r) => (ready = r));
//监视构建 ->更新Renderer
let template;
let serverBunder;
let clientManifest;
const update = () => {
if (template && serverBunder && clientManifest) {
ready();
console.log(++num + "次:数据修改了");
callback(template, serverBunder, clientManifest);
}
};
//监视构建template,调用update渲染
const templatePath = resolve("../index.template.html");
template = fs.readFileSync(templatePath, "utf-8");
update();
chokidar.watch(templatePath).on("change", () => {
template = fs.readFileSync(templatePath, "utf-8");
update();
});
//监视构建serverBunder,调用update渲染
const serverConfig = require("./webpack.server.config");
const serverCompiler = webpack(serverConfig);
serverCompiler.watch({}, (err, status) => {
if (err) throw err;
if (status.hasErrors()) return;
serverBunder = JSON.parse(
fs.readFileSync(resolve("../dist/vue-ssr-server-bundle.json"), "utf8")
);
update();
});
clientComplier.watch({}, (err, status) => {
if (err) throw err;
if (status.hasErrors()) return;
clientManifest = JSON.parse(
fs.readFileSync(resolve("../dist/vue-ssr-client-manifest.json"), "utf8")
);
update();
});
return onReader;
};
server.js服务器
const fs = require("fs");
const { createBundleRenderer } = require("vue-server-renderer");
const setUpDevserver = require("./build/setup-dev-server");
const express = require("express");
const isProd = process.env.NODE_ENV === "production" ? true : false;
//创建服务器
const server = express();
server.use("/dist", express.static("./dist")); //将dist下的资源获取到
let render;
let onReader;
if (isProd) {
//生产环境,直接使用打包好的资源
const serverBunder = require("./dist/vue-ssr-server-bundle.json"); //服务端打包的json
const template = fs.readFileSync("./index.template.html", "utf-8");
const clientManifest = require("./dist/vue-ssr-client-manifest.json");
render = createBundleRenderer(serverBunder, {
template,
clientManifest,
});
} else {
//开发环境 监视打包构建=>重新生成render渲染器
onReader = setUpDevserver(
server,
(template2, serverBunder2, clientManifest2) => {
//打包在内存中数据编译失去效果template2, serverBunder2, clientManifest2返回数据不能编译
//读取打包到硬盘中的数据
const serverBunder = require("./dist/vue-ssr-server-bundle.json"); //服务端打包的json
const template = fs.readFileSync("./index.template.html", "utf-8");
const clientManifest = require("./dist/vue-ssr-client-manifest.json");
render = createBundleRenderer(serverBunder, {
template,
clientManifest,
});
}
);
}
const renderData = async (req, res) => {
try {
const html = await render.renderToString({
url: req.url,
title: "测试",
}); //对象{}数据返给entry-server.js的context
res.setHeader('Content-Type','text/html;charset=utf8')
res.end(html);
} catch (err) {
res.status(500).end("myerror");
}
};
//所有路由经过该渲染
server.get(
"*",
isProd
? renderData
: async (req, res) => {
//等待重新打包渲染
await onReader;
renderData(req, res);
}
);
server.listen(3001, () => {
console.log("3001 is running");
});
index.template.html
<!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" />
{{{meta.inject().title.text()}}}
{{{meta.inject().meta.text()}}}
</head>
<body>
<!--vue-ssr-outlet-->//此标签一定要有
</body>
</html>
数据服务器post.js
const fs = require("fs");
const express = require("express");
const { resolve } = require("path");
//创建服务器
const server = express();
server.get("/postdata", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
const data1 = {
data: [
{ id: 001, title: "1111" },
{ id: 002, title: "2222" },
{ id: 003, title: "3333" },
{ id: 004, title: "4444" },
{ id: 005, title: "5555" },
],
};
res.send(data1);
});
server.listen(3002, () => {
console.log("3002 is running");
});
app.vue
<template>
<div id="app">
<ul>
<li>
<router-link to="/">Home</router-link>
</li>
<li>
<router-link to="/about">About</router-link>
</li>
</ul>
<router-view></router-view>
<Post></Post>
</div>
</template>
<script>
import Post from './pages/Post.vue'
export default {
name: 'App',
data() {
return {
message: 'hello ssr',
}
},
components: { Post },
}
</script>
<style></style>
Post.vue
<template>
<div>
<h1>Post List</h1>
<ul>
<li v-for="post in posts" :key="post.id">{{post.title}}</li>
</ul>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
name: 'PostList',
metaInfo: {
title: 'Posts'
},
data() {
return {}
},
computed: {
...mapState(['posts'])
},
methods: {
...mapActions(['getPosts'])
},
// Vue SSR 特殊为服务端渲染提供的一个生命周期钩子函数
async serverPrefetch() {
// 发起 action, 返回 Promise
// return this.$store.dispatch('getPosts')
return this.getPosts()
},
}
</script>
<style>
</style>
npm run dev效果图:先由服务端返回数据,再在页面展示


浙公网安备 33010602011771号