0. Introduction to server rendering

Server-side rendering is not a new technology; In the early days of the Web, pages were returned by server-side rendering. In PHP, it was common to write template files using templates like Smarty, and then the PHP server framework rendered the data and templates back to the page. The downside of this server-side rendering is that if you want to view a new page, you have to call the server. Refresh the page.

In today’s front-end, however, the entire rendering is usually done in javascript on the browser side, in combination with history.pushState and other methods for single-page applications (SPA: Single-page Application), but it still has some disadvantages: the first load is too slow, and the user has to wait a long time for the browser to complete rendering; Not friendly to search engine crawlers etc. At this point, front-end frameworks like React and Vue 2.0 emerged to do server-side rendering.

Using these frameworks to do server-side rendering takes advantage of all of the above, and one piece of code can run both server-side and browser-side. Vue 2.0 has been released for some time. One of the biggest updates in the new version is the support for server rendering. Recently, I had some time to play with the server rendering of Vue.

1. Use server-side rendering in Vue 2.0

The official documentation gives a simple example to do server-side rendering:

Var Vue = require(' Vue ') var app = new Vue({render: Function (h) {return h('p', 'hello world')}}) Var renderer = require(' jue-server-renderer ').createrenderer () Renderer. RenderToString (app, function (error, html) { if (error) throw error console.log(html) // => <p server-rendered="true">hello world</p> })Copy the code

In this way, server rendering can be easily implemented with the usual Node server framework. However, in real life scenarios, we usually use the module organization of.vue files. In this case, server rendering needs to use Webpack to package vue components into a single file.

2. Render with Webpack.vuefile

Start by creating a server side entry file server.js

import Vue from 'vue';

import App from './vue/App';

export default function (options) {
    const VueApp = Vue.extend(App);

    const app = new VueApp(Object.assign({}, options));

    return new Promise(resolve => {
        resolve(app);
    });
}
Copy the code

This is the same as the browser-side entry file, except that by default it exports a function that receives some configuration from the server when rendered by the server and returns a Promise containing the app instance.

Write a simple app.vue file

<template>
    <h1>{{ title }}</h1>
</template>

<script>
module.exports = {
    props: ['title']
</script>
Copy the code

Here, the data property of options passed in from the server entry file will be read, and the title value will be taken and rendered into the corresponding DOM.

Looking at the Configuration of Webpack, it is much the same as client rendering:

const webpack = require('webpack'); const path = require('path'); const projectRoot = __dirname; const env = process.env.NODE_ENV || 'development'; Module. exports = {target: 'node', // tells Webpack to export node code devtool: null, // Devtool: {app: path.join(projectRoot, 'src/server.js') }, output: Object.assign({}, base.output, { path: Path.join (projectRoot, 'SRC '), filename: 'bundle.server.js', libraryTarget: 'commonjs2' // different from client}), plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(env), 'process.env.VUE_ENV': })], resolve: {extensions: ['', '.js', '.vue'], fallback: [path.join(projectRoot, 'node_modules')] }, resolveLoader: { root: path.join(projectRoot, 'node_modules') }, module: { loaders: [ { test: /\.vue$/, loader: 'vue' }, { test: /\.js$/, loader: 'babel', include: projectRoot, exclude: /node_modules/ } ] } };Copy the code

There are three main differences: declare node module packaging; Modify the loading mode of packaged modules to commonJS (commonJs2 can see the official Webpack document for details); Another is the vUE server packaging optimization, if this part is not passed after vUE server rendering will be slow to tens of seconds, once thought that the server code hung.

Finally, the server loads the generated bundle.server.js file:

const fs = require('fs'); const path = require('path'); const vueServerRenderer = require('vue-server-renderer'); const filePath = path.join(__dirname, 'src/bundle.server.js'); Const code = fs.readFileSync(filePath, 'utf8'); const bundleRenderer = vueServerRenderer.createBundleRenderer(code); / / rendering Vue applications for a string bundleRenderer renderToString (options, (err, HTML) = > {the if (err) {console. Error (err); } content.replace('<div id="app"></div>', html); });Copy the code

Here options can pass in data and other information required by vUE components; The following uses the official instance express as a server example:

const fs = require('fs');
const path = require('path');
const vueServerRenderer = require('vue-server-renderer');
const filePath = path.join(think.ROOT_PATH, 'view/bundle.server.js');
global.Vue = require('vue')

// 读取 bundle 文件,并创建渲染器
const code = fs.readFileSync(filePath, 'utf8');
const bundleRenderer = vueServerRenderer.createBundleRenderer(code);

// 创建一个Express服务器
var express = require('express');
var server = express();

// 部署静态文件夹为 "assets" 文件夹
server.use('/assets', express.static(
    path.resolve(__dirname, 'assets');
));

// 处理所有的 Get 请求
server.get('*', function (request, response) {
    // 设置一些数据,可以是数据库读取等等
    const options = {
        data: {
            title: 'hello world'
        }
    };

    // 渲染 Vue 应用为一个字符串
    bundleRenderer.renderToString(options, (err, html) => {
        // 如果渲染时发生了错误
        if (err) {
            // 打印错误到控制台
            console.error(err);
            // 告诉客户端错误
            return response.status(500).send('Server Error');
        }

        // 发送布局和HTML文件
        response.send(layout.replace('<div id="app"></div>', html));
    });

// 监听5000端口
server.listen(5000, function (error) {
    if (error) throw error
    console.log('Server is running at localhost:5000')
});
Copy the code

This is basically the entire process of Vue server rendering, this way and the use of ordinary template rendering does not have any other advantages, but when the rendering is completed and then taken over by the client rendering can do a seamless switch, let’s look at the client side with the rendering;

3. Render seamlessly with the browser

In order to combine with client rendering, we divide the Webpack configuration file into three parts: base shared configuration, Server configuration, client browser configuration, as follows:

webpack.base.js

const path = require('path'); const projectRoot = path.resolve(__dirname, '.. / '); module.exports = { devtool: '#source-map', entry: { app: path.join(projectRoot, 'src/client.js') }, output: { path: path.join(projectRoot, 'www/static'), filename: 'index.js' }, resolve: { extensions: ['', '.js', '.vue'], fallback: [path.join(projectRoot, 'node_modules')], alias: { 'Common': path.join(projectRoot, 'src/vue/Common'), 'Components': path.join(projectRoot, 'src/vue/Components') } }, resolveLoader: { root: path.join(projectRoot, 'node_modules') }, module: { loaders: [ { test: /\.vue$/, loader: 'vue' }, { test: /\.js$/, loader: 'babel', include: projectRoot, exclude: /node_modules/ } ] } };Copy the code

webpack.server.js

const webpack = require('webpack'); const base = require('./webpack.base'); const path = require('path'); const projectRoot = path.resolve(__dirname, '.. / '); const env = process.env.NODE_ENV || 'development'; module.exports = Object.assign({}, base, { target: 'node', devtool: null, entry: { app: path.join(projectRoot, 'view/server.js') }, output: Object.assign({}, base.output, { path: path.join(projectRoot, 'view'), filename: 'bundle.server.js', libraryTarget: 'commonjs2' }), plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(env), 'process.env.VUE_ENV': '"server"', 'isBrowser': false }) ] });Copy the code

Server configuration, and before more than an isBrowser global variable, used to do some differences in the Vue module;

webpack.client.js

const webpack = require('webpack'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const base = require('./webpack.base'); const env = process.env.NODE_ENV || 'development'; const config = Object.assign({}, base, { plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(env), 'isBrowser': true }) ] }); config.vue = { loaders: { css: ExtractTextPlugin.extract({ loader: 'css-loader', fallbackLoader: 'vue-style-loader' }), sass: ExtractTextPlugin.extract('vue-style-loader', 'css! sass? indentedSyntax'), scss: ExtractTextPlugin.extract('vue-style-loader', 'css! sass') } }; config.plugins.push(new ExtractTextPlugin('style.css')); if (env === 'production') { config.plugins.push( new webpack.LoaderOptionsPlugin({ minimize: true }), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }) ); } module.exports = config;Copy the code

CSS and other style files are ignored on the server side while style files are separately packaged into a CSS file on the browser side. When executing webpack, you need to specify the –config parameter to compile the different versions as follows:

# webpack --config webpack.client.js # webpack --config webpack.server.jsCopy the code

Similarly, the entry file also presents three files, index.js, server.js, and client.js

index.js

import Vue from 'vue';

import App from './vue/App';
import ClipButton from 'Components/ClipButton';
import Toast from 'Components/Toast';

Vue.filter('byte-format', value => {
    const unit = ['Byte', 'KB', 'MB', 'GB', 'TB'];
    let index = 0;
    let size = parseInt(value, 10);

    while (size >= 1024 && index < unit.length) {
        size /= 1024;
        index++;
    }

    return [size.toString().substr(0, 5), unit[index]].join(' ');
});

Vue.use(Toast);
Vue.component('maple-clip-button', ClipButton);

const createApp = function createApp(options) {
    const VueApp = Vue.extend(App);

    return new VueApp(Object.assign({}, options));
};

export {Vue, createApp};
Copy the code

Index.js does some general components, plug-in loading, some global Settings, and finally returns a function that can generate app instances for different environments to call;

server.js

import {createApp} from './index';

export default function (options) {
    const app = createApp(options);

    return new Promise(resolve => {
        resolve(app);
    });
}
Copy the code

Most of the logic has already been shared, so on the server side you simply return the app instance through promise;

client.js

import VueResource from 'vue-resource';
import {createApp, Vue} from './index';

Vue.use(VueResource);
const title = 'Test';

const app = createApp({
    data: {
        title
    },
    el: '#app'
});

export default app;
Copy the code

Similarly on the client side, the VueResource plug-in is loaded on the client side for ajax requests on the client side; Usually, ajax requests the server to return data and then initializes the app, which is basically a single-page server rendering framework. In general, we make single-page applications and distribute routes through URLS in cooperation with history.pushState and so on. In this way, it is better for our servers to render different pages using the same formula.

4. The server and browser share routes

The browser still loads the routing configuration in the normal way, and the server loads the routing configuration and renders the page with router.push before rendering, so add the routing configuration in the general entry file:

import Vue from 'vue';
import router from './router';
import App from './vue/App';

const createApp = function createApp(options) {
    const VueApp = Vue.extend(App);

    return new VueApp(Object.assign({
        router
    }, options));
};

export {Vue, router, createApp};
Copy the code

The routing file looks like this:

import Vue from 'vue'; import VueRouter from 'vue-router'; import ViewUpload from '.. /vue/ViewUpload'; import ViewHistory from '.. /vue/ViewHistory'; import ViewLibs from '.. /vue/ViewLibs'; Vue.use(VueRouter); const routes = [ { path: '/', component: ViewUpload }, { path: '/history', component: ViewHistory }, { path: '/libs', component: ViewLibs } ]; const router = new VueRouter({mode: 'history', routes, base: __dirname}); export default router;Copy the code

Here the route uses HTML5 history mode;

The server entry file is configured like this:

import {createApp, router} from './index';

export default function (options) {
    const app = createApp({
        data: options.data
    });

    router.push(options.url);
    return new Promise(resolve => {
        resolve(app);
    });
}
Copy the code

After initializing the app instance, router.push(options.url) is called to push the URL fetched by the server to the route.

5. Pits encountered in use

The whole process went well, one of the most problems is some module on the server or the browser to use, and use the ES6 module loading is static, so you need to change the static load module to dynamic loading, so there will be a top configuration isBrowser this global properties, judging by this attribute module is loaded, For example, the Clipboard module used in my project was directly loaded using ES6 before.

<template> <a @click.prevent :href="text"><slot></slot></a> </template> <script> import Clipboard from 'clipboard'; export default { props: ['text'], mounted() { return this.$nextTick(() => { this.clipboard = new Clipboard(this.$el, { text: () => { return this.text; }}); this.clipboard.on('success', () => { this.$emit('copied', this.text); }); }); }}; </script>Copy the code

This will result in a server rendering error, so you can change the load to dynamic load:

let Clipboard = null;

if (isBrowser) {
    Clipboard = require('clipboard');
}
Copy the code

If the module is not rendered on the server, the code does not need to change;

The VueResource plug-in also requires a browser environment, so it needs to be configured separately in client.js;

6. Summary

The same route is rendered by the Vue server and the HTML is always the same. This is different from the hashing of React renderers, so you can optimize the cache of rendered results. See the official documentation for this. The only downside is that the server also needs WebPack.

EOF

Article link: blog.alphatr.com/how-to-use-…

Last updated: 20:59, 10 November 2016