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
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 | 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效果图:先由服务端返回数据,再在页面展示
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通