使用 Vue.js 构建客户端应用程序时,默认状况下是在浏览器中输出 Vue 组件,进行生成 DOM 和操做 DOM。而使用 SSR 能够将同一个组件渲染为服务器端的 HTML 字符串,而后将它们直接发送到浏览器,最后将静态标记"混合"为客户端上彻底交互的应用程序。css
简单的方式是在 Chrome 浏览器打开控制台/开发者工具,查看 Network 中加载的资源,以下图所示 segmentfault 网站,能够看到第一个文件老是 document 类型,这是服务器发送过来的完整的 HTML 文档,浏览器只须要加载 css/js 进行视图渲染便可。
看 Vue SSR 官网也是服务器端渲染:html
这里说的渲染,就是指生成 HTML 文档的过程,和以前浏览器的 CSS+HTML 渲染没有关系。简单来讲,浏览器端渲染,指的是用 JS 去生成 HTML,例如 React, Vue 等前端框架作的路由。服务器端渲染,指的是用后台语言经过一些模版引擎生成 HTML,例如 Java 配合 VM 模版引擎、NodeJS配合 Jade 等,将数据与视图整理输出为完整的 HTML 文档发送给浏览器。前端
新建项目,安装 Vue 与 SSR 依赖包 vue-server-renderervue
$ mkdir testSSR // 新建空文件夹 vueSSR $ cd testSSR // 进入 vueSSR 目录 $ npm init // 初始化,生成 package.json $ npm install vue vue-server-renderer --save-dev // 安装
先使用 vue-server-renderer 渲染一个简单的 Vue 组件node
$ touch test.js // 新建 test.js
// test.js const Vue = require('vue') const app = new Vue({ // 建立一个 Vue 实例 template: `<div>Hello World</div>` }) const vueRenderer = require('vue-server-renderer') const renderer = vueRenderer.createRenderer() // 建立一个 renderer // 经过 renderToString 将 Vue 实例渲染为 HTML // 函数签名: renderer.renderToString(vm, context?, callback?): ?Promise<string> renderer.renderToString(app, (err, doc) => { if (err) throw err console.log(doc) })
运行 test.js,输出渲染后的 HTMLwebpack
注意到应用程序的根元素上添加了一个特殊的属性 data-server-rendered,这是让客户端 Vue 知道这部分 HTML 是由 Vue 在服务端渲染的。git
上例只是渲染一个 vue 组件,一般应用程序都会抽象出一个或多个模板来嵌入不一样的组件。github
Render 的 template 选项为整个页面的 HTML 提供一个模板。此模板应包含注释 <!--vue-ssr-outlet-->,做为渲染应用程序内容的占位符。web
首先建立一个 HTML 模板 index.template.htmlvue-router
<!--index.template.html --> <!doctype html> <html lang="en"> <head><title></title></head> <body> <!--vue-ssr-outlet--> </body> </html>
这里的 <!--vue-ssr-outlet--> 注释就是应用程序 HTML 标记注入的地方。
将此模板经过 fs 读取, 而后在 createRenderer( ) 时注入,修改 test.js 以下:
// test.js //const renderer = vueRenderer.createRenderer() const fs = require('fs') const renderer = vueRenderer.createRenderer({ template: fs.readFileSync('./index.template.html', 'utf-8') // 同步读取文件 })
运行 test.js 能够看到以前定义的 hello world 组件已嵌入模板中。
选取基于 node.js 的 express 做为服务器,示例 vue ssr 在服务器端的工做。
$ cd testSSR // 进入项目 $ npm install express --save-dev // 安装 express $ touch server.js // 新建 server.js
引入 express 并设置一个测试路由
// server.js const express = require('express') const server = express() server.get('/mytest', (request, response) => { response.send("hello world "+request.url) }) server.listen(8000)
运行$ node server.js 后打开浏览器访问 http://localhost:8000/mytest
服务器启动成功。
首先建立一个能够重复执行的工厂函数,为每一个请求建立新的 Vue 实例,若是建立一个单例对象,它将在每一个传入的请求之间共享,很容易致使交叉请求状态污染。
$ cd testSSR // 进入项目 $ touch app.js // 新建 app.js
// app.js const Vue = require('vue') module.exports = function createApp (context) { return new Vue({ data: { url: context.url }, template: `<div>Vue SSR URL: {{ url }}</div>` }) }
而后在 server.js 中引入 app.js 建立实例,并配置路由与请求渲染。
// server.js const createApp = require('./app') const vueRenderer = require('vue-server-renderer') const renderer = vueRenderer.createRenderer() server.get('/ssr', (request, response) => { const context = { url: request.url } const app = createApp(context) renderer.renderToString(app, (err, doc) => { if (err) throw err response.send(doc) }) })
运行$ node server.js 后打开浏览器访问 http://localhost:8000/ssr?sadas=2222
增长页面模板,使用以前定义的 index.template.html 做为模板,注入到一个新的 renderer
// server.js const fs = require('fs') const rendererTmp = vueRenderer.createRenderer({ template: fs.readFileSync('./index.template.html', 'utf-8') // 同步读取文件 }) server.get('/template', (request, response) => { const context = { url: request.url } const app = createApp(context) rendererTmp.renderToString(app, (err, doc) => { if (err) throw err response.send(doc) }) })
运行$ node server.js 后打开浏览器访问 http://localhost:8000/template
能够看到一个简单的服务器端渲染已经完成。
一般 Vue 应用程序是由 webpack 和 vue-loader 构建,而且许多 webpack 特定功能不能直接在 Node.js 中运行(例如经过 file-loader 导入文件,经过 css-loader 导入 CSS)。
对于客户端应用程序和服务器应用程序,咱们都要使用 webpack 打包 - 服务器须要「服务器 bundle」而后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。基本流程以下图。
因此一个基本的项目目录可能以下:
src ├── config │ ├── webpack.base.config.js │ ├── webpack.client.config.js │ └── webpack.server.config.js ├── components │ ├── Foo.vue │ └── xxx.vue ├── build │ ├── index.js │ └── xxx.js ├── template │ ├── index.template.html │ └── xxx.html ├── route.js # vue-router 路由 ├── App.vue # 根实例 ├── app.js # 通用 entry ├── entry-client.js # 配置 仅运行于浏览器 ├── entry-server.js # 配置 仅运行于服务器 ├── server.js # 服务器 ├── webpack.config.js └── package.json
使用 vue-router
$ npm intall vue-router --save-dev $ touch route.js
在新建的 router.js 中建立 router,相似于 createApp,咱们也须要给每一个请求一个新的 router 实例,因此文件导出一个 createRouter 函数
// router.js import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) export function createRouter () { return new Router({ mode: 'history', routes: [ // ... ] }) }
修改 app.js,添加路由
// app.js import Vue from 'vue' import App from './App.vue' import { createRouter } from './router' export function createApp () { // 建立 router 实例 const router = createRouter() const app = new Vue({ // 注入 router 到根 Vue 实例 router, render: h => h(App) }) // 返回 app 和 router return { app, router } }
新建 entry-server.js,实现服务器端路由逻辑:
// entry-server.js import { createApp } from './app' export default context => { // 由于有可能会是异步路由钩子函数或组件,因此咱们将返回一个 Promise, // 以便服务器可以等待全部的内容在渲染前, // 就已经准备就绪。 return new Promise((resolve, reject) => { const { app, router } = createApp() // 设置服务器端 router 的位置 router.push(context.url) // 等到 router 将可能的异步组件和钩子函数解析完 router.onReady(() => { const matchedComponents = router.getMatchedComponents() // 匹配不到的路由,执行 reject 函数,并返回 404 if (!matchedComponents.length) { return reject({ code: 404 }) } // Promise 应该 resolve 应用程序实例,以便它能够渲染 resolve(app) }, reject) }) }
服务器端配置 webpack.server.config.js,是用于生成传递给 createBundleRenderer 的 server bundle
// webpack.server.config.js const merge = require('webpack-merge') const nodeExternals = require('webpack-node-externals') const baseConfig = require('./webpack.base.config.js') const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') module.exports = merge(baseConfig, { // 将 entry 指向应用程序的 server entry 文件 entry: '/path/to/entry-server.js', // 这容许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import), // 而且还会在编译 Vue 组件时, // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。 target: 'node', // 对 bundle renderer 提供 source map 支持 devtool: 'source-map', // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports) output: { libraryTarget: 'commonjs2' }, // https://webpack.js.org/configuration/externals/#function // https://github.com/liady/webpack-node-externals // 外置化应用程序依赖模块。可使服务器构建速度更快, // 并生成较小的 bundle 文件。 externals: nodeExternals({ // 不要外置化 webpack 须要处理的依赖模块。 // 你能够在这里添加更多的文件类型。例如,未处理 *.vue 原始文件, // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单 whitelist: /\.css$/ }), // 这是将服务器的整个输出 // 构建为单个 JSON 文件的插件。 // 默认文件名为 `vue-ssr-server-bundle.json` plugins: [ new VueSSRServerPlugin() ] })
在生成 vue-ssr-server-bundle.json 以后,只需将文件路径传递给 createBundleRenderer:
// server.js const { createBundleRenderer } = require('vue-server-renderer') const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', { // ……renderer 的其余选项 })
除了 server bundle 以外,咱们还能够生成客户端构建清单(client build manifest)。使用客户端清单(client manifest)和服务器 bundle(server bundle),renderer 如今具备了服务器和客户端的构建信息,所以它能够自动推断和注入资源预加载 / 数据预取指令(preload / prefetch directive),以及 css 连接 / script 标签到所渲染的 HTML。
// webpack.client.config.js const webpack = require('webpack') const merge = require('webpack-merge') const baseConfig = require('./webpack.base.config.js') const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') module.exports = merge(baseConfig, { entry: '/path/to/entry-client.js', plugins: [ // 重要信息:这将 webpack 运行时分离到一个引导 chunk 中, // 以即可以在以后正确注入异步 chunk。 // 这也为你的 应用程序/vendor 代码提供了更好的缓存。 new webpack.optimize.CommonsChunkPlugin({ name: "manifest", minChunks: Infinity }), // 此插件在输出目录中 // 生成 `vue-ssr-client-manifest.json`。 new VueSSRClientPlugin() ] })
这样就能够生成客户端构建清单(client build manifest)。
到目前为止,咱们假设打包的服务器端代码,将由服务器经过 require 直接使用:
const createApp = require('/path/to/built-server-bundle.js')
然而在每次编辑过应用程序源代码以后,都必须中止并重启服务。这在开发过程当中会影响开发效率。此外,Node.js 自己不支持 source map。
vue-server-renderer 提供一个名为 createBundleRenderer 的 API,用于处理此问题,经过使用 webpack 的自定义插件,server bundle 将生成为可传递到 bundle renderer 的特殊 JSON 文件。
// server.js const { createBundleRenderer } = require('vue-server-renderer') const template = require('fs').readFileSync('/path/to/template.html', 'utf-8') const serverBundle = require('/path/to/vue-ssr-server-bundle.json') const clientManifest = require('/path/to/vue-ssr-client-manifest.json') const renderer = createBundleRenderer(serverBundle, { runInNewContext: false, // 推荐 template, // (可选)页面模板 clientManifest // (可选)客户端构建 manifest }) // 在服务器处理函数中…… server.get('/', (req, res) => { const context = { url: req.url } // 这里无需传入一个应用程序,由于在执行 bundle 时已经自动建立过。 // 如今咱们的服务器与应用程序已经解耦! renderer.renderToString(context, (err, html) => { // 处理异常…… res.end(html) }) })
此外,vue SSR 提供 css 管理、缓存管理、流式渲染等,期待之后继续整理。
Vue SSR 指南:https://ssr.vuejs.org/zh/
API 参考:https://ssr.vuejs.org/zh/api/