工程化下的SSR初探-降级渲染

该文章阅读需要 7 分钟,更多文章请点击本人博客halu886

  • 概念
  • 思路
    • 集成 Router 和 Store
    • 抽象逻辑层
    • 客户端激活
  • 踩坑
  • 总结
概念

在续上篇 ssr 骨架搭建之后,服务端渲染生成的 HTML 代码直接渲染在浏览器客户端上,可以大大减少 TTC(time-to-content)。

但是在现在前端 MVVM 的框架中,例如 VUE,React,都是在单页面中采用动态虚拟 DOM 的思路进行实现页面的交互和组件的更新。

如果采用服务端渲染的话,节点都是直接基于 HTML 中的代码片段直接生成的。 MVVM 中的 V(视图模型)这一步直接都省略了,以及相关绑定器也没有实例化不会被绑定。

那么对于现代的前端的框架的支持太不友好了。

所以降级渲染这个概念也就诞生了,所谓的降级渲染通俗理解则是一套代码基于 SSR 渲染后,在客户端后降级为 CSR(客户端渲染)

这样就能同时享受到两个渲染方式带来到便利和优势。

思路

集成 Router 和 Store

我们先将代码的路由和数据状态分别托管到 Router 和 Store 组件中,将项目逻辑细化提升可维护性和减少代码量,

并且基于 Router 对事件触发数据的更新。同时只用 Store 对接存在差异的 Api 层,让组件对数据的处理无感知。

// store/index.js
import * as api from "../api";

Vue.use(Vuex);

export default () => {
  return new Vuex.Store({
    state: {
      recommend: [],
      top: [],
    },
    mutations: {
      updateRecommend(state, recommend) {
        /**/
      },
      updateTop(state, top) {
        /**/
      },
    },
    actions: {
      async updateTop({ commit }, context) {
        // 调用API封装层
        let tops = await api.fetchTop(context);
        /**/
        commit("updateTop", tops);
      },
      async updateRecommend({ commit }, context) {
        // 调用API封装层
        let recommends = await api.fetchRecommder(context);
        /**/
        commit("updateRecommend", recommends);
      },
    },
  });
};

// roter/index.js
import main from "../App.vue";
export default () => {
  return new VueRouter({
    routes: [
      {
        path: "/",
        component: main,
        children: [
          {
            path: "top",
            /* 动态加载组件减少初始化依赖包所需要的大小 */
            component: () => import("../components/mainHeader/index.vue"),
          },
          {
            path: "bottom",
            /* 动态加载组件减少初始化依赖包所需要的大小 */
            component: () => import("../components/mainFooter/index.vue"),
          },
        ],
      },
    ],
  });
};

抽象逻辑层

为了减少对于相关服务端或者客户端对于数据拉取的重复代码,我们在每个 Vue 组件中封装通用的方法asyncData进行数据处理。

// App.vue
export default {
  /* 其他属性 */
  computed: { ...mapState(["recommend"]) },
  asyncData(store, router, context) {
    /* 触发store更新 */
    return store.dispatch("updateRecommend", context);
  },
  /* 其他属性 */
};

在 router 匹配时触发该方法进行数据获取挂载在 Store 上,然后在服务端和客户端路由变化时触发。

// web/index.js  服务端打包入口文件
export default (context) => {
  return new Promise((resolve, reject) => {
    const appInit = new Vue({
      /* 初始化相关设置(router/store/render) */
    });
    router.push(context.path); // 通过将上下文的路由手动推入router中
    router.onReady(() => {
      // 当router准备完毕后进行数据加载
      const routeComponents = router.getMatchedComponents();
      Promise.all(
        routeComponents
          .map(({ asyncData }) => {
            // 调用每个组件手工对外开发的asyncData接口
            asyncData && asyncData(store, router, context);
          })
          .filter((_) => _)
      )
        .then(() => {
          /* 部分业务处理 */
          resolve(appInit);
        })
        .catch((e) => {
          reject(e);
        });
    });
  });
};

我们将所有数据逻辑统一管理在 Store 中,其中也负责统一对 Api 进行调用

由于在服务端渲染相关数据处理封装在 Service 层,客户端相关数据获取通过 HTTP 请求获取,所以这里分别封装service-apiclient-api开放标准接口,在 webpack 打包中使用alias特性将对于 API 逻辑打包进对应文件中。

// client-api
export default function () {
  return {
    fetchTop: async () => {
      return (await axios.get("/get/top")).data;
    },
    fetchRecommder: async () => {
      return (await axios.get("/get/recommender")).data;
    },
  };
}

// server-api
export default function (ctx) {
  return {
    fetchTop: ctx.service.header.getTop,
    fetchBottom: ctx.service.bottom.getBottom,
  };
}

// api
import api from "api"; // 通过分别在Server和Client Webpack打包配置中Alias属性配置对应api文件

export async function fetchTop(context) {
  return await api(context).fetchTop(); //服务端渲染时传入当前请求上下文
}

export async function fetchRecommder(context) {
  return await api(context).fetchRecommder(); //服务端渲染时传入当前请求上下文
}

客户端激活

使用 Webpack 将源码打包后,在生成 HTML 片段时,以参数传入后,将会以预加载。

同时在 Router 的onReady事件后,使用实例化后对 Vue 对象进行$mount 挂载。

在服务端渲染出的 DOM 根节点上,自动添加了**data-server-rendered="true"**属性,与此同时在客户端激活时,Vue 会识别该属性,进行自上而下顺序的匹配 Visual Dom Tree,当无法匹配时,退出混合模式,重新渲染,并且抛出 warm。生产环境跳过检查,直接渲染

服务端渲染时,会将 Store 属性挂载上 Windows 的__INITIAL_STATE__

// web/client-index.js
const clientApp = new Vue({
  render: (h) => h("div", [h("router-view")]),
  /**传递相关属性 (store/router)**/
});

if (window.__INITIAL_STATE__) {
  /** 将服务端挂载的相关属性挂载到Store对象上,避免重新加载 **/
  store.replaceState(window.__INITIAL_STATE__);
}

// 这里假定 App.vue 模板中根元素具有 `id="app"`
router.onReady(() => {
  router.beforeResolve((to, from, next) => {
    const components = router.getMatchedComponents(to);
    if (!components.length) {
      next();
    }

    Promise.all(components.map((c) => c && c.asyncData(store, router, {})))
      .then(next)
      .catch(next);
  });
  clientApp.$mount("#app");
});
踩坑

在 Egg 中当进行 SSR 渲染时,相关业务数据的 fetch 为了兼容同构,另外封装在 Server-api 层,在 Vue 根据路由生成 HTML 时进行调用。

但是相关业务层又封装在 service 层,在请求访问时,挂载在当前请求的上下文中,造成了页面生成与请求上下文强耦合。

// plugin:egg-view-vue-tuji/lib/vuew.js

/* 将this.ctx(当前请求上下文)传入渲染上下文 */
renderer.renderToString(this.ctx, (err, html) => {
  if (err) {
    reject(err);
  }
  resolve(html);
});
总结

基于 Egg 进行 Vue SSR 的降级渲染主要就是以上的思路,这样既保留了 SSR 的优势,同时也能兼顾单页面下 MVVM 框架所带来的优势,同时在业务开发的过程对开发人员也可以是无感知。

拉取数据主要通过 Router 的 onReady 事件触发,每个的组件的数据相关的操作都封装在asyncData 方法中。API 层通过 Webpack 的alias属性区分打包。

在 Egg 中将请求上下文传入打包文件中调用 ctx.Service 方法。

服务端会将 Store 中的状态挂载到客户端 Window 对象上的__INITIAL_STATE__上,可以通过store.replaceState植入客户端中的 Store 中从而减少消耗。

原文链接:juejin.im

上一篇:$route和$router是什么?区别是什么?
下一篇:Vue 3.0 带着 Composition API 来了

相关推荐

  • (前端工程化01)私人管家-包管理器

    字数:3883, 阅读时间:10分钟,点击阅读原文 目录: 磨刀篇-开发环境搭建 私人管家-包管理器 待续 包管理器 在很久很久以前,那时候的前端被大家”亲切“的称为“切图仔”,那时前...

    6 个月前
  • (前端工程化01)私人管家-包管理器

    字数:3883, 阅读时间:10分钟,点击阅读原文 目录: 磨刀篇-开发环境搭建 私人管家-包管理器 待续 包管理器 在很久很久以前,那时候的前端被大家”亲切“的称为“切图仔”,那时前...

    6 个月前
  • (前端工程化01)私人管家-包管理器

    字数:3883, 阅读时间:10分钟,点击阅读原文 包管理器 在很久很久以前,那时候的前端被大家”亲切“的称为“切图仔”,那时前端的工作非常简单,仅仅只是将设计图还原,然后加上一些交互和...

    6 个月前
  • 针对搜索引擎爬虫的欺骗式ssr

    玩Google Webmasters的可能会有这种经历。自己开发的app用了Vue/React,写完后用Fetch as Google一爬傻眼了,爬不到东西。 网上搜解决方案出来的都是一堆额外的SS...

    3 年前
  • 针对搜索引擎爬虫的欺骗式SSR

    玩Google Webmasters的可能会有这种经历。自己开发的app用了Vue/React,写完后用Fetch as Google一爬傻眼了,爬不到东西。 网上搜解决方案出来的都是一堆额外的SS...

    3 年前
  • 重新认识prettier及如何工程化

    背景 对前端代码进行格式化时大多数同学都用到过prettier,例如在vscode中安装prettier插件,即可格式化任意文件,或者只格式化文件的选中部分。 prettier起到的作用是按照统一...

    1 年前
  • 这是一个基于 Vue SSR Genesis 框架快速开发的模板例子

    介绍 这是一个基于 Vue SSR Genesis 框架快速开发的模板例子 启动 # 安装依赖 npm install # 开发 npm run dev # 编译 npm run build # ...

    9 个月前
  • 跟著小明一起搞懂技術名詞:MVC、SPA 與 SSR

    這篇的靈感來自於 Front-End Developers Taiwan 裡面的一串討論,有人 po 了一個影片是來討論「MVC vs SPA」,這個標題一出來大家都驚呆了,想說怎麼會有這樣的比較,...

    2 年前
  • 超简单的react服务器渲染(ssr)入坑指南

    前言 本文是基于react ssr的入门教程,在实际项目中使用还需要做更多的配置和优化,比较适合第一次尝试react ssr的小伙伴们。技术涉及到 koa2 + react,案例使用create-re...

    2 年前
  • 谈谈前端工程化 js加载

    当年的 js 加载 在没有 前端工程化之前,基本上是我们是代码一把梭,把所需要的库和自己的代码堆砌在一起,然后自上往下的引用就可以了。 那个时代我们没有公用的cdn,也没有什么特别好的方法来优化加载j...

    2 年前

官方社区

扫码加入 JavaScript 社区