preface

Recently, I took over an old project, which was typical Vue componentized front-end rendering. The subsequent business optimization might go in the direction of SSR, so I made some technical reserves first. If you don’t know anything about Vue SSR, please read the official documentation first.

Train of thought

Vue provides an official Demo, the advantage of the Demo is large and complete, the disadvantage is not friendly to novice, easy to let people see. So, today we’re going to write a more user-friendly Demo. It’s a three-step process.

  1. Write a simple front-end rendering Demo (no Ajax data);
  2. Change the front-end rendering to the back-end rendering (still without Ajax data);
  3. On the basis of back-end rendering, plus Ajax data processing;

Step 1: Front-end render Demo

This part is relatively simple, consisting of two components in a page: Foo and Bar.

<! -- index.html --> <body> <div id="app"> <app></app> </div> <script src="./dist/web.js"></script> <! </body> </body>Copy the code
// app.js, also webpack entry import Vue from 'Vue '; import App from './App.vue'; var app = new Vue({ el: '#app', components: { App } });Copy the code
// App.vue
<template>
    <div>
        <foo></foo>
        <bar></bar>
    </div>
</template>
<script>
    import Foo from './components/Foo.vue';
    import Bar from './components/Bar.vue';
    export default {
        components:{
            Foo,
            Bar
        }
    }
</script>Copy the code
// Foo.vue
<template>
    <div class='foo'>
        <h1>Foo</h1>
        <p>Component </p>
    </div>
</template>
<style>
    .foo{
        background: yellow;
    }
</style>Copy the code
// Bar.vue
<template>
    <div class='bar'>
        <h1>Bar</h1>
        <p>Component </p>
    </div>
</template>
<style>
    .bar{
        background: blue;
    }
</style>Copy the code

The final rendering result is shown in the figure below. Please refer to the source codehere.

Step 2: Back-end rendering (without Ajax data)

The first Demo didn’t contain any Ajax data, but even so, it wasn’t easy to turn it into a back-end rendering. Where should we start?

  1. Split JS entry;
  2. Split Webpack packaging configuration;
  3. Write server-side render body logic.

1. Split the JS entry

At the time of front-end rendering, only one entry is required, app.js. To do back-end rendering now, you need to have two JS files: Entry-client. JS and entry-server. JS, which act as entrances to the browser and server, respectively. Let’s start with entry-client.js. Is it different from app.js in step 1? → no difference, just changed a name, the content is the same. Looking at entry-server.js, it simply returns an instance of app.vue.

// entry-server.js
export default function createApp() {
    const app = new Vue({
        render: h => h(App)
    });
    return app;  
};Copy the code

The main differences between entry-server.js and entry-client.js are as follows:

  1. entry-client.jsExecute on the browser side, so you need to specify el and explicitly call the $mount method to start the browser rendering.
  2. entry-server.jsIs called on the server side and therefore needs to be exported as a function.

2. Split the Webpack configuration

In the first step, because there is only one entry, app.js, you only need a Webpack configuration file. Now that you have two entries, you naturally need two Webpack configuration files: webpack.server.conf.js and webpack.client.conf.js, whose common parts are abstracted as webpack.base.conf.js. There are two things to note about webpack.server.conf.js:

  1. libraryTarget: 'commonjs2'→ Because the server is Node, it mustPackage according to commonJS specificationCan be called by the server.
  2. target: 'node'→ Specify Node environment, avoid non-node environment specific API error, such as document, etc.

3. Write server-side rendering body logic

Vue SSR relies on the package Vue -server-render, and its invocation supports two entry formats: createRenderer and createBundleRenderer. The former uses Vue components as the entry and the latter uses packaged JS files as the entry.

// dist/server.js const bundle = // dist/server.js const bundle = fs.readFileSync(path.resolve(__dirname, 'dist/server.js'), 'utf-8'); const renderer = require('vue-server-renderer').createBundleRenderer(bundle, { template: fs.readFileSync(path.resolve(__dirname, 'dist/index.ssr.html'), 'utf-8') }); server.get('/index', (req, res) => { renderer.renderToString((err, html) => { if (err) { console.error(err); Res.status (500).end(' Server internal error '); return; } res.end(html); })}); Server.listen (8002, () => {console.log(' backend rendering server started, port number: 8002'); });Copy the code

The final rendering of this step is shown below, where we can see that the component has been successfully rendered by the back end. Please refer to the source codehere.

Step 3: Back-end Render (pre-fetch Ajax data)

This is the crucial step, but also the most difficult step. What if the components in step 2 each need to request Ajax data? The official document gives us the idea, which I briefly summarize as follows:

  1. Get all the Ajax data you need up front (and Store it in Vuex’s Store) before you start rendering;
  2. For back-end rendering, Ajax data is injected into each component via Vuex.
  3. Bury all Ajax data in window.INITIAL_STATE and pass it to the browser via HTML;
  4. The browser side uses Vuex to inject Ajax data from Window. INITIAL_STATE into each component separately.

Here are a few highlights.

In Mounted, call this.fetchData, and then write the return data to the instance’s data in the callback. In SSR, this is not possible because the server does not perform mounted cycles. Can this. FetchData be executed earlier in the created or beforeCreate life cycle? No. This. FetchData is an asynchronous request. After the request is sent, before the data is returned, the back end is already rendered and cannot render the data returned by Ajax. Therefore, we need to know which components have Ajax requests in advance and wait for the Ajax requests to return data before we start rendering the components.

// store.js function fetchBar() {return new Promise(function (resolve, reject) {resolve('bar ajax return data '); }); } export default function createStore() { return new Vuex.Store({ state: { bar: '', }, actions: { fetchBar({commit}) { return fetchBar().then(msg => { commit('setBar', {msg}) }) } }, mutations:{ setBar(state, {msg}) { Vue.set(state, 'bar', msg); }}})}Copy the code
// Bar.uve asyncData({store}) { return store.dispatch('fetchBar'); }, computed: { bar() { return this.$store.state.bar; }}Copy the code

The component’s asyncData method is already defined, but how do I index it to the asyncData method? Let’s look at how my root component app.vue is written.

// App.vue
<template>
    <div>
        <h1>App.vue</h1>
        <p>vue with vue </p>
        <hr>
        <foo1 ref="foo_ref"></foo1>
        <bar1 ref="bar_ref"></bar1>
        <bar2 ref="bar_ref2"></bar2>
    </div>
</template>
<script>
    import Foo from './components/Foo.vue';
    import Bar from './components/Bar.vue';

    export default {
        components: {
            foo1: Foo,
            bar1: Bar,
            bar2: Bar
        }
    }
</script>Copy the code

As we can see from the root component app.vue, we can find each component’s asyncData method in turn by parsing its components field.

// entry-server.js export default function (context) {const store = createStore();  let app = new Vue({ store, render: h => h(App) }); // Find all asyncData methods let Components = app.components; let prefetchFns = []; for (let key in components) { if (! components.hasOwnProperty(key)) continue; let component = components[key]; if(component.asyncData) { prefetchFns.push(component.asyncData({ store })) } } return Promise.all(prefetchFns).then((res) => {// After all components' Ajax returns, the app is finally returned to render context.state = store.state; // Context. state is assigned to whatever, window.__INITIAL_STATE__ is what return app is; }); };Copy the code

A few more interesting questions:

  1. Is vue-router required? And it isn’t. Although the official Demo uses vue-Router, that’s just because the official Demo is a SPA with multiple pages. In general, vue-router is needed, because different routes correspond to different components, and asyncData of all components may not be executed every time. However, there are exceptions, such as my old project, there is only one page (a page contains many components), so there is no need to use vue-Router, still can do SSR. The main difference is how to find the asyncData methods that should be executed: the official Demo uses vue-Router, while I parse the Components field directly, and that’s it.
  2. Is Vuex necessary? → Yes, but no, look at Utah’s answer. Why must there be something like Vuex? So let’s analyze it. 2.1. After the pre-acquired Ajax data is returned, the Vue component has not yet started rendering. So, we have to store Ajax somewhere. 2.2. When the Vue component starts rendering, the Ajax data has to be taken out and properly passed to the components. 2.3. Window. INITIAL_STATE needs to be properly resolved and passed to each component during browser rendering. Therefore, we need a place to store, manage and deliver data independently of the view, which is why Vuex exists.
  3. Why pass Ajax data to the front end via window.initial_State when the back end has already converted Ajax data to HTML? → because the front-end rendering still needs to know these data. For example, you might write a component and bind it to a click event that prints the value of the this. MSG field when clicked. Now the back end renders the component HTML, but the binding of the event must be done by the browser. If the browser does not have the same data as the server side, where will the MSG field be when the click event is triggered?

At this point, we have completed the back-end rendering with Ajax data. This is the most complex and critical step, requiring a lot of thought and trial and error. Specific rendering renderings are shown below, please refer to the source codehere.

The effect

Are we done? Not yet. People say SSR improves first screen rendering speed, so let’s compare and see if it’s true. (Also in the Fast 3G network conditions).



A distortion of official thinking

This is the end of the Vue SSR Demo. The following are some variations that I have combined with the characteristics of my own project, so readers who are not interested can skip them. What are the downsides of step three’s official thinking? I think so: for older projects, the cost of retrofitting is relatively high. It is necessary to explicitly introduce VUEX, so action and mutations are necessary. Both the code change momentum and the learning cost of newcomers are not low. Is there any way to reduce the amount of change to older front-end rendering projects? Here’s what I did.

// action, mutations, those are not needed, State export default function createStore() {return new vuex.store ({state: {}})}Copy the code
// bar. vue // tagName is the name of the component instance, such as bar1, bar2, foo1, etc. Export default {prefetchData: Function (tagName) {return new Promise((resolve, reject) => {resolve({tagName, data: 'Bar ajax '}); }}})Copy the code
// Entry-server.js return promise.all (prefetchFns).then((res) => { // state (tagName, tagName) Res. forEach((item, key) => {Vue. Set (store.state, '${item.tagName}', item.data); }); context.state = store.state; return app; });Copy the code
// ssrmixin.js // Abstract the computed data required by each component into a mixin, and inject export default {computed: { prefetchData () { let componentTag = this.$options._componentTag; // bar1, bar2, foo1 return this.$store. State [componentTag]; }}}Copy the code

At this point, we have obtained a variant of Vue SSR. For component developers, simply abstract the original this.fetchData method into the prefetchData method, and then use {{prefetchData}} in the DOM to retrieve the data. See here for the code for this section.

conclusion

Vue SSR is really an interesting thing, the key is to use it flexibly. There is one remaining issue with this Demo: when Ajax is abstracted to prefetchData and made into SSR, the original front-end rendering fails. Can the same code support both front-end and back-end rendering? This way I can always cut back to the front end when things go wrong with the back end rendering and have a backstop solution.

The resources

  1. Build vuE-SSR series from scratch: write in front of words, By talking fish
  2. Vue SSR server rendering record, By echo_numb
  3. Practice one: From zero to rough mixing front and back ends, By Songlairui
  4. Vue 2 server rendering preliminary, By the topic leaf
  5. Vue SSR treading pits, By Ghosert
  6. NodeJS VM module By Dorsywang

— — — — — — — — — — –