實現 Vue 服務端渲染(Vue SSR)

什麼是服務端渲染(SSR)?

Vue.js 是構建客戶端應用程序的框架,可是也能夠將同一個組件渲染爲服務端的 HTML 字符串,將它們直接發送到瀏覽器,最後將這些靜態標記「激活」爲客戶端上徹底可交互的應用程序。
服務器渲染的 Vue.js 應用程序也能夠叫作「同構」或「通用」,程序上的大部分代碼均可以在服務器和客戶端上運行。html

是否須要服務器渲染?

與傳統 SPA 相比,SSR 的主要優點在於:vue

  • 更好的 SEO
  • 更快的內容到達時間(time-to-content)

須要注意的是:node

  • 開發條件有限,因爲沒有動態更新,服務端渲染過程當中,只有 beforeCreatecreated鉤子函數被調用,在這兩個生命週期函數中應該避免產生全局反作用的代碼,例如在其中使用setInterval設置 timer。
  • 涉及構建設置和部署的更多要求。服務器渲染應用程序,須要處於 Node.js server 運行環境。
  • 更多的服務器端負載。若是預料到在高流量環境下使用,須要準備相應的服務器負載,並明智地採用緩存策略。

若是確實須要服務端渲染,那麼能夠繼續看下面的用法。webpack

基本用法


安裝

npm install vue vue-server-renderer --save
複製代碼

注意:web

  • Node.js 版本 6+
  • vue-servier-renderervue必須匹配版本

構建步驟


1. 創建入口文件

對於客戶端應用程序和服務器應用程序,都須要使用 webpack 打包兩個 Bundle,服務器須要 Server Bundle 用於服務器渲染,Client Bundle 會發送給瀏覽器,用於混合靜態標記。npm

一個基本項目像這樣:json

src
├── components
│   ├── Foo.vue
│   └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── index.template.html
├── entry-client.js # 僅運行於瀏覽器
└── entry-server.js # 僅運行於服務器
複製代碼

因單線程機制,在服務端渲染中有相似於單例的操做,全部的請求都會共享這個單例的操做,因此應該使用工廠函數來確保每一個請求之間的獨立性。app.js主要是 export 一個createApp函數。相似地 storerouter都須要導出這樣的工廠函數:api

# app.js
import Vue from 'vue';
import App from './App.vue';
import { createStore } from './store';
import { createRouter } from './router';

export const createApp = () => {
  const store = createStore();
  const router = createRouter();
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  });
  return { app, router, store };
};

複製代碼

在客戶端 entry 中建立應用程序,而且將其掛載到 DOM 中。瀏覽器

# entry-client.js
import { createApp } from './app';
const { app, router, store } = createApp();

if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__);
}

router.onReady(() => {
  # 添加路由鉤子函數,用於處理 asyncData.
  # 在初始路由 resolve 後執行,以便咱們不會二次預取(double-fetch)已有的數據。
  # 使用 `router.beforeResolve()`,以便確保全部異步組件都 resolve。
  router.beforeResolve((to, from, next) => {
    ...
  });

  app.$mount('#app');
});

複製代碼

服務器 entry 使用 default export 導出函數,並在每次渲染中重複調用此函數。在這裏能夠執行服務端路由匹配 (server-side route matching) 和數據預取邏輯(data-pre-fetching logic)。緩存

# entry-server.js
import { createApp } from './app';

export default context => {
  # 由於有可能會是異步路由鉤子函數或組件,因此咱們將返回一個 Promise,
  # 以便服務器可以等待全部的內容在渲染前就已經準備就緒。
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp();
    router.push(context.url);
    
    router.onReady(() => {
      # 服務器端數據預取
      ...
    }, reject);
  });
};

複製代碼

2. webpack 構建配置

配置文件結構像這樣:

build
├── dev-server.js
├── setup-dev-server.js
├── webpack.base.conf.js 
├── webpack.client.conf.js 
├── webpack.dev.conf.js 
├── webpack.prod.conf.js 
└── webpack.server.conf.js
複製代碼

package.json打包命令:

"scripts": {
    "dev": "NODE_ENV=dev node server/index.js",
    "build:client": "webpack --config build/webpack.client.conf.js --progress --hide-modules --progress",
    "build:server": "webpack --config build/webpack.server.conf.js --progress --hide-modules --progress",
    "build:prod": "NODE_ENV=prod npm run build:client && NODE_ENV=prod npm run build:server",
  },
複製代碼

3.開發服務器集成

開發服務使用的是 Koa,配置參考:

import Koa from 'koa';
import koaRouter from 'koa-router';
import { createBundleRenderer } from 'vue-server-renderer';

const app = new Koa();
const router = koaRouter();
const createRenderer = (bundle, options) => {
  return createBundleRenderer(
    bundle,
    {...options, { runInNewContext: false }
  );
};

const renderData = (ctx, renderer) => {
  const context = {
    url: ctx.url
  };
  return new Promise((resolve, reject) => {
    renderer.renderToString(context, (err, html) => {
      if (err) {
        reject(err);
      }
      resolve(html);
    });
  });
};

let renderer;
require('../build/setup-dev-server.js')(app, (bundle, options) => {
  renderer = createRenderer(bundle, options);
});

# proxy api request
const proxy = require('koa-server-http-proxy');
# 代理配置...

router.get('*', async (ctx, next) => {
    if (!renderer) {
      ctx.type = 'html';
      return (ctx.body = 'waiting for compilation...');
    }
    
    let html;
    try {
      html = await renderData(ctx, renderer);
    } catch (e) {
      # 處理特殊狀況
      ...
    }
    ctx.body = html;
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(80, '0.0.0.0', () => {
  console.log(`server is running...`);
});

複製代碼

4.線上服務器集成

線上服務使用的是 Egg.js,參考配置以下:

# app/controller/home.js
const Controller = require('egg').Controller;
const path = require('path');
const { createBundleRenderer } = require('vue-server-renderer');

const serverBundle = require('../public/vue-ssr-server-bundle.json');
const clientManifest = require('../public/vue-ssr-client-manifest.json');
const template = require('fs').readFileSync(
  path.resolve(__dirname, '../public/index.html'),
  'utf-8'
);
const renderer = createBundleRenderer(serverBundle, {
  runInNewContext: false,
  template,
  clientManifest
});

class HomeController extends Controller {
  async index() {
    const ctx = this.ctx;
    const context = { url: ctx.url };

    try {
      # 傳入context渲染上下文對象
      renderer.renderToString(context, (err, html) => {
        if (err) {
          throw err;
        }
        ctx.status = 200;
        # 傳入了template, html結構會插入到<!--vue-ssr-outlet-->
        ctx.body = html;
      });
    } catch (error) {
      ctx.status = 500;
      ctx.body = 'Internal Server Error';
    }
  }
}

module.exports = HomeController;

複製代碼

路由匹配:

router.get(/^(?!\/api\/)/, controller.home.index);
複製代碼

如此依照開發和生產環境配置,可以實現基本的服務端渲染。篇幅有限,大段代碼暫時沒有貼出,後續會開放源代碼示例。

相關文章
相關標籤/搜索