EggJs+Vue服務端渲染實踐

最近須要把公司基於Vue進行開發的webView頁面改形成ssr的。因爲現有的node後臺是基於EggJs開發的,若是改用其餘腳手架搭建須要的成本也忒大了,這得加多少班啊,索性就本身瞎搗鼓一下吧。
咱們先來看看當咱們使用vue客戶端渲染時一個頁面的加載過程。首先是咱們請求的index.html到達瀏覽器,而後瀏覽器再根據index.html中的script標籤去請求js文件,請求到了js文件後開始執行Vue實例的初始化操做。 vue的實例初始化大致上須要通過:
實例初始屬性設置,掛載createElement方法->beforecreate->initState中對Props/data聲明響應式對象,初始化methods並bind執行上下文,對computed屬性生成computed watcher->created->生成渲染watcher->執行updateComponent->執行render方法,經過以前掛載的createElement方法生成vnode節點->執行_update方法,調用_patch_方法根據虛擬dom生成真實的dom節點->mounted
這麼多步驟的處理以後,咱們才能在瀏覽器上看到咱們的頁面,並且注意是每個組件都須要經歷這麼多步驟才能夠哦,因此若是在對首屏加載時間有必定要求的頁面中咱們還使用客戶端渲染的方式去處理。那麼大概,可能,應該是會被兇殘的產品經理打死的吧。。。
而後咱們先來看一下經過客戶端渲染完成的頁面的屏幕快照(使用chrome的Capture screenshots截取)
咱們能夠看到,頁面在337ms以前頁面處於不忍直視的狀態,直到403ms頁面終於顯示出了第一幅畫面。。。
進行服務端渲染就是在咱們請求index.html時,在node服務器中先將咱們的vue實例渲染成html字符串,而後再從服務器返回。這時候瀏覽器獲取到的html文件中將再也不是空空如也。
這是服務器端渲染返回的結果,在58ms時瀏覽器就已經顯現出了咱們頁面的大致樣式,省去了前面大約0.3s的從vue實例建立到mounted的空白期應該足夠給產品經理交差了。
vue-ssr實現的代碼在
https://ssr.vuejs.org/zh/​ssr.vuejs.org
中已經有了至關詳細的實現了,這裏主要寫寫如何將vue-ssr集成到EggJs框架中。
項目目錄結構:
config文件夾下放的是咱們的webpack配置文件;public是EggJs的靜態資源目錄,因此我把打包後的dist文件夾放在了這裏,順便把模板index.html也放在這兒;src目錄下是vue項目的源文件。
首先是webpack-base-config.js文件:
// webpack-base-config.js
const { VueLoaderPlugin } = require('vue-loader');
const path = require('path');

module.exports = {
    //輸出路徑爲EggJs靜態資源文件夾public下的dist文件夾
    output: {
        publicPath: '/public/dist/',
        path: path.join(__dirname, '../public/dist'),
    },
    resolve: {
        extensions: ['.js', '.vue', '.json'],
        alias: {
            'vue$': 'vue/dist/vue.esm.js',
        }
    },
    module: {
        rules: [{
            test: /\.vue$/,
            loader: 'vue-loader'
        }, {
            test: /\.css$/,
            use: ["vue-style-loader", "css-loader", 'less-loader']  
        }, {
            test: /\.less$/,
            use: ["vue-style-loader", "css-loader", 'less-loader']  
        }, {
            test: /\.(gif|png|jpg|woff|svg|ttf|eot)\??.*$/,
            loader: {
                loader: 'url-loader',
                options: {
                    limit: 8192,
                    name: './resource/[name].[ext]',
                },
            }
        }, {
            test: /\.js$/,
            loader: 'babel-loader',
            exclude: /node_modules/
        }],
    },
    plugins: [
        new VueLoaderPlugin(),
    ]
}

webpack-client-config.jscss

const webpack = require('webpack');
const merge = require('webpack-merge');
const baseConfig = require('./webpack-base-config.js');
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
const path = require('path');

module.exports = merge(baseConfig, {
    // 爲了兼容安卓4.0版本,編譯成es5語法
    entry: [ 'babel-polyfill', path.join(__dirname, '../src/entry-client.js') ],
    plugins: [
        new webpack.optimize.SplitChunksPlugin({
            name: 'manifest',
            minChunks: Infinity
        }),
        new VueSSRClientPlugin()
    ]
})

webpack-server-config.jshtml

const merge = require('webpack-merge');
const nodeExternals = require('webpack-node-externals');
const baseConfig = require('./webpack-base-config');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
const path = require('path');

module.exports = merge(baseConfig, {
    entry: [ 'babel-polyfill', path.join(__dirname, '../src/entry-server.js') ],
    target: 'node',
    devtool: 'source-map',
    output: {
        libraryTarget: 'commonjs2'
    },
    externals: nodeExternals({
        whitelist: /\.css$/
    }),
    plugins: [
        new VueSSRServerPlugin()
    ]
})

這裏因爲是服務端渲染,hash模式的路由並不可以讓服務器獲取到路由改變事件,因此咱們的路由模式必須是history模式而不是hash模式,具體router文件以下:vue

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

const home = (resolve) => { require(["../views/home.vue"], resolve) };

Vue.use(Router);

let router = new Router({
    mode: 'history',
    routes: [{
        path: '/home',
        component: home
    }]
});

export function createRouter() {
    return router;
}

Vue項目的入口文件須要由原來的直接new Vue()實例並mount修改成暴露一個createApp實例的方法:node

import Vue from 'vue';
import App from './App.vue';
import { createRouter } from './router/index.js';

export function createApp() {

    // 建立router實例
    const router = createRouter()

    // 建立vue對象實例
    const app = new Vue({
        render: h => h(App),
        router
    });

    return { app, router };
}

客戶端打包入口文件:webpack

import { createApp } from './app';

const { app, router } = createApp();

router.onReady(() => {
    app.$mount('#app', true);
});

服務器端打包入口文件:git

import { createApp } from './app';

export default context => {

    return new Promise((resolve, reject) => {
        const { app, router } = createApp();

        router.push(context.url);

        router.onReady(() => {
            const matchedComponents = router.getMatchedComponents();

            if (!matchedComponents.length) {
                return reject({
                    code: 404
                });
            }
            resolve(app);
        }, reject);
    });
};

配置完以上文件以後運行咱們熟悉的npm run build指令在dist文件夾下會生成一個服務端渲染所需的bundle和客戶端混合所需的manifest文件:github

 

而後咱們在EggJs項目的根目錄下添加app.js文件,在EggJs啓動的willReady生命週期中根據咱們的bundle和manifest文件建立renderer對象,
並將renderer方法掛載到EggJs全局的Application對象上方便以後在controller中調用該方法進行服務端渲染。
而後咱們在EggJs項目的根目錄下添加app.js文件,在EggJs啓動的willReady生命週期中根據咱們的bundle和manifest文件建立renderer對象,並將renderer方法掛載到EggJs全局的Application對象上方便以後在controller中調用該方法進行服務端渲染。
const { createBundleRenderer } = require('vue-server-renderer');
const serverBundle = require('./app/public/dist/vue-ssr-server-bundle.json');
const clientManifest = require('./app/public/dist/vue-ssr-client-manifest.json');
const path = require('path');
const file = require('fs');

class AppBootHook {
    constructor(app) {
        this.app = app;
    }

    // 配置文件加載完畢事件
    async willReady() {
        let renderer = createBundleRenderer(serverBundle, {
                runInNewContext: false,
                template: file.readFileSync(path.join(__dirname, './app/public/index.html'), 'utf-8'),
                clientManifest
            });

        this.app.renderer = renderer;
    }
}

module.exports = AppBootHook;

以後再在controller中根據請求的url路徑調用renderToString方法把頁面渲染爲html字符串返回給瀏覽器就大功告成了。web

'use strict';

const Controller = require('egg').Controller;

class HomeController extends Controller {
    async index() {
        let renderer = this.app.renderer;

        let context = {
            url: this.ctx.request.url
        };

        renderer.renderToString(context, (err, html) => {
            if (err) {
                if (err.code === 404) {
                    this.ctx.body = "404";
                } else {
                    this.ctx.body = process.env.NODE_ENV;
                }
            } else {
                this.ctx.body = html;
            }
        });
    }
}

module.exports = HomeController;

git地址:https://github.com/cgy-tiaopi/egg-vue-ssrvue-router

相關文章
相關標籤/搜索