preface

First of all, welcome everyone to pay attention to my Github blog, which can also be regarded as a little encouragement to me. After all, there is no way to make money by writing things, and persistence also depends on my enthusiasm and everyone’s encouragement. Readers’ Star is my motivation to move forward, please don’t be stingy with it.

Vue isomorphism series has been published in the third article, the first two articles Vue isomorphism (a): The first two articles are essentially about how to use Vue and Vue Router in server rendering. The basic Vue bucket is not covered except for Vuex. This article also focuses on this topic.

primers

Always agree with Redux author Dan Abramov:

Flux architectures are like glasses: you know when you need them.

There’s a little bit of a “can’t say it” vibe, so let’s take a look at when we need to introduce Vuex in server-side rendering.

The examples in the previous two articles are simple enough, but the actual business scenario is not so simple. For example, if we want to render a list of articles, we definitely need to request data from the data source. In client rendering, this is all too common. Mounted () {/ / mounted () {/ / Vue () {/ / Vue () {/ / Vue () {/ / Vue () {/ / Vue () {/ / Vue () {/ / Vue () {/ / Vue () {/ / Vue ();

<template> // ...... </template> <script> export default {data: function(){return {items: []}}, mounted: Function (){fetchAPI().then(data => {// assign this.items = data.items; }) } } </script>Copy the code

In the mounted instance, the function beforeCreate and created will only be executed for the restored state on the server. Is it possible to place the logic of the data request between these two life cycles? The answer is no, because the operation of the data request is asynchronous and we cannot predict when the data will be returned. We also need to consider that not only does the server need the data to render the interface, but the client also needs the data of the first screen, because the client needs to activate it. Do we need to request the same data twice on the server and the server? Then both the server and the data source will experience a sudden increase in stress, which is certainly not desirable.

The solution is fairly straightforward: separate the data from the components, and we prepare the data and place it in the container before the server renders the components, so that the server can take the data directly from the container during rendering. Not only that, but we can use the data in a container directly serialization, injection to the request in the HTML, so that the client activation component, also can directly to get the same data rendering, not only can reduce the request of the same data and can also prevent because of the request data caused by different activation failure so the client to render (development mode, Not detected in production mode, activation will fail). So who is going to be the data container, which is obviously Vuex.

Server data prefetch

We went on to build configuration in the previous article code based on the start we try (link) there will be at the end of the code, first of all we say our goals, we borrow the CNode articles provide interface, then apply colours to a drawing gives in the interface of the list of articles under different labels, switching between different routing label can load a different list of articles. We use AXIOS as the common HTTP request library for Node servers and browser clients. Write interfaces first. CNode provides the following interfaces:

The GET URL: cnodejs.org/api/v1/topi…

Parameter: Page Number Number of pages Parameter: TAB Topic category. Limit Number Specifies the Number of topics on a page

We will choose three TAB themes this time, which are good, Share and Ask.

First provide interfaces to components:

// api/index.js
import axios from "axios";

export function fetchList(tab = "good") {
    const url = `https://cnodejs.org/api/v1/topics?limit=20&tab=${tab}`;
    return axios.get(url).then((data) = >{
        returndata.data; })}Copy the code

For demonstration purposes we only render the first 20 pieces of data.

Next, we introduce Vuex. As mentioned in the previous two articles, we need to generate new Vue and Vue Router instances for each request. The fundamental reason is to prevent state contamination caused by data sharing between different requests. For the same reason, we need to generate a new instance of Vuex for each request.

import Vue from 'vue'
import Vuex from 'vuex'

import { fetchList } from '.. /api'

Vue.use(Vuex)

export function createStore() {
    return new Vuex.Store({
        state: {
            good: [].ask: [].share: []},actions: {
            fetchItems: function ({commit}, key = "good") {
                return fetchList(key).then( res= > {
                    if(res.success){
                        commit('addItems', {
                            key,
                            items: res.data
                        })
                    }
                })
            }
        },

        mutations: {
            addItems: function (state, payload) {
                const{key, items} = payload; state[key].push(... items); }}})}Copy the code

We assume you already know something about Vuex. First we call Vue.use(Vuex) to inject Vuex into Vue, and then each call to createStore returns a new instance of Vuex. State contains good, Ask and Share arrays to store article information of corresponding topics. The mutation named addItems is responsible for adding data to the corresponding array in state, while the action named fetchItems is responsible for calling the asynchronous interface to request the data and update the corresponding mutation.

When we call fetchItems needs to be considered. Specific routes correspond to specific components, and specific components require specific data for rendering. We said that the implementation of the logic is in front of the component rendering is access to the data used in the, in a pure client-side rendering program logic we will request placed in corresponding to the life cycle of the component in the service side rendering, we still place the logic within the component, in this way, not only on the server side rendering time by matching components can perform the request data logic, After the client is activated, the logic inside the component can also be executed to request or update data at the necessary time. Let’s look at examples:

// TopicList.vue <template> <div> <div v-for="item in items"> <span>{{ item.title }}</span> <button @click="openTopic(item.id)"> open </button> </div> </div> </template> "topic-list", asyncData: Function ({store, route}) { If (store.state[route.params.id].length <=0){return store.dispatch("fetchItems", route.params.id) }else { return Promise.resolve() } }, computed: { items: function () { return this.$store.state[this.$route.params.id]; } }, methods: { openTopic: function (id) { window.open(`https://cnodejs.org/topic/${id}`) } } } </script> <style scoped> </style>Copy the code

The template of the Vue component needs no explanation. The main reason for adding a button to open the link to the corresponding article is to verify that the client is properly activated. The component gets the data from store, where the ID of route represents the topic of the article. The most unusual thing about this component is that we expose a custom static function asyncData. Because it is a static function of the component, we can call the method before the component has even created an instance, but because the instance has not been created, we cannot access this inside the function. The internal logic of asyncData is the action that triggers fetchItems in the store.

Let’s look at the configuration of the route:

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export function createRouter() {
    return new Router({
        mode: "history".routes: [{
            path: '/good'.component: (a)= > import('.. /components/TopicListCopy.vue')
        },{
            path: '/:id'.component: (a)= > import('.. /components/TopicList.vue')})}}]Copy the code

We configured a special TopicListCopy component for the good route, which is the same as TopicList except for its name. For the rest of the route, we use the TopicList component as described earlier, mainly for convenience.

Then let’s look at app.js:

import Vue from 'vue'

import { createStore } from './store'
import { createRouter } from './router'

import App from './components/App.vue'

export function createApp() {

    const store = createStore()
    const router = createRouter()
    
    const app =  new Vue({
        store,
        router,
        render: h= > h(App)
    })

    return {
        app,
        store,
        router
    }
}
Copy the code

This is basically the same code as before, except that each time the createApp function is called, an instance store of Vuex is created and a Store instance is injected into the Vue instance.

Let’s look at the server render entry entry-server.js:

// entry-server.js
import { createApp } from './app'

export default function (context) {
    return new Promise((resolve, reject) = > {
        const {app, store, router} = createApp()
        router.push(context.url)
        router.onReady((a)= > {
            const matchedComponents = router.getMatchedComponents()
            if(matchedComponents.length <= 0) {return reject({ code: 404})}else {
                Promise.all(matchedComponents.map((component) = > {
                    if(component.asyncData){
                    
                        return component.asyncData({
                            store,
                            route: router.currentRoute
                        })
                    }
                })).then((a)= > {
                    context.state = store.state
                    resolve(app)
                })
            }
        }, reject)
    })
}
Copy the code

The server render entry file is basically the same structure as before, and onReady will execute the passed callback function after all the asynchronous hook functions and asynchronous components are loaded. In the previous article, resolve(app) was executed directly in the onReady callback to pass the corresponding component instance. But here we did some other work. We call the first router. GetMatchedComponents () to obtain the current routing routing component matching, note we match the routing component not only configuration object instance, then we call all matching routing in the component asyncData static method, Load the data required by each routing component, and when all routing component data is loaded, assign the state value in the current store to context.state and resolve the component instance. Note that the store contains all the data needed for the first rendering component, and we assign the value to context.state. If the renderer is using template, The state is serialized and stored on window.__initial_state__ by injecting HTML.

Next, let’s look at the browser render entry entry-client.js:

//entry-client.js
import { createApp } from './app'

const {app, store, router} = createApp();


if (window.__INITIAL_STATE__) {
    store.replaceState(window.__INITIAL_STATE__)
}

router.onReady((a)= > {
    app.$mount('#app')})Copy the code

The logic for the browser activation is similar to that in the previous article, except that we call replaceState at the beginning to replace the state state in the store with window.__initial_state__ so that the client can use this data activation directly to avoid a second request.

The server.js code for the server remains the same with no other changes compared to the code in the previous article. Now let’s wrap up and see what our program looks like:

We found that the server took the data and rendered the list of articles and clicked the button on the right to open the link to the article, indicating that the client had been properly activated. However, when we switch between different routes, we find that other topics are not loaded. This is because we only write the data fetching in the server rendering, and the data loading corresponding to different route switching in the client side should be requested independently by the client side. So we need to add this part of logic.

When should the client call the asyncData function when the data request logic is preset in the component’s static function asyncData?

Client request

The official documentation gives two ideas. One is to parse the data before the route navigates. One is to request data after the view has been rendered

Request before render

To achieve this logic, we use the beforeResolve parsing guard in Vue Router. After all the guards and asynchronous routing components are parsed, the beforeResolve parsing guard is called. Let’s revamp the client render entry logic:

import { createApp } from './app'

const {app, store, router} = createApp();

if (window.__INITIAL_STATE__) {
    store.replaceState(window.__INITIAL_STATE__)
}

router.onReady((a)= > {
    router.beforeResolve((to, from, next) = > {
        const matched = router.getMatchedComponents(to)
        const prevMatched = router.getMatchedComponents(from)
        // We only care about non-prerendered components
        // So we compare them to find the different components of the two matching lists
        let diffed = false
        const activated = matched.filter((c, i) = > {
            returndiffed || (diffed = (prevMatched[i] ! == c)) })if(! activated.length) {return next()
        }
        // If there is a loading indicator, it will trigger
        Promise.all(activated.map(c= > {
            if (c.asyncData) {
                return c.asyncData({ store, route: to })
            }
        })).then((a)= > {
            // Stop loading indicator
            next()
        }).catch(next)
    })
    app.$mount('#app')})Copy the code

The code logic in the above beforeResolve firstly compares the matching routing components of to and FROM routes, then finds out the difference components of the two matching lists, and then calls asyncData in all the difference components to obtain data. After all the data is obtained, calls next to continue the execution.

At this time, we packaged and ran the program. We found that switching good to Ask or share could load data, but switching ask and share could not load data, as shown below:

Why is that? Remember that we set TopicListCpoy routing components for good routes and TopicList routing components for Share and Ask routes, so there is no difference component in the switchover between Share and Ask, but the routing parameters are changed. To solve this problem, we added a component inside guard to solve this problem:

beforeRouteUpdate: function (to, from, next) {
    this.$options.asyncData({
        store: this.$store,
        route: to
    });
    next()
}
Copy the code

The component guard beforeRouteUpdate is called when the current route has changed but is still being reused by the component, such as when dynamic parameters change. At this point we execute the logic to load the data and the problem will be solved. In using the prefetch data first, then load components there is a problem to see is feeling, will feel obviously card, because you can’t keep data when can request over, if the request data cannot render time is too long and cause components, the user experience will be discounted, it is recommended that in the process of loading to provide a uniform load indicator, To minimize the loss of interactive experience.

Render first, request later

The logic of rendering components first and then requesting data is similar to that of pure client rendering. We place the logic of prefetching data in the beforeMount or Mounted life cycle functions of components. After the route switch, components will be rendered immediately, but there will be no complete data when rendering components. Therefore, the component itself needs to provide the corresponding load state. The logic for data prefetching can be invoked individually in each routing component, or it can be implemented globally by means of vue.mixin:

Vue.mixin({
    beforeMount () {
        const { asyncData } = this.$options
        if (asyncData) {
            asyncData({
                store: this.$store,
                route: this.$route
            })
        }
    }
})
Copy the code

Of course, there is also the case of route switching but component reuse as we mentioned before, so it is not enough to just fetch data in the beforeMount operation. We should also request data if route parameters change but component reuse. This problem can still be handled by the component guard beforeRouteUpdate.

So far we have covered how to handle data and preview in server rendering. if you need to see the source code, please go here. Feel free to point out any incorrectness, and I encourage you to follow my Github blog and the next series of posts.