What is Server-side rendering (SSR)?

Vue.js is a framework for building client applications. By default, the Vue component can be exported to the browser for DOM generation and DOM manipulation. However, it is also possible to render the same component as HTML strings on the server side, send them directly to the browser, and finally “activate” these static tags into a fully interactive application on the client side.

Vue. Js server side rendering guide, generally speaking, the general process is as follows:

User request –> server parses the route to find the corresponding component –> render an HTML string with Renderer and send it to the client –> client parses the HTML and loads the few necessary JS files –> execute JS for page activation. Client-side activation is essentially the process of mounting a DOM node and taking over the HTML returned by the server.

This is significantly less loaded than a typical SPA (since it’s all on-demand rendering, it doesn’t matter if it’s lazy loading) and more targeted. So pages are usually displayed faster. Because the returned content contains pre-rendered data, it is relatively SEO friendly.

The essence of server-side rendering is to run the Vue and its libraries on the server to create a “snapshot” of the application. But it should be made clear that SSR only renders the first screen under the corresponding route, and other pages still need to be rendered by the client.

Principle of building

According to the above description, we have the following picture

As you can see, the source code has two entries, a server-entry and a client-entry. Each bundle is generated. The server executes the server-bundle to create the Renderer, and after receiving the request and rendering the corresponding first screen page, it returns the rendering result in the form of AN HTML string, along with the remaining routing information to the client to render other routing pages.

So, how does the routing and state information on the server get synchronized to the client?

Source code Structure (Demo)

Build ├ ─ ─ base. Js ├ ─ ─ client. Config. Js └ ─ ─ for server config. Js SRC ├ ─ ─ components │ └ ─ ─ the HelloWorld. Vue ├ ─ ─ App. Vue ├ ─ ─ App.js // Common entry, ├── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ├─ entry-client.js // Only run in browser, ├─ entry-server.js // Only run in server, only run in serverCopy the code

In a pure client application, each user uses a new application instance (new Vue()) in their respective browsers. The same is true for server-side rendering. But the server is a long-running process, and to avoid sharing the same state with each request, we need to create a new root Vue instance for each request, rather than globally sharing a singleton. So you need to expose a factory function that can be executed repeatedly, creating a new application instance for each request.

/** * app.js exports an instance factory function shared by client-entry and server-entry */
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'

export function createApp () {
    const router = createRouter()
    const store = createStore()

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

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

Referring to the same createApp, the instance is actually created exactly the same, so the routing information and the state information are the same in the initial state. However, after the introduction of asyncData, if the server obtains and sets state in advance, the data of the client and the server will be different. At this time, another way is needed to synchronize state, that is, state is serialized as a string, enlivened in HTML, received and replaced by the client as the initial state. The synchronization is complete. These will be mentioned later.

  • server-entryResponsible for creating and returning vm instances. But there are synchronous and asynchronous cases
import { createApp } from './app'
// Synchronize the situation
export default context => {
    const { app } = createApp()
    return app
}
// If there are complex asynchronous operations, such as data prefetch or route matching, return Promise and resolve(app)
// So that the server can wait for everything to be ready before rendering.
export default context => {
    const { app } = createApp()
    return new Promise(async (resolve, reject) => {
        try {
            awaitAsync resolve(app)}catch(e){ reject(e) }
    })
}
Copy the code
  • client-entryResponsible for creating instances and mounting them into the corresponding DOM to activate the application
import { createApp } from './app'
// Client-specific boot logic......
const { app, router } = createApp()

router.onReady(() = > {
    // This assumes that the root element in the app. vue template has' id=" App "'
    app.$mount('#app')})Copy the code

Bundle Renderer

Vue-server-renderer provides a core API called createBundleRenderer and two core plug-ins

import { createBundleRenderer } from 'vue-server-renderer'
vue-server-renderer/server-plugin // Referenced in server.config.js
vue-server-renderer/client-plugin // referenced in client.config.js
Copy the code

By using plug-ins, The server-bundle and client-bundle will generate special JSON files vue-ssR-server-bundle. JSON and vue-SSR-client-manifest.json that can be passed to the BundleRenderer. This special JSON helps us automatically introduce dependencies, and has many advantages:

  • Provides built-in source-map support
  • Support for hot overloading (by reading the updated bundle and then re-creating the renderer instance without recompiling the bundle.js file)

See BundlerRenderer

At build time, the Server-bundle and client-bundle are generated from the two entry files and passed into the Renderer instance, which combines the two. After rendering the final HTML text, it is returned to the browser, which executes JS code to activate the Vue instance.

The problem

How to get the data ahead of time and render the corresponding HTML?

One way to increase in the component options, such as asyncData, vue – the router after the success of the jump, obtain a list of current active component router. GetMatchedComponents (), in turn, the calling component of asyncData method, and wait for all the request is completed.

Because of the asynchronous operation, the server-bundle needs to export a function that returns a Promise to work with the Vue-server-Renderer plug-in.

So the server-Entry code looks like this:

/** * server-entry.js parses routes and preloads data */
import { createApp } from './app'


export default context => {
    return new Promise((resolve, reject) = > {
        const { app, router, store } = createApp()

        router.push(context.url)
        router.onReady(() = > {
            const matchedComponents = router.getMatchedComponents()
            if(! matchedComponents.length) {return reject({ code: 404})}Promise.all(matchedComponents.map(item= > {
                const { asyncData } = item
                if (asyncData) {
                    return asyncData.call(item, {
                        store,
                        route: router.currentRoute
                    })
                } else return Promise.resolve()
            })).then(() = > {
                // Append state to context
                context.state = store.state
                resolve(app)
            })
        }, reject)
    })
}
Copy the code

This can be used in components

export default {
    asyncData ({ store, route }) {
        // When the action is triggered, a Promise is returned
        return store.dispatch('fetchItem'.'HelloWorld')}},Copy the code

The relevant code in store is as follows:

actions: {
    fetchItem ({ commit }, title) {
        return new Promise((resolve, reject) = > {
            // Simulate an asynchronous operation and return a Promise
            setTimeout(() = > {
                commit('setItem', title)
                resolve()
            }, 1000)})}},Copy the code

How to synchronize data in Vuex

The server waits for these asynchronous operations to end (if any) and renders the final HTML with the latest data before returning to the pre-render interface. In order for the client to synchronize the existing data in Vuex, when we attach the state to the context and the Template option is used for the renderer, the state is automatically serialized to window.INITIAL_STATE and injected into HTML. At the same time, the client also needs to use this value as the initial state of the store to complete the synchronization. The client code looks like this:

/** * entry-client.js The client entry file is mainly used to mount instances and synchronize state */

import { createApp } from './app'
const { app, router, store } = createApp()

// Prevent asynchronous data or other asynchronous operations from causing server and client rendered structures to be inconsistent
router.onReady(() = > {
    if (window.hasOwnProperty('__INITIAL_STATE__')) {
        // Initialize the replacement Store state
        store.replaceState(window.__INITIAL_STATE__)
    }
    app.$mount('#app')})Copy the code

You can see that the state exists in the HTML in this way.

Nuxt profile

Here’s one that feels pretty good — Nuxt pit climbing

Initialize the

npm init nuxt-app <name>

Running the above code will enter an interactive interface, similar to VUE-CLI. The following content can be generated after selecting the corresponding option as prompted.

The directory structure

├─ test_nuxt ├─ // Nuxt automatically generated, temporary file for compilation, Build ├─ Assets // For organizing uncompiled static resources like LESS, SASS or JavaScript, for static resources files that don't need to be processed by Webpack, ├─ Components can be placed in the static directory for custom Vue components, such as The Calendar component, the paging component, and ├─layouts // ⭐ ├─ Middleware // For storing middleware ├─node_modules ├─pages // for organizing application routes and views. Nuxt.js automatically generates routing configurations according to the directory structure. File names cannot be changed ⭐ ├─plugins // for organizing Javascript plug-ins that need to be run before the root vue.js application is instantiated. ├─ Static // Used to store application static files, such files will not be called by Nuxt.js Webpack build processing. When the server starts, files in this directory are mapped to the root/of the application. Folder names cannot be changed. ⭐ ├ ─store // Vuex status management for organizational applications. Folder names cannot be changed. ⭐ ├─.editorConfig // Development tool Format Configuration ├─.eslintrc.js // ESLint configuration file, Used to check code format ├─.gitignore // configure git to ignore file ├─nuxt.config.js // used to organize personalized Settings for nuxt.js applications to override default Settings. The file name cannot be changed. ⭐ ├─package-lock.json // NPM ├─package-lock.json // NPM package management config file ├─ readme.mdCopy the code

The directory structure is roughly the same as vue-CLI, but Nuxt does something with it. Among them:

  • componentsInstead of referring to a bunch of components every time you write code, the following components can be referenced automatically. Of course you can quote it if you like.
  • layoutsComponents under the directory are used for layout and can be specified in the page componentLayout options. If not specified, the default.vue component is used by default, similar to app. vue, except that you can manually specify which layout to use.
  • middlewareThe directory houses middleware, which can be likened to a route guard, and can be used for delicate controls such as interception or redirection. The difference is that the middleware parameter passed in is context, which, as mentioned later, is much richer.
  • pagesThe directory structure automatically generates the corresponding route configuration. You can select which layout to apply to the current page and specify which middleware to apply
  • pluginsDirectory to configure various plug-ins, such as UI component library import on demand, I18N, and so on

The life cycle

When the request arrives, the Store is first initialized, similar to the asyncData method in the component, where nuxtServerInit initializes state and still needs to return a Promise. Since you can get the context object here, you can get the request object out of it, which means you can store some of the login information and other information stored on the client side in vuEX for the client to use.

Next comes middleware validation. Middleware is a function whose parameters include context and request objects, which can be intercepted or redirected, similar to route guards. There are three levels of middleware that are called in turn.

The route parameters are then verified using the validate function. Validate is also nuxt’s extension option for vue. It passes in the context as a function and, depending on the return value, determines whether it is 404. It can be asynchronous or redirected to another page.

In this case, the asyncData function is merged into the data option after the asyncData is completed, which means that the asyncData function can set component data. The FETCH method is mainly used to handle vuEX logic, such as initiating an action to fetch some data. After version 2.12, this method moved behind Created in the lifecycle and can be called as a method

Finally enter the client side of the page rendering. When the route changes, the loop executes from the middleware phase.

Context object

function (context) {
  const {
    app,
    store,
    route,
    params,
    query,
    env,
    isDev,
    isHMR,
    redirect,
    error,
    $config
  } = context
  // Server-side
  if (process.server) {
    const { req, res, beforeNuxtRender } = context
  }
  // Client-side
  if (process.client) {
    const { from, nuxtState } = context
  }}
Copy the code

Context objects provide additional parameters to help developers fine-tune application logic. It is available in nuxT’s lifecycle functions such as asyncData, Fetch, Middleware, nuxtServerInit, and so on, as shown in the figure below

This is similar to the principle section above, where asyncData passes in arguments that are called context

Promise.all(matchedComponents.map(item= > {
    const { asyncData } = item
    if (asyncData) {
        return asyncData.call(item, {
            store,
            route: router.currentRoute
        })
    } else return Promise.resolve()
}))
Copy the code

High frequency problems

Window or document is not defined.

This is because some client-only scripts are packaged into server-side execution scripts. For scripts that are only suitable to run on the client side, you need to determine the import by using the process.client variable.

SSR needs to distinguish the environment, different host environment provides different API, the server has no window object and no Document object

For example

if (process.client) {
  require('external_library')
  window.scrollTo(100.100)... }Copy the code

The client-side rendered virtual DOM tree is not matching server-rendered content.

In development mode, Vue will infer whether the virtual DOM tree generated by the client matches the DOM structure rendered from the server. If there is no match, it will exit the blending mode, discard the existing DOM and start rendering from scratch. In production mode, this detection is skipped to avoid performance degradation. For details, see Client Activation.

Usually this problem does not occur, but browsers may change some of the special HTML structures. For example, when you write in a Vue template:

<table>
  <tr><td>hi</td></tr>
</table>
Copy the code

Browsers will automatically inject inside

, however, because the virtual DOM generated by Vue does not contain

, it will fail to match. For a proper match, make sure you write valid HTML to the template.

Another situation, often due to environmental issues, is that because the component library defaults to the client’s environment, there is no judgment on API availability, resulting in server rendering errors by calling an API that does not belong to that environment. We need to be context-sensitive and import the component libraries only on the client side. Configure the following code in nuxt.config.js to specify that the component libraries are imported only on the client side.

plugins: [
  { src: '~/plugins/bydesign.js'.mode: 'client'}].Copy the code

Components will then be stored in HTML as XML text due to the lack of component library support for server rendering.

<byted-alert type="info">{{ title }}</byted-alert>
<byted-button @click="onClick">Click on the</byted-button>
Copy the code

The above code will be rendered as follows

You can see that the component is not parsed into a regular HTML structure, but in the form of XML text. Because the client cannot match the structure, it will be destroyed and rebuilt. Fortunately, Nuxt provides us with a built-in component, ClientOnly, that only renders what is included on the client side. Wrap them up and render them as follows

<CLientOnly>
    <byted-alert type="info">{{ title }}</byted-alert>
    <byted-button @click="onClick">Click on the</byted-button>
</CLientOnly>
Copy the code

You can see that the wrapped component is not rendered. The console error disappears and the problem is resolved.

Therefore, if the component library does not support server-side running, it has to be wrapped in ClientOnly. In this way, SSR is less effective, after all, the intention is to pre-render the content. If so, the client will still receive an empty shell with no content and will have to render it itself. So if the component library is not supported on the server side, think twice.

conclusion

SSR solutions have better SEO and faster time-to-content, but they also have many attendant problems, such as:

  • Browser-specific code that can only be used in some lifecycle hook functions; Some external extension libraries may require special handling, such as mock global variables, to run in a server environment.
  • More build configurations and deployment requirements are involved
  • This increases server-side load, so if it is used in a high traffic environment, you need to prepare for the server load and use caching policies wisely

Generally speaking, for news, blog, community, consulting or some companies’ external home page projects that need traffic and exposure, high ranking in search engines can bring business benefits to a certain extent, in this case, SSR solutions can be considered. For some systems or Intranet projects with no SEO requirements for internal use, pure client rendering is sufficient and the benefits may not be obvious.

But it is undeniable that Nuxt framework based on Vue to do a higher level of encapsulation, while providing a lot of convenience, but also brought some new organization code ideas, for example, through the component is divided into routing components, layout components and business components, so that they can independently deal with their own things, and achieve decoupling. This idea is worth learning.

Finally, with or without SSR, Nuxt is worth a try, but be sure to read the official tutorial before using it.