This article takes 7 minutes to read. For more articles, please visit my bloghalu886

  • concept
  • Train of thought
    • Integrate Router and Store
    • Abstract logic layer
    • Client Activation
  • Hit the pit
  • conclusion

concept

After the SSR skeleton construction in the previous part, the HTML code generated by server rendering is directly rendered on the browser client, which can greatly reduce the TTC(time-to-content).

However, in the current front-end MVVM framework, such as VUE and React, the idea of dynamic virtual DOM is adopted to realize page interaction and component update in a single page.

With server-side rendering, nodes are generated directly from snippets of CODE in HTML. The V(viewmodel) step in MVVM is omitted directly, and the associated binders are not instantiated and will not be bound.

Then the support for modern front-end frameworks is too unfriendly.

So the concept of degraded rendering was born. Degraded rendering is generally understood as a set of code based on SSR rendering and degraded to CSR (Client rendering) after the client.

This allows you to enjoy the convenience and advantages of both rendering methods.

Train of thought

Integrate Router and Store

We first hosted the routing and data state of the code in Router and Store components respectively to refine the project logic to improve maintainability and reduce the amount of code.

And update the event data based on the Router. At the same time, only Store connects to the Api layer with differences, so that components have no perception of data processing.

// 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) {
        // Call the API encapsulation layer
        let tops = await api.fetchTop(context);
        / * * /
        commit("updateTop", tops);
      },
      async updateRecommend({ commit }, context) {
        // Call the API encapsulation layer
        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"./* Dynamically loading components reduces the size required to initialize dependent packages */
            component: (a)= > import(".. /components/mainHeader/index.vue"),}, {path: "bottom"./* Dynamically loading components reduces the size required to initialize dependent packages */
            component: (a)= > import(".. /components/mainFooter/index.vue"),},],},],}); };Copy the code

Abstract logic layer

In order to reduce the repetitive code for the related server or client to pull data, we encapsulate the general method asyncData in each Vue component for data processing.

// App.vue
export default {
  /* Other attributes */computed: { ... mapState(["recommend"]) },
  asyncData(store, router, context) {
    /* Trigger store update */
    return store.dispatch("updateRecommend", context);
  },
  /* Other attributes */
};
Copy the code

This method is fired when the router matches for data retrieval and is mounted on the Store, and then fired when the server and client routes change.

// web/index.js server package entry file
export default (context) => {
  return new Promise((resolve, reject) = > {
    const appInit = new Vue({
      /* Router /store/render */
    });
    router.push(context.path); // Manually push the context route into the router
    router.onReady((a)= > {
      // Load the data when the router is ready
      const routeComponents = router.getMatchedComponents();
      Promise.all(
        routeComponents
          .map(({ asyncData }) = > {
            // Invoke the manually developed asyncData interface for each component
            asyncData && asyncData(store, router, context);
          })
          .filter((_) = > _)
      )
        .then((a)= > {
          /* Some business processing */
          resolve(appInit);
        })
        .catch((e) = > {
          reject(e);
        });
    });
  });
};
Copy the code

All of our data logic is managed in the Store, which is also responsible for making calls to the Api

Because relevant data processing for rendering on the server side is encapsulated in the Service layer, and relevant data on the client side is obtained through HTTP request, service-API and client-API open standard interfaces are respectively encapsulated here. Use the Alias feature in the Webpack package to package the API logic into the corresponding file.

// 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"; // Configure the corresponding API file through the Alias property in the Server and Client Webpack configuration respectively

export async function fetchTop(context) {
  return await api(context).fetchTop(); // Server rendering passes in the current request context
}

export async function fetchRecommder(context) {
  return await api(context).fetchRecommder(); // Server rendering passes in the current request context
}
Copy the code

Client Activation

When the source code is packaged with Webpack, it will be preloaded as arguments are passed in when the HTML fragment is generated.

After the onReady event on the Router, mount the Vue object with $mount after instantiation.

Rendered DOM root nodes automatically have a ** data-server-Rendered =”true” attribute added. Vue will recognize this attribute when the client is activated, and rendered top-down to the Visual DOM Tree. Exit blend mode, re-render, and throw warm. Production skips checking and renders directly

When rendered by the server, the Store property is mounted to Windows’ __INITIAL_STATE__

// web/client-index.js
const clientApp = new Vue({
  render: (h) = > h("div", [h("router-view")),/** Pass related attributes (store/router)**/
});

if (window.__INITIAL_STATE__) {
  /** Mount the server-mounted properties to the Store object to avoid reloading **/
  store.replaceState(window.__INITIAL_STATE__);
}

// This assumes that the root element in the app. vue template has' id=" App "'
router.onReady((a)= > {
  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");
});
Copy the code

Hit the pit

When SSR rendering is carried out in Egg, fetch of relevant business data is encapsulated in server-API layer for compatibility and isomer, and is called when Vue generates HTML according to route.

However, the relevant business layer is encapsulated in the Service layer, which is mounted in the context of the current request when the request is accessed, resulting in strong coupling between page generation and the request context.

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

/* Pass this.ctx (the current request context) into the render context */
renderer.renderToString(this.ctx, (err, html) => {
  if (err) {
    reject(err);
  }
  resolve(html);
});
Copy the code

conclusion

The degraded rendering of Vue SSR based on Egg is mainly based on the above ideas, which not only retains the advantages of SSR, but also takes into account the advantages brought by the MVVM framework under a single page. At the same time, developers can be unaware in the process of business development.

Pull data is triggered by the Router’s onReady event, and the data related operations of each component are encapsulated in asyncData methods. The API layer distinguishes packaging through Webpack’s Alias attribute.

Call the ctx.service method by passing the request context into the package file in the Egg.

The server will mount the state in Store to __INITIAL_STATE__ on the client Window object, which can be reduced by embedding Store. ReplaceState in the client Store.