preface

This article will gradually analyze the specific implementation of VUE server-side rendering from the perspective of a practical case, the whole process does not use a third-party server-side rendering framework, to explain the underlying implementation principle.

Server-side rendering (SSR) mainly solves the following two problems.

  • Improves the loading speed of the home page, which has obvious advantages for single-page applications.
  • Server rendering is optimizedseoIt is important that pages rendered on the server are more easily captured by search engines to improve site rankings. In someTo cIf users enter keywords into search engines and find that the site doesn’t come up, it’s not even profitable. As long as it is linked with economic benefits of technology, for every technician should focus on.

Back in the day, there was no separation between the front and the back, older programming languages like JAVA and PHP. They have always used servers to render pages, and many mature solutions have been developed over the years.

Front-end programmers routinely use three frameworks to develop pages: Vue, React, and Angular. Once the front end develops pages using these advanced frameworks and the back end programming languages are JAVA or PHP, they’re a little out of their depth for SSR. SSR of the old programming language can only be done in its own ecology, so this part of the work falls on the head of the front-end students.

Once the front end takes over SSR, the development mode of the page can be consistent with the previous. The same single-page application that was developed before is still being developed today, but with some additional configuration. This allows front-end programmers to retain their old development habits while allowing applications to support SRR at a low cost.

What exactly is SSR doing

Server-side rendering (SRR), as the name implies, is a process in which pages are rendered in the background and then sent to the front end for display. To contrast this with client rendering, look at the following code.

//index.js import Vue from 'vue'; import App from '.. /App.vue'; new Vue({ render: (h) => h(App), }).$mount('#app'); //index.html <! DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> </head> <body> <div id="app"></div> <! -- built files will be auto injected --> <script src="http://www.xxx.com/main.js"></script> </body> </html>Copy the code

The front-end often encounters this code when developing a single page application. The most obvious feature of client rendering is that the app node in the index.html sent from the back end is empty. The whole client-side rendering process is easy to get through.

  • The browser enters the url and requests the server
  • The backend will have a non-page contenthtmlSend it to the browser
  • The browser receiveshtmlStart loading when read to the endscriptThe request to the server beginsjsResources. At this timehtmlIs an empty template with no content
  • The back end receives the request and sends itjsSend the message to the browser. The browser receives the message and starts loading and executingjsThe code.
  • At this timevueIt starts to take over the entire application, and it starts to loadAppComponent, but foundAppThe component contains code for an asynchronous request. The browser starts to launch into the backgroundajaxRequest data, and start renderingAppComponent template.
  • AppWhen all the work is done for the component,vueThen put theAppThe contents of the component are inserted intoindex.htmlIn theidforappthedomElements.

From the above client rendering process, the back end sends the index.html to the front end is an empty template that does not contain the page content. The rendering process of the page content is completed by the browser, so this method is called client rendering.

The main difference between SRR and client-side rendering is step 2 above, where the back end directly sends a fully populated HTML to the browser for rendering.

This eliminates the need for the browser to send additional requests for data rendering templates, so the page loads more smoothly. Secondly, because the HTML itself sent is content, search engines can judge the type and use of the site through these content, so as to optimize SEO.

A profound

Let’s take a macro look at the server-side rendering process with a very simple example.

import Koa2 from 'koa'; import { createRenderer } from 'vue-server-renderer'; import Vue from 'vue'; const renderer = createRenderer(); const app = new Koa2(); Use (async function(CTX) {const vm = new Vue({template:"<div>hello world</div>"}); ctx.set('Content-Type', 'text/html; charset=utf-8'); const htmlString = await renderer.renderToString(vm); ctx.body = `<html> <head> </head> <body> ${htmlString} </body> </html>`; }); app.listen(3000);Copy the code

The logic of the above code is very simple, using KOA2 to build a Web server, listening to port 3000.

The browser initiates a request by typing in the url localhost:3000, and the request goes to the function method in app.use. In this function, you first define a very simple vUE instance and then format the data in the response to tell the browser that the data returned is a piece of HTML.

Important, we now have a VUE instance VM created on the server side.

What is VM? It’s a data object.

If you are familiar with the interaction between the front and back ends, you should know that the communication between the front and back ends is through sending strings as data formats, such as THE JSON string usually used by the API server. Vm is an object, which cannot be directly sent to the browser, but must be converted into a string before sending.

How do I convert a Vue instance to a string? This process is not arbitrary, because when creating an instance of vue, we can add not only the template property, but also the reactive data to it. We can also add the event method to it.

Vue server renderer is a plugin that converts a vue instance to a string. To use this plugin, you need to install the renderer using NPM.

Renderer. RenderToString, passing the VM as a parameter, easily returns the converted vm string, as shown below.

<div data-server-rendered="true">hello world</div>
Copy the code

Once you have the content string, you insert it into the HTML string, send it to the front end, and you’re done. The page will display Hello World.

From the above case, you can grasp the whole context of server-side rendering from a macro perspective.

  • The first step is to find out what the current request path isvuecomponent
  • Populate the component data content into a string
  • Finally, concatenate the strings intohtmlSend to the front end.

The VM above is a very simple vue instance with only one template attribute. A VM in a real business is a little more complicated because it integrates routing and VUex with the VM as the business grows. Let’s go through them.

Routing integration

Generally speaking, a project cannot have only one page. The purpose of integrated routing is to make a request path match a VUE page component for project management.

In the task of implementing SRR, the main task is to find out which VUE component the current request path matches after the client sends the request.

To create a route.js, fill in the following code.

import Vue from 'vue';
import Router from 'vue-router';
import List from './pages/List';
import Search from './pages/Search';

//route.js

Vue.use(Router);

export const createRouter = () => {
  return new Router({
    mode: 'history',
    routes: [
      {
        path: '/list',
        component: List,
      },
      {
        path: '/search',
        component: Search,
      },
      {
        path: '/',
        component: List,
      },
    ],
  });
};
Copy the code

Route and page components are defined in Route.js in the same way that the front end defined routes. If the front-end accesses the root path, the List component is loaded by default.

The App component is the same as before, with only a
viewport to display the content.

Go back to the entry file index.js on the server side and introduce the createRouter method defined above.

import Koa2 from 'koa'; import { createRenderer } from 'vue-server-renderer'; import Vue from 'vue'; import App from './App.vue'; import { createRouter, routerReady } from './route'; const renderer = createRenderer(); const app = new Koa2(); /** */ app.use(async function(CTX) {const req = ctx.request; const router = createRouter(); Const vm = new Vue({router, render: (h) => h(App),}); router.push(req.url); // Wait until the router hook function is finished parsing await routerReady(router); const matchedComponents = router.getMatchedComponents(); // Get the matching page component if (! Matchedcomponents.length) {ctx.body = 'this page was not found,404'; return; } ctx.set('Content-Type', 'text/html; charset=utf-8'); const htmlString = await renderer.renderToString(vm); ctx.body = `<html> <head> </head> <body> ${htmlString} </body> </html>`; }); app.listen(3000);Copy the code
  • usecreateRouter()Method to create a route instance objectrouterAnd inject it intoVueInstance.
  • Then performrouter.push(req.url)This step is very important. Equivalent to tellingVueExample, the current request path has been passed to you, you quickly find the page component to render according to the path.
  • await routerReady(router);Once the execution is complete, you have the page component that matches the current request path.
  • matchedComponents.lengthIf a is equal to the0The current request path does not match the route we defined, so we should customize a beautiful404The page is returned to the browser.
  • matchedComponents.lengthIs not equal to0, indicating the currentvmThe viewport has been occupied by the matching page component based on the request path. All I need to do is putvmConvert it to a string and send it to the browser.

Enter localhost:3000 in the browser and go through the process to render the contents of the List page.

Vuex integration

homogeneous

Routing integration allows you to render specified page components based on the path, but server rendering has limitations.

For example, if you add a V-Click event to a page component template, only to find that the event does not respond after the page has been rendered in the browser, that would definitely defeat our purpose.

How to solve such a thorny problem? Back to the essence of server-side rendering, the main thing it does is return an HTML that fills the page content to the client, and it doesn’t care what happens after that.

Event binding, clicking on links to jump to these are all capabilities of the browser. So client rendering can help us out.

The entire process can be designed as follows.

  • The browser enters a link request to the server, which will contain the page contenthtmlReturn, but inhtmlFile to add the client renderjsThe script.
  • htmlIt starts loading in the browser, and the page is already rendering static content. When the thread goes tohtmlUnder the filescriptTag to request client rendering of the script and execute it.
  • At this point in the client scriptvueThe instance starts to take over the application, and it starts to give the static that the back end originally returnedhtmlVarious capabilities, such as making event bindings on tags take effect.

This combines client-side rendering with SSR, which simply returns static HTML file content to make the page appear faster. The client vUE instance takes over the application after the static page is rendered, giving the page a variety of capabilities. This way of collaboration is called isomorphism.

The following is a code demonstration of the above process to deepen understanding.

To achieve isomorphism, you need to add client-side rendering code. Create client/index.js as the entry point for the WebPack build client script.

import Vue from 'vue'; import App from '.. /App.vue'; import { createRouter } from '.. /route'; const router = createRouter(); / / create routing new Vue ({the router, render: (h) = > h (App),}). $mount (' # App ', true);Copy the code

Webpack runs through the above client code and packs it into a bundle.js. The only difference is that $mount(‘#app’, true) is followed by a true argument.

The reason for this is understandable, since SSR sends rendered static HTML to the browser for rendering, and the client takes over the application.

But the page currently accessed by this path has already been rendered in the background and does not need to be rendered again by the client vue instance. Adding the true argument allows the client vUE instance to add only event binding and functionality support to the current template content.

In the entry file index.js of SSR, you need to add the following code.

import Koa2 from 'koa'; import { createRenderer } from 'vue-server-renderer'; import Vue from 'vue'; import staticFiles from 'koa-static'; import App from './App.vue'; import { createRouter, routerReady } from './route'; const renderer = createRenderer(); const app = new Koa2(); /** * app.use(staticFiles('public')); /** */ app. Use (async function(CTX) {... / / omit CTX. Body = ` < HTML > < head > < / head > < body > ${htmlString} < script SRC = "/ bundle. Js" > < / script > < / body > < / HTML > `; }); app.listen(3000);Copy the code

As you can see from the above changes, it is only a minor modification of the original, adding a script tag to the HTML returned by ctx.body to allow the browser to perform the configuration rendered by the client.

In order for the browser to request bundle.js, app.use(staticFiles(‘public’)) needs to be executed. If the request path is static, return the resources in the public folder directly to the client.

With this round of configuration, static pages rendered by SSR can be given various capabilities on the client side. Once the HTML file is loaded in the browser, the SSR mission is completed, all the things behind such as page jump, interactive operations are taken over by the Vue instance of the client JS script. At this point, there is no difference with the familiar scene before the front end.

Vuex configuration

Now assume that the list. vue template has the following contents.

<template> <div class="list"> <p> current page: list page </p> <a @click="jumpSearch()">go search page </a> <ul> <li v-for="item in list" : key = "item. Id" > < p > city: {{item. The name}} < / p > < / li > < / ul > < / div > < / template >Copy the code

As you can see from above, the template isn’t all static tag content, it renders a list of cities below. The city list data list is placed on a remote JAVA server.

This is where the problem arises. First, the client requests the node server with localhost:3000/list. Node intercepts the request and finds that the current page to render is list. vue based on the /list path.

It turns out inside the component that the data it needs to render is on a remote server, so the current Node server must first request the remote server to fetch the data before rendering list. vue, and then return the generated string to the browser.

To implement the above process smoothly, you need to leverage vuEX’s capabilities.

  • Create in the project root directoryvuex/store.js.
import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export function createStore() { return new Vuex.Store({ state: { list: [], name: 'kay', }, actions: GetList ({commit}, params) {return Promise((resolve)=>{commit("setList",[{name:" 中 国 "},{name:" 中 国 "}]); resolve(); },2000) }, }, mutations: { setList(state, data) { state.list = data || []; ,}}}); }Copy the code

This vuex configuration is no different from what was done before the front end. Defines an action method getList to get a list of cities. Use a timer in the getList method to simulate a remote request for delayed return of data.

  • Client integrationvuex
import Vue from 'vue'; import App from '.. /App.vue'; import { createRouter } from '.. /route'; import { createStore } from '.. /vuex/store'; const router = createRouter(); Const store = createStore(); new Vue({ router, store, render: (h) => h(App), }).$mount('#app', true);Copy the code
  • List.vueFile adds methods to get data asynchronously.
<template> <div class="list"> <p> current page: list page </p> <a @click="jumpSearch()">go search page </a> <ul> <li v-for="item in list" Key ="item.name"> <p> City: {{item.name}}</p> </li> </ul> </div> </template> <script> export default { asyncData({ store, route }) { return store.dispatch("getList"); }, computed: { list() { return this.$store.state.list; }, }, methods: { jumpSearch() { this.$router.push({ path: "search", }); ,}}}; </script>Copy the code

Add an asyncData method to the component to get remote data.

  • ssrintegrationvuex. Render the entry file on the server sideindex.jsaddstore.
import { sync } from 'vuex-router-sync'; . Use (async function(CTX) {const req = ctx.request; const router = createRouter(); Const store = createStore(); // Synchronize route state to store sync(store, router); const vm = new Vue({ router, store, render: (h) => h(App), }); router.push(req.url); . Omit the const matchedComponents = router. GetMatchedComponents (); All (matchedComponents. Map ((Component) => {if (component.asyncData) {return Component.asyncData({ store, route: router.currentRoute, }); }})); const htmlString = await renderer.renderToString(vm); . Omit})Copy the code

First create a store repository, then use sync to synchronize the route state and inject it into the vue instance, and then use Promise. All to execute the asyncData of the page component.

Let’s go through these steps again. The data for the page component template list.vue is now stored on a remote server and needs to be loaded to render.

Add an asyncData function to list. vue. This function, when triggered, will initiate actions in vuex to request remote data.

Now the browser opens the link localhost:3000/list. the server-side render entry file index.js takes over the request and discovers that the component of the page to render is list.vue.

All checks to see if asyncData is defined under list. vue, and if so, executes this function to request remote data. The data is returned and synchronized to the Store repository. The entire Vue instance is then re-rendered as vuex data changes, list. vue fills the template with remote data, and finally the Vue instance is converted into AN HTML string and returned to the browser.

dehydration

Now BOTH SSR and client are configured with VUex, but the difference is that the server store contains the remote request data required by List.vue, while the client store is empty.

And that creates a problem. The page was perfectly displayed in the browser when suddenly, in a flash, the city List of the list. vue page template disappeared.

Why is that? The SRR returns static HTML with a list of cities and various initialization operations once the client vUE takes over the entire application. The client also has to configure VUex, which retriggers page rendering because its data warehouse is empty. The part of the page that originally contained the list of cities disappeared.

To solve this problem, it is necessary to find a way to make SSR remote request data also sent to the client store. This way, even if the client takes over the application and finds that the city list data stored in the Store is the same as the page, it won’t cause flicker problems.

Add the following code to the SSR entry file.

  ctx.body = `<html>
  <head>
  </head>
    <body>
      ${htmlString}
      <script>
        var context = {
          state: ${JSON.stringify(store.state)}
        }
      </script>
      <script src="/index.js"></script>
    </body>
  </html>`;
Copy the code

In fact, the data in the server-side store is converted into a string into js variables and returned to the browser together.

The advantage of this is that the client script can access context.state to retrieve the remote request data.

The process of injecting data from the server to the client JS is called dehydration.

Water injection

The server side puts the data into the JS script, and the client side can get the data easily.

import Vue from 'vue'; import App from '.. /App.vue'; import { createRouter } from '.. /route'; import { createStore } from '.. /vuex/store'; const router = createRouter(); Const store = createStore(); if (window.context && window.context.state) { store.replaceState(window.context.state); } new Vue({ router, store, render: (h) => h(App), }).$mount('#app', true);Copy the code

Store. ReplaceState (window.context.state); . If window.context.state is found, this part of data is used as the initial data of VUEX. This process is called water injection.

Loading real data

In VuEX, the request data is simulated using a timer, followed by access to the real data using some open apis on the web.

Make the following changes to the action method in vuex.

actions: { getList({ commit }, params) { const url = '/api/v2/city/lookup? location=guangzhou&key=9423bb18dff042d4b1716d084b7e2fe0'; return axios.get(url).then((res)=>{ commit("setList",res.data.location); }}})Copy the code

The server loads the list. vue based on the request path, finds the asyncData method in it, and starts running it.

When asyncData runs it will go to the getList in the actions above and it will make a request for that URL. But a closer look found that this URL is not written domain name, such access will definitely report an error.

How about adding the remote domain name to it? There will be problems if you add it in this way. There is a scenario where the client takes over the application and it can also call getList, so this part of the Vuex code that we’re writing is shared between the server and the client. So if the client accesses the path with the remote domain name directly, it will cause cross-domain.

So how to solve this problem? It is best to use urls that do not include a domain name and start with a /. Client access to this path will then lead to the Node server. At this point, just add an interface proxy forwarding is done.

import proxy from 'koa-server-http-proxy'; export const proxyHanlder = (app)=>{ app.use(proxy('/api', { target: 'https://geoapi.qweather.com', / / the open API interface, online looking for return to geographic data support. PathRewrite: {' ^ / API ':'}, changeOrigin: true})); }Copy the code

Define a middleware function to add to KOA2 before performing server-side rendering.

The Node server sees a request path that starts with/API and then forwards the data to a remote address instead of following the server-side rendering logic.

Server side path request problem

Imagine a scenario in which the above proxy forwarding brings new problems. If the browser types localhost:3000/list, Node parses the request to load the list. vue page component, which in turn has an asyncData asynchronous method, so it runs the asynchronous method to get the data.

actions: { getList({ commit }, params) { const url = '/api/v2/city/lookup? location=guangzhou&key=9423bb18dff042d4b1716d084b7e2fe0'; return axios.get(url).then((res)=>{ commit("setList",res.data.location); }}})Copy the code

The asynchronous method is getList. Note that the Node server is executing the script, not the client’s browser.

If request to/at the beginning of the url, browser requests will be sent to the node server. The node server now need to request yourself, as long as the request his proxy Settings can put forward the request to the remote server, and now the node server requests begin with/path is absolutely cannot request to own, this time can only use absolute paths.

We mentioned above that this part of the Vuex code is shared by both the client and the server, so it is best not to write down the absolute path. A more elegant approach is to configure axios’s baseURL to generate an AXIos instance with a domain name to request. This part of the code can be changed to the following.

export function createStore(_axios) { return new Vuex.Store({ state: { list: [], name: 'kay', }, actions: { getList({ commit }, params) { const url = '/api/v2/city/lookup? location=guangzhou&key=9423bb18dff042d4b1716d084b7e2fe0'; return _axios.get(url).then((res)=>{ commit("setList",res.data.location); }) }, }, mutations: { setList(state, data) { state.list = data || []; ,}}}); }Copy the code

_axios is the instance object after the base domain name is configured. The client generates an _axios, and the server generates an _axios, but the client does not configure baseURL.

import axios from "axios"; // export const getClientAxios = ()=>{const instance = axios.create({timeout: 3000,}); return instance; } / / export const getServerAxios = (CTX)=>{const instance = axios.create({timeout: 3000, baseURL: 'http://localhost:3000' }); return instance; }Copy the code

Generating two axios instances preserves the unity of the Vuex code while also eliminating the node server’s inability to access itself.

How to handle cookies

After using the interface proxy, how to ensure that each interface forward will also send cookies to the remote server. The configuration can be as follows.

It’s in the SSR entry file.

*/ app.use(async function(CTX) {const req = ctx.request; / / icon directly return the if (the req. Path = = = '/ favicon. Ico') {CTX. Body = ' '; return false; } const router = createRouter(); Const Store = createStore(getServerAxios(CTX)); // Create data warehouse *** omitted})Copy the code

CTX is passed in when the CTX and AXIOS instances are created.

/** * export const getServerAxios = (CTX)=>{const instance = axios.create({timeout: 3000, headers:{ cookie:ctx.req.headers.cookie || "" }, baseURL: 'http://localhost:3000' }); return instance; }Copy the code

Fetch the cookie from CTX and assign it to THE HEADERS of Axios, thus ensuring that the cookie is carried.

Style to deal with

Files on.vue pages typically divide code into three tags

Some attributes can be added.

Compared to client-side rendering, the process of implementing SSR takes one more step. Extract the style content from

Add the following code to the SSR entry file index.js.

. Omit const context = {}; HtmlString = await renderer.renderToString(vm, context); ctx.body = `<html> <head> ${context.styles ? context.styles : ''} </head> <body> ${htmlString} <script> var context = { state: ${JSON.stringify(store.state)} } </script> <script src="./bundle.js"></script> </body> </html>`;Copy the code

The process of extracting the style on the server is very simple, defining a context object, the Context.

Renderer. RenderToString passes context as the second argument. After this function is executed, the styles property of the context object will have the style of the page component. Finally, concatenate the style into the HTML head header.

Head information processing

A regular HTML file not only contains styles in its head, it may also need to set

and
. The vuE-meta plugin can be used to customize header information for each page.

Now you need to add some headers to the list. vue page component, which you can set as follows.

<script> export default {metaInfo: {title: "table page ", meta: [{charset:" utF-8 "}, {name: "viewport", content: "width=device-width, initial-scale=1" }, ], }, asyncData({ store, route }) { return store.dispatch("getList"); }... Omit}Copy the code

Add a property metaInfo to the exported object and set title and meta, respectively;

Add the following code to the SSR entry file.

import Koa2 from 'koa'; import Vue from 'vue'; import App from './App.vue'; import VueMeta from 'vue-meta'; Vue.use(VueMeta); /** */ app. Use (async function(CTX) {... Const vm = new Vue({router, store, render: (h) => h(App),}); const meta_obj = vm.$meta(); Router.push (req.url); router.push(req.url); . Omit htmlString = await renderer. RenderToString (VM, context); const result = meta_obj.inject(); const { title, meta } = result; ctx.body = `<html> <head> ${title ? title.text() : ''} ${meta ? meta.text() : ''} ${context.styles ? context.styles : ''} </head> <body> ${htmlString} <script> var context = { state: ${JSON.stringify(store.state)} } </script> <script src="./index.js"></script> </body> </html>`; }); app.listen(3000);Copy the code

Meta_obj header information is generated through vm.$meta(). After the vue instance is loaded, execute meta_obj. Inject () to obtain meta and title data of the rendered page component, and then fill them into THE HTML string.

When the browser visits localhost:3000/list, the header of the HTML file will contain the title and meta information defined above.

The source code

The complete code

At the end

The whole process above is quite complicated, and the difficulty of server-side rendering is not in its own technical difficulties. It’s that the process is a bit complicated, with a lot of detail to deal with. However, if you really understand these principles, not only vue frameworks, but also react and Angular frameworks can be used to implement server-side rendering in the same way.