Vue.js 是構建客戶端應用程序的框架,可是也能夠將同一個組件渲染爲服務端的 HTML 字符串,將它們直接發送到瀏覽器,最後將這些靜態標記「激活」爲客戶端上徹底可交互的應用程序。
服務器渲染的 Vue.js 應用程序也能夠叫作「同構」或「通用」,程序上的大部分代碼均可以在服務器和客戶端上運行。html
與傳統 SPA 相比,SSR 的主要優點在於:vue
須要注意的是:node
beforeCreate
和created
鉤子函數被調用,在這兩個生命週期函數中應該避免產生全局反作用的代碼,例如在其中使用setInterval
設置 timer。若是確實須要服務端渲染,那麼能夠繼續看下面的用法。webpack
npm install vue vue-server-renderer --save
複製代碼
注意:web
vue-servier-renderer
和vue
必須匹配版本對於客戶端應用程序和服務器應用程序,都須要使用 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
函數。相似地 store
和router
都須要導出這樣的工廠函數: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);
});
};
複製代碼
配置文件結構像這樣:
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",
},
複製代碼
開發服務使用的是 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...`);
});
複製代碼
線上服務使用的是 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);
複製代碼
如此依照開發和生產環境配置,可以實現基本的服務端渲染。篇幅有限,大段代碼暫時沒有貼出,後續會開放源代碼示例。