Vue生态及实践 - SSR(下)

目录

目标

asyncData

src/module/topic/views/UTopic.vue

src/entry-server.js

src/entry-client.js

缓存相关处理

微缓存(Microcache)

server.js

src/module/topic/components/UItem.vue

Stream【node.js中的Stream;可以通过stream渲染html】

 server.js

Prerendering

build/webpack.client.config.js

src/prerender.template.html

build/webpack.base.config.js

一张ai图片~


SSR(ServerSideRender)服务器端渲染SSR(Server Side Render)

目标

  1. 同构代码中获取数据
  2. 性能调优——缓存
  3. 性能调优——Stream页面渲染(优化内存使用效率)
  4. 性能调优——Prerendering对页面进行静态化操作

asyncData

src/module/topic/views/UTopic.vue

<template>
  <div>
    <!-- <u-infinite-list :items="items" :item-height="80" #default="{ sliceItems }"> -->
    <u-list :items="items"></u-list>
    <!-- </u-infinite-list> -->
    <!-- 自定义指令intersect,出现在屏幕中执行handler -->
    <div class="x-bottom" v-intersect="{ handler: fetchNext }">
      滚动到底部,加载下一页
    </div>
  </div>
</template>
<script>
import { createNamespacedHelpers } from "vuex";
import UList from "../components/UList.vue";
// import UInfiniteList from "../../../components/UInfiniteList.vue";
// 便捷的映射topic命名空间的一个mapState方法
const { mapState, mapActions } = createNamespacedHelpers("topic");
export default {
  name: "u-top",
  props: ["type"],
  components: {
    UList,
    // UInfiniteList,
  },
  computed: {
    ...mapState({
      items: (state) => state[state.activeType].items,
    }),
  },
  // created() {
  //   this.fetchNext();
  // },
  // 1.asyncData
  // 服务端,客户端都可以使用,
  // 服务端可以同步获得数据进行渲染
  // 客户端也可以进行同样的操作
  // 约定的列表函数
  asyncData({ store, route }) {
    return store
      .dispatch("topic/FETCH_LIST_DATA", {
        type: route.name,
      })
      .catch((e) => console.error(e));
  },
  watch: {
    type(type) {
      this.fetchData({ type });
    },
  },
  methods: {
    ...mapActions({
      fetchData: "FETCH_LIST_DATA",
    }),
    fetchNext() {
      const { type } = this;
      this.fetchData({ type });
    },
  },
};
</script>
<style scoped>
.x-bottom {
  width: 100%;
  height: 1px;
}
</style>

src/entry-server.js

view-source:http://localhost:9090/top【访问源代码】

// 【服务端】入口文件:
import { createApp } from "./app";
const isDev = process.env.NODE_ENV !== "production";
import Vue from "vue";
import ULink from "./components/ULink.server.vue";
Vue.component("u-link", ULink);
export default (context) => { // nodejs 或 web server服务,返回一些对象
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp(); // 解构,获得app,router,store
    const s = isDev && Date.now();
    // url in router ? 判断一下当前请求的url是否在路由列表中 
    const { url } = context;
    // 解构出route.fullPath
    const { fullPath } = router.resolve(url).route;
    if (fullPath !== url) {
      return reject({ // 报错
        url: fullPath,
      });
    }
    // 路由跳转
    router.push(url);
    router.onReady(() => { // 路由触发后
      //=======================================================
      // 异步数据-1. 根据路由表信息获得路由组件信息
      const matchedComponents = router.getMatchedComponents();
      if (!matchedComponents.length) { // 路由不合法
        return reject({ code: 404 }); // 报错404
      }
      // /a/b/c // matchedComponents多级数组。
      // /a => A asyncData
      // /b => B asyncData
      // /c => C asyncData
      Promise.all(
        matchedComponents.map(
          ({ asyncData }) =>// 数组对应的asyncData解构出来
            asyncData && asyncData({ store, route: router.currentRoute })
        )
      )
        .then(() => {
          isDev && console.log(`data-fetched in: ${Date.now() - s}ms`);
          // 👇提供给页面进行vuex的初始化
          context.state = store.state;
          resolve(app);
        })
        .catch(reject);
      // 在服务端进行数据请求的操作
      //=======================================================
    });
  });
};

src/entry-client.js

// 客户端【浏览器端】入口文件:
// 1.只需创建应用程序,
// 2.还要挂载到dom当中,
// 3.还要做客户端激活操作,服务端数据和客户端后续要进行的操作有机结合起来
import { createApp } from "./app";
import Vue from "vue";
const { app, router, store } = createApp();
import ULink from "./components/ULink.client.vue";
Vue.component("u-link", ULink);
//=====================================================================
// 【服务端调用约定asyncData】context.state = store.state;生成的
if (window.__INITIAL_STATE__) {
  // vuex.store同步操作,vuex在客户端激活的操作
  store.replaceState(window.__INITIAL_STATE__);
}
Vue.mixin({
  // 监听路由localhost:9090/top/:id
  // 扩展
  beforeRouteUpdate(to, from, next) {
    // 解构
    const { asyncData } = this.$options;
    if (asyncData) {// 存在
      asyncData({ // 进行异步请求
        store: this.$store,
        route: to,
      })
        .then(() => next)
        .catch(next);
    } else {
      next();
    }
  },
});
// 同步asyncData数据处理完成了
//=====================================================================
// 1. 路由加载完成执行app.$mount操作;
router.onReady(() => {
  app.$mount("#app");// 3.app.$mount("#app", true)强制客户端激活操作
});
// 保证在服务端和客户端渲染完全一致,这样才可以激活,否则会有客户端激活失败的情况
// <div></div>
// <table>  <tbody><tr><td></td></tr></tbody>  </table>

缓存相关处理

微缓存(Microcache)

我们把一个缓存的有效性设置一个很小的时间,比如说1s,在这1s之内当页面只会进行一次渲染,其他时候我们都用缓存生成给用户

  • 页面级别缓存
    • 可以在node.js的路由层面进行缓存
    • 适用于页面内容基本不变的页面(运维推荐的页面)
  • 组件级别缓存
    • vue-server-renderer内置支持组件级别缓存,可以在创建renderer时传入LRU cache
    • 适用于千人千面的用户推荐页面(每个用户看到的是不同界面)

server.js

// 使用微缓存,要先安装个工具   npm i lru-cache
const fs = require("fs");
const path = require("path");
const express = require("express");
// 缓存:使用微缓存,要先安装个工具   npm i lru-cache
const LRU = require("lru-cache");
const setUpDevServer = require("./build/setup-dev-server");
const isProd = process.env.NODE_ENV === "production";
// 4.修改一下页面模板地址
const HTML_FILE = path.join(__dirname, "./src/index.template.html");
// 1. 安装个插件 npm i vue-server-renderer
const { createBundleRenderer } = require("vue-server-renderer");
const app = express();
// 缓存:页面级微缓存
const microCache = new LRU({
  max: 100,
  maxAge: 1000 * 60,
});
// ---------------------------------------------------------------------
// 2. 定义一个函数
// bundle :webpack打包输出的一个bundle
// options 参数传递
const createRenderer = (bundle, options) =>
  createBundleRenderer(
    bundle,
    Object.assign(options, {// 对options参数加强,加入一些其它参数,如:自定义指令,对服务端版本的处理
      // 基于组件微缓存
      // cache: LRU({ 
      //   max: 100, // 最大缓存个数100
      //   maxAge: 60 * 1000, // 淘汰时间1分钟
      // }),
      shouldPrefetch: (file, type) => false,
    })
  );
let renderer;
// ---------------------------------------------------------------------
const resolve = (file) => path.resolve(__dirname, file);
const serve = (path, cache) =>
  express.static(resolve(path), {
    maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0,
  });
app.use("/dist", serve("./dist", true));
app.use("/public", serve("./public", true));
// ---------------------------------------------------------------------
// 3. app:node server 的app信息
// templatePath: 页面模板
// cb: 回调函数
const serverReady = setUpDevServer(app, HTML_FILE, (bundle, options) => {
  // cb:回调函数里,给renderer通过createRenderer赋一下值
  renderer = createRenderer(bundle, options);
});
// ---------------------------------------------------------------------
const json = require("./mock.json");
app.get("/api/lists", function (req, res) {
  res.send(json);
});
// 5.---------------------------------------------------------------------
app.get("*", (req, res) => {
  serverReady.then((clientCompiler) => {
    const s = Date.now();
    // clientCompiler.outputFileSystem.readFile(HTML_FILE, (err, result) => {
    //   if (err) {
    //     return next(err);
    //   }
    //   res.set("content-type", "text/html");
    //   res.send(result);
    //   res.end();
    // });
    // 获取缓存(基于页面的微缓存)
    // const hit = microCache.get(req.url);
    // if (hit) {
    //   if (!isProd) {
    //     console.log(`whole request in: ${Date.now() - s}ms`);
    //   }
    //   // 使用缓存(基于页面的微缓存,完成)
    //   return res.end(hit);
    // }
    // 5. 生成一个字符串模板
    renderer.renderToString(
      {
        url: req.url, // 访问的页面
      },
      (err, html) => {
        if (err) { // 报错
          res.status(404).send("404 | Not Found");
        } else { // 正常
          // 缓存:key:req.url,value:html (基于页面微缓存)
          // microCache.set(req.url, html);
          res.send(html); // send html模板
          if (!isProd) { //缓存: 不是线上生产环境,完成整个请求***ms
            console.log(`whole request in: ${Date.now() - s}ms`);
          }
        }
      }
    );
    // const stream = renderer.renderToStream({
    //   url: req.url,
    // });
    // let html = "";
    // stream.on("data", (chunk) => {
    //   html += chunk.toString();
    //   res.write(chunk.toString());
    // });
    // stream.on("end", (chunk) => {
    //   microCache.set(req.url, html);
    //   res.end();
    //   if (!isProd) {
    //     console.log(`whole request in: ${Date.now() - s}ms`);
    //   }
    // });
    // stream.on("error", (err) => {
    //   if (err) {
    //     res.status(404).send("404 | Not Found");
    //   }
    // });
  });
});
let port = process.env.PORT || 9090;
app.listen(port, () => {
  console.log(`server started at localhost:${port}`);
});

src/module/topic/components/UItem.vue

<template>
  <a
    class="item"
    :href="article.originalUrl"
    :style="{ borderColor: theme.primary }"
  >
    <div class="title">{{ article.title }}</div>
    <div class="tags" v-if="hasTag">
      #
      <span class="tag" v-for="tag in node.item_info.tags" :key="tag.id">{{
        tag.tag_name
      }}</span>
    </div>
  </a>
</template>
<script>
export default {
  name: "u-item", // 基于组件微缓存,需要有一个name属性
  // 基于组件微缓存,服务端缓存key,根据key缓存组件模板,当用到同样组件id的时候就在缓存取出来
  serverCacheKey: (props) => props.node.id,
  inject: ["theme"],
  props: {
    node: {
      required: true,
    },
  },
  computed: {
    article() {
      return this.node.item_info.article_info;
    },
    hasTag() {
      return this.node.item_info.tags && this.node.item_info.tags.length > 0;
    },
  },
};
</script>
<style scoped>
.item {
  display: block;
  background: #fff;
  color: #333;
  padding: 20px 0px 20px;
  border-bottom: 1px solid #eee;
  position: relative;
}
.title {
  font-size: 16px;
  margin-bottom: 5px;
}
.tags {
  font-size: 12px;
  color: #999;
}
.tag {
  margin-right: 3px;
}
</style>

Stream【node.js中的Stream;可以通过stream渲染html】

为什么要用streams

  • 内存效率
    • 相比较一次性加载一大块内容,stream将内容切分为小块chunks,分批在内存中处理,
    • 避免高并发下,内存占用过大的情况发生。
  • 时间效率
    • 当处理完第一个chunk,即返回用户可视内容,能够减少用户访问页面的等待时间。 

 server.js

const fs = require("fs");
const path = require("path");
const express = require("express");
// 缓存:使用微缓存,要先安装个工具   npm i lru-cache
const LRU = require("lru-cache");
const setUpDevServer = require("./build/setup-dev-server");
const isProd = process.env.NODE_ENV === "production";
// 4.修改一下页面模板地址
const HTML_FILE = path.join(__dirname, "./src/index.template.html");
// 1. 安装个插件 npm i vue-server-renderer
const { createBundleRenderer } = require("vue-server-renderer");
const app = express();
// 缓存:页面级微缓存
const microCache = new LRU({
  max: 100,
  maxAge: 1000 * 60,
});
// ---------------------------------------------------------------------
// 2. 定义一个函数
// bundle :webpack打包输出的一个bundle
// options 参数传递
const createRenderer = (bundle, options) =>
  createBundleRenderer(
    bundle,
    Object.assign(options, {// 对options参数加强,加入一些其它参数,如:自定义指令,对服务端版本的处理
      // 基于组件微缓存
      // cache: LRU({ 
      //   max: 100, // 最大缓存个数100
      //   maxAge: 60 * 1000, // 淘汰时间1分钟
      // }),
      shouldPrefetch: (file, type) => false,
    })
  );
let renderer;
// ---------------------------------------------------------------------
const resolve = (file) => path.resolve(__dirname, file);
const serve = (path, cache) =>
  express.static(resolve(path), {
    maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0,
  });
app.use("/dist", serve("./dist", true));
app.use("/public", serve("./public", true));
// ---------------------------------------------------------------------
// 3. app:node server 的app信息
// templatePath: 页面模板
// cb: 回调函数
const serverReady = setUpDevServer(app, HTML_FILE, (bundle, options) => {
  // cb:回调函数里,给renderer通过createRenderer赋一下值
  renderer = createRenderer(bundle, options);
});
// ---------------------------------------------------------------------
const json = require("./mock.json");
app.get("/api/lists", function (req, res) {
  res.send(json);
});
// 5.---------------------------------------------------------------------
app.get("*", (req, res) => {
  serverReady.then((clientCompiler) => {
    const s = Date.now();
    // clientCompiler.outputFileSystem.readFile(HTML_FILE, (err, result) => {
    //   if (err) {
    //     return next(err);
    //   }
    //   res.set("content-type", "text/html");
    //   res.send(result);
    //   res.end();
    // });
    // 获取缓存(基于页面的微缓存)
    // const hit = microCache.get(req.url);
    // if (hit) {
    //   if (!isProd) {
    //     console.log(`whole request in: ${Date.now() - s}ms`);
    //   }
    //   // 使用缓存(基于页面的微缓存,完成)
    //   return res.end(hit);
    // }
    // 5. 生成一个字符串模板,整体页面渲染
    // renderer.renderToString(
    //   {
    //     url: req.url, // 访问的页面
    //   },
    //   (err, html) => {
    //     if (err) { // 报错
    //       res.status(404).send("404 | Not Found");
    //     } else { // 正常
    //       // 缓存:key:req.url,value:html (基于页面微缓存)
    //       // microCache.set(req.url, html);
    //       res.send(html); // send html模板
    //       if (!isProd) { //缓存: 不是线上生产环境,完成整个请求***ms
    //         console.log(`whole request in: ${Date.now() - s}ms`);
    //       }
    //     }
    //   }
    // );
    //================================================================================
    // stream 分块 渲染
    // stream 优化内存管理,第一时间触达我们的页面
    const stream = renderer.renderToStream({ // 定义一个stream流
      url: req.url,
    });
    // 缓存+stream结合使用
    let html = "";
    stream.on("data", (chunk) => { // 监听数据chunk信息
      // 拼接页面字符串
      html += chunk.toString();
      res.write(chunk.toString()); // 返回写入页面
    });
    stream.on("end", (chunk) => { // 表示页面都传输完毕了
      // 微缓存页面
      microCache.set(req.url, html);
      res.end();
      if (!isProd) { // 时间打点
        console.log(`whole request in: ${Date.now() - s}ms`);
      }
    });
    stream.on("error", (err) => { // 监听一下异常情况
      if (err) {
        res.status(404).send("404 | Not Found");
      }
    });
  });
});
let port = process.env.PORT || 9090;
app.listen(port, () => {
  console.log(`server started at localhost:${port}`);
});

Prerendering

如果只是要把工程中几个帮助页面做一个静态化,没有必要使用ssr,我们可以使用Prerendering

build/webpack.client.config.js

const webpack = require("webpack");
const merge = require("webpack-merge");
const base = require("./webpack.base.config");
const path = require("path");
// 预渲染Prerendering,先安个插件   npm i prerender-spa-plugin -D
const PrerenderSPAPlugin = require("prerender-spa-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
// 1.先安装个插件   npm i vue-server-renderer -D
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
const config = merge(base, {
  entry: {
    // 3.入口文件由app.js—改为—>entry-client.js
    app: "./src/entry-client.js",
  },
  resolve: {
    alias: {},
  },
  plugins: [
    // strip dev-only code in Vue source
    new webpack.DefinePlugin({
      "process.env.NODE_ENV": JSON.stringify(
        process.env.NODE_ENV || "development"
      ),
      "process.env.VUE_ENV": '"client"',
    }),
    // 2.加入到webpack插件里面去就可以了。
    new VueSSRClientPlugin(),
  ],
});
//===============================================================================
// client webpack【打包】 => html js css
// PrerenderSPAPlugin headless【无头浏览器】: chrome puperteer,对页面进行渲染,对这个页面做静态化输出
// 正式上线打包时候,Prerender做一下预渲染
if (process.env.NODE_ENV === "production") {
  config.plugins.push( // 生产环境加插件
    new HtmlWebpackPlugin({ // 使用静态页面的模板要用到的插件
      template: "src/prerender.template.html", // 模板路径
    }),
    new PrerenderSPAPlugin({
      staticDir: path.join(__dirname, "../dist"), // 定义静态文件输入到哪个文件下
      routes: ["/about"],// 对哪一个路由进行预渲染
    })
  );
}
module.exports = config;

src/prerender.template.html

<!DOCTYPE html>
<html lang="en">
<head>
  <title></title>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, minimal-ui" />
</head>
<body>
  <!-- 预渲染使用的模板 -->
  <div id="app"></div>
</body>
</html>

build/webpack.base.config.js

const path = require("path");
const webpack = require("webpack");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ManifestPlugin = require("webpack-manifest-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { VueLoaderPlugin } = require("vue-loader");
const isProd = process.env.NODE_ENV === "production";
module.exports = {
  mode: isProd ? "production" : "development",
  output: {
    path: path.resolve(__dirname, "../dist"),
    // 如果npm run build:client 打包出的静态页面没有内容 => Prerendering预渲染时修改为"./"
    publicPath: "/dist/", 
    filename: isProd ? "[name].js" : "[name].[chunkhash].js",
  },
  resolve: {
    alias: {
      public: path.resolve(__dirname, "../public"),
    },
  },
  module: {
    noParse: /es6-promise\.js$/, // avoid webpack shimming process
    rules: [
      {
        test: /\.vue$/,
        loader: "vue-loader",
        options: {
          compilerOptions: {
            preserveWhitespace: false,
          },
        },
      },
      {
        test: /\.js$/,
        loader: "babel-loader",
        exclude: /node_modules/,
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: "url-loader",
        options: {
          limit: 10000,
          name: "[name].[ext]?[hash]",
        },
      },
      {
        test: /\.css$/,
        use: isProd ? ["css-loader"] : ["vue-style-loader", "css-loader"],
      },
    ],
  },
  plugins: isProd
    ? [new VueLoaderPlugin()]
    : [
      new VueLoaderPlugin(),
      new HtmlWebpackPlugin({
        template: "src/index.template.html",
      }),
    ],
  optimization: {
    splitChunks: {
      chunks: "all",
    },
  },
  devtool: isProd ? "" : "inline-source-map",
};

一张ai图片~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值