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效果图:先由服务端返回数据,再在页面展示

 

posted @   lijun12138  阅读(129)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示