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);
复制代码
如此依照开发和生产环境配置,可以实现基本的服务端渲染。篇幅有限,大段代码暂时没有贴出,后续会开放源代码示例。