什么是服务端渲染(SSR)?
Vue.js 是构建客户端应用程序的框架,但是也可以将同一个组件渲染为服务端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记“激活”为客户端上完全可交互的应用程序。
服务器渲染的 Vue.js 应用程序也可以叫做“同构”或“通用”,程序上的大部分代码都可以在服务器和客户端上运行。
是否需要服务器渲染?
与传统 SPA 相比,SSR 的主要优势在于:
- 更好的 SEO
- 更快的内容到达时间(time-to-content)
需要注意的是:
- 开发条件有限,由于没有动态更新,服务端渲染过程中,只有
beforeCreate
和created
钩子函数被调用,在这两个生命周期函数中应该避免产生全局副作用的代码,例如在其中使用setInterval
设置 timer。 - 涉及构建设置和部署的更多要求。服务器渲染应用程序,需要处于 Node.js server 运行环境。
- 更多的服务器端负载。如果预料到在高流量环境下使用,需要准备相应的服务器负载,并明智地采用缓存策略。
如果确实需要服务端渲染,那么可以继续看下面的用法。
基本用法
安装
npm install vue vue-server-renderer --save
复制代码
注意:
- Node.js 版本 6+
vue-servier-renderer
和vue
必须匹配版本
构建步骤
1. 建立入口文件
对于客户端应用程序和服务器应用程序,都需要使用 webpack 打包两个 Bundle,服务器需要 Server Bundle 用于服务器渲染,Client Bundle 会发送给浏览器,用于混合静态标记。
一个基本项目像这样:
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
都需要导出这样的工厂函数:
# 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);
复制代码
如此依照开发和生产环境配置,能够实现基本的服务端渲染。篇幅有限,大段代码暂时没有贴出,后续会开放源代码示例。