LOADING

vue ssr

2024/10/23

通过服务器渲染页面

客户端渲染缺点:

首屏加载慢 (落地页)

SEO 不好

服务器渲染缺点:

服务端要跑一次,客户端也要渲染

原理

利用 vue-server-renderer 包先在服务器端生成渲染后的 html 代码

服务端渲染要做的三件事:

  1. 创建 vue 实例

  2. 创建渲染器

  3. 利用渲染器将 vue 实例转化成 html 字符串

1.先搭建一个 vue 环境,入口需要特殊处理

注意:其中 main.js 需要服务器端和客户端共用

2.打包两套代码(服务器端使用和客户端使用)

3.启动一个 node 服务器

const express = require("express");
const server = express();
const fs = require("fs");
const { resolve } = require("path");
const { createBundleRenderer } = require("vue-server-renderer");
const serverBundle = require("../dist/vue-ssr-server-bundle.json");
const clientManifest = require("../dist/vue-ssr-client-manifest.json");

server.use(express.static(resolve("../dist"), { index: false }));

server.get("*", async (req, res) => {
  try {
    // 2. 创建渲染器
    const render = createBundleRenderer(serverBundle, {
      template: fs.readFileSync("./index.ssr.html", "utf-8"),
      clientManifest,
    });
    const html = await render.renderToString();
    res.send(html);
    // 3. 利用渲染器将vue实例转化成html字符串
  } catch (error) {
    console.log(error);
    res.status(500).send("服务器错误");
  }

  // res.send('hello');
});

server.listen(12306, () => console.log("server is run at 12306"));

真实的 html 页面

内部其实打包了两份代码以后 createBundleRenderer 渲染的是服务器打包出来的 html 页面,然而 css 和 js 还是靠客户端打包出来的 js 运行(只为了 seo 和渲染节约时间)

4.vue-router 的模式一定是 history

5.细节(router 传递 url 的值)

server index.js(运行服务器)

const express = require("express");
const server = express();
const fs = require("fs");
const { resolve } = require("path");
const { createBundleRenderer } = require("vue-server-renderer");
const serverBundle = require("../dist/vue-ssr-server-bundle.json");
const clientManifest = require("../dist/vue-ssr-client-manifest.json");

server.use(express.static(resolve("../dist"), { index: false }));

server.get("*", async (req, res) => {
  // /demo, req.url /demo
  try {
    const url = req.url;
    // 2. 创建渲染器
    const render = createBundleRenderer(serverBundle, {
      template: fs.readFileSync("./index.ssr.html", "utf-8"),
      clientManifest,
    });
    const html = await render.renderToString({ url }); //这里可以传参数
    res.send(html);
    // 3. 利用渲染器将vue实例转化成html字符串
  } catch (error) {
    console.log(error);
    if (error.code == 404) {
      res.status(404).send("页面去火星了,找不到了,404啦");
      return;
    }
    res.status(500).send("服务器错误");
  }

  // res.send('hello');
});

server.listen(12306, () => console.log("server is run at 12306"));

server.entry.js(打包的服务器端)

import createApp from "../main.js";

export default function (ctx) {
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp();
    router.push(ctx.url);
    router.onReady(() => {
      // 判断当前路由下是否存在组件
      const matchedComponents = router.getMatchedComponents();
      if (matchedComponents.length == 0) {
        return reject({ code: 404 });
      }
      Promise.all(
        matchedComponents.map((c) => {
          if (c.asyncData) {
            return c.asyncData(store);
          }
        })
      )
        .then(() => {
          // window.__INITIAL_STATE__
          ctx.state = store.state;
          resolve(app);
        })
        .catch(reject);
      // resolve(app);
    }, reject);
  });
}

6.store 数据预取

在页面加载之前,登录信息等数据想要同步获取到而不是闪烁(提前将异步数据跟着页面过来)

<template>
  <div>
    <h1>我是Home页面</h1>
    <div>欢迎,{{ name }}</div>
  </div>
</template>

<script>
import { mapState } from 'vuex';
export default {
  computed: {
    ...mapState(['name']),
  },
  created() {
    this.$store.dispatch('getName');
  },

  //可以在vue中编写一个方法
  asyncData(store) {
    return store.dispatch('getName');
  }
}
</script>

<style>

</style>

server.entry.js

调用方法

保存结果

import createApp from "../main.js";

export default function (ctx) {
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp();
    router.push(ctx.url);
    router.onReady(() => {
      // 判断当前路由下是否存在组件
      const matchedComponents = router.getMatchedComponents();
      if (matchedComponents.length == 0) {
        return reject({ code: 404 });
      }
      Promise.all(
        matchedComponents.map((c) => {
          //这边判断如果有这个方法的话就直接调用
          if (c.asyncData) {
            return c.asyncData(store);
          }
        })
      )
        .then(() => {
          // 会将结果保存到这里面
          //服务端保存最新的state仓库数据
          // window.__INITIAL_STATE__
          ctx.state = store.state;
          resolve(app);
        })
        .catch(reject);
      // resolve(app);
    }, reject);
  });
}

vue-cli 改成 ssr

1.将 vue-router vuex main.js 都变成函数

import Vue from "vue";
import App from "./app.vue";
import createRouter from "./router";
import createStore from "./store";

export default function () {
  const router = createRouter();
  const store = createStore();
  const app = new Vue({
    render: (h) => h(App),
    router,
    store,
  });
  return { app, router, store };
}

2.编写 server 文件夹(最终运行的服务器)

3.编写 entry 文件夹(里面分成服务器端渲染和客户端渲染两部分)

4.编写 vue.config.js

const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin");
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";

module.exports = {
  configureWebpack: () => ({
    entry: {
      app: `./src/entry/${target}.entry.js`,
    },
    target: TARGET_NODE ? "node" : "web",
    output: {
      libraryTarget: TARGET_NODE ? "commonjs2" : undefined,
    },
    plugins: [
      TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin(),
    ],
  }),
  chainWebpack: (config) => {
    config.optimization.splitChunks(undefined);
  },
};

5.编写脚本命令

"scripts": {
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "lint": "vue-cli-service lint",
  "build:client": "vue-cli-service build",
  "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server",
  "build:all": "npm run build:server && move dist\\vue-ssr-server-bundle.json bundle && npm run build:client && move bundle dist\\vue-ssr-server-bundle.json"
},

nuxt.js

vue ssr 的脚手架

翻盖上面自己搭的脚手架,还多很多东西(router,vuex 都已经集合进去了)

api

路由

<nuxt-link to="/">首页</nuxt-link>

layouts 模板

<div id="components-layout-demo-basic">
  <a-layout>
    <a-layout-header>Header</a-layout-header>
    <a-layout-content>
      <nuxt />
    </a-layout-content>
    <a-layout-footer>Footer</a-layout-footer>
  </a-layout>
</div>

nuxt 的基本目录

components:组件,可以直接使用

layouts:模板,每个页面可以使用不一样的模板

middleware:中间件,用于打开页面的服务器前置条件

pages:每一个页面对应一个路由

static:静态文件(图标)

store:全局可用的数据(vuex)

plugins:第三方插件在这里(axios,element-plus)

nuxt.config.js:基本所有的配置放在这里

_开头是一个动态路由 nuxt 框架