前端技术发展到今天,用户通过终端浏览到应用内容的方式一般包含两种:
- 客户端渲染(CSR):客户端请求数据及 JS 内容,在客户端渲染成标准的页面呈现出来
- 服务端渲染(SSR):服务端从本地拿到数据,然后先渲染好页面再发送到客户端呈现给用户
Vue.js 默认在客户端渲染页面,但是我们可以通过 vue-server-renderer 实现服务端渲染。 用流程图的方式描述二则的区别如下:
CSR 与 SSR 是页面渲染的两种方式,二则各有利弊:
客户端渲染(CSR:Client Side Render)
客户端渲染需要先从服务器拿到数据(主要是些 JS 文件),等这些必要的文件全部加载完后才会渲染页面,比如 Vue.js 中会先拿到一个简单的 html 文件,然后获取必要的 js 文件,通过 js 文件生成虚拟 DOM 用来展示页面。因此客户端渲染往往具有如下特点:
- 初始加载慢,白屏时间长
- 无 DOM 结构,不利于 SEO
- 利于前后端分离,前端专注页面实现,后端专注于 API 开发
- 用户体验好, 方便应用交互
- 服务器压力小,页面渲染工作在客户端完成
服务端渲染(SSR:Server Side Render)
服务端渲染一般从本地拿到数据,根据数据渲染成前端可用的 html 页面,将页面发送给客户端快速显示,因此服务端渲染往往具有如下特点:
- 服务器压力大,特别是对 Vue.js 需要根据客户请求创建独立的 vue 实例,防止数据污染
- 用户发生交互行为后可能会频繁请求服务器
- 不利于前后端分离,开发效率低,开发受限
- 有利于 SEO,方便爬虫抓取数据
- 首屏加载迅速,避免长时间白屏造成用户体验不好
- 后端可生成静态缓存文件,避免大量数据库查询
先放两张图,对比下客户端渲染与服务端渲染,浏览器请求信息中的差异:
Vue 代码改造
Vue.js 是构建客户端应用程序的框架。默认情况下创建的应用程序是在浏览器中渲染并生成 DOM 和操作 DOM 的。也就是说,默认的 Vue 应用是客户端渲染(CSR)的,如果我们想让其在服务端渲染(SSR)就需要对其进行简单的改造。
为了避免服务端渲染造成的数据污染,我们需要通过工厂函数为每一个发起请求的用户创建独立的路由、状态管理、Vue实例。
createRouter
客户端渲染方式创建路由:
// 直接在客户端 new 出一个 Router 实例 export default new VueRouter({ linkActiveClass: 'router-link-active', // 当包含父路由的子路由被激活时,父路由的样式类名 linkExactActiveClass: 'router-link-exact-active', // 当前路由的样式类名 routes // 路由配置项(数组) })
服务端渲染创建路由:
// 使用工厂函数,防止数据污染,每次客户端请求都需要服务端创建一个路由实例 export function createRouter() { return new Router({ mode: 'history', linkActiveClass: 'router-link-active', // 当包含父路由的子路由被激活时,父路由的样式类名 linkExactActiveClass: 'router-link-exact-active', // 当前路由的样式类名 routes // 路由配置项(数组) }) }
createStore
客户端渲染方式创建状态管理器:
// 直接在客户端 new 出一个 Store 实例 export default new Vuex.Store({ modules:{ base, // 其他模块的状态管理... } })
服务端渲染方式创建状态管理器:
// 使用工厂函数,防止数据污染,每次客户端请求都需要服务端创建一个路由实例 export function createStore() { return new Vuex.Store({ modules:{ base, // 其他模块的状态管理... } }) }
createApp
创建一个公共的文件 app.js ,方便客户端及服务端分别编译时调用。注意这里为方便区分没有直接改造 main.js。
客户端渲染方式创建 Vue 实例:
// 直接在客户端 new 出一个 Vue 实例 new Vue({ router, store, render: h => h(App) }).$mount('#app') // 直接绑定 #app 的 DOM
服务端渲染方式创建 Vue 实例:
// 按需加载 createRouter import { createRouter } from './router/index.js' // 按需加载 createStore import { createStore } from './store/index.js' // 使用工厂函数,客户端请求时创建独立的 Vue 实例,防止数据污染 export function createApp() { const router = createRouter(); // 创建路由 const store = createStore(); // 创建状态管理器 const app = new Vue({ // 创建 Vue 实例 router, store, render: h => h(App), }); return { app, router, store}; }
entry-client.js
客户端编译时的入口文件,引入公共文件 app.js 中的代码后挂载实例。
注意:需要在 router.onReady 后挂载实例。
// 按需加载 createApp import { createApp } from './app'; // 按需加载 app router const {app, router} = createApp(); // 挂载真正的实例 router.onReady(() => { app.$mount('#app') })
entry-server.js
服务端编译时的入口文件,引入公共文件 app.js 中的代码后
// 按需加载 createApp import { createApp } from './app'; // context 请求上下文 export default context => { return new Promise((resolve, reject) => { const {app, router} = createApp(); // 进入首屏 router.push(context.url) router.onReady(() => { resolve(app); }, reject) }) };
vue.config.js 改造
服务器端打包 JSON 数据,客户端打包静态文件,用于挂载实例。改造代码如下:
// webpack 插件 const VueSSRServerPlugin = require("vue-server-renderer/server-plugin"); const VueSSRClientPlugin = require("vue-server-renderer/client-plugin"); const nodeExrternals = require("webpack-node-externals"); const merge = require("lodash.merge"); const TARGET_NODE = process.env.WEBPACK_TARGET === "node"; const target = TARGET_NODE ? "server" : "client"; module.exports = { outputDir: './dist/'+target, configureWebpack: () => ({ // 将 entry 指向应用程序的 server / client 文件 entry: `./src/entry-${target}.js`, // 对 bundle renderer 提供 source map 支持 devtool: 'source-map', // 这允许 webpack 以 Node 使用方式处理动态导入(dynamic import) // 并且还会在编译 Vue 组件时告知 'vue-loader' 输送面向服务器代码(server-oriented code) target: TARGET_NODE ? "node" : "web", node: TARGET_NODE ? undefined : false, output: { // 此处告知 server bundle 使用 Node 风格导出模块 libraryTarget: TARGET_NODE ? "commonjs2" : undefined }, // 外置化应用程序依赖模块。可以使用服务器构建速度更快,并生成较小的 bundle 文件 externals: TARGET_NODE ? nodeExrternals({ // 不要外置化 webpack 需要处理的依赖模块 // 可以再这里添加更多的文件类型,例如:未处理 *.vue 原始文件 // 你还应该将修改 'global' (例如 polyfill)的依赖模块列入白名单 allowlist: [/\.css$/] }) : undefined, optimization: { // 为vue-cli3配置了webpack4 里面的分包功能 splitChunks: TARGET_NODE ? false : undefined }, // 这是将服务器的整个输出构建为单个 JSON 文件的插件 // 服务器默认文件名为 'vue-ssr-server-bundle.json' plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()] }) }
package.json 改造
将 package.json 中的 scripts 内容改成如下内容,这样就可以直接运行 npm run build 打包程序了。
"scripts": { "build": "npm run build:server && npm run build:client", "build:client": "vue-cli-service build", "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server" },
服务器端渲染
要使用 vue-server-renderer 在服务器端渲染页面,我们需要先对其进行部署,这里以 node.js 服务器为例,使用 express 快速搭建服务器。
安装 vue-server-renderer
npm install vue vue-server-renderer --save
注意:vue-server-renderer 和 vue 必须匹配版本,且需要 node.js 环境支持
API 参考
因为之后的部署过程中可能用到相关 API ,这里先简单介绍一下。
- createRenderer:使用(可选的)选项创建一个 Renderer 实例
const renderer = createRenderer({ /* 选项 */ })
- createBundleRenderer:使用 server bundle 和(可选的)选项创建一个 BundleRenderer 实例
const renderer = createBundleRenderer(serverBundle, { /* 选项 */ })
- Class: Renderer
- renderer.renderToString:将 Vue 实例渲染为字符串。上下文对象(context)可选。回调函数是的第一个参数是可能抛出的错误,第二个参数是渲染完毕的字符串。2.5.0+中,回调可选,默认返回 Promise 对象。
renderer.renderToString(vm, context?, callback?): ?Promise<string>
- renderer.renderToStream:将 Vue 实例渲染为一个 Node.js 可读流。上下文对象(context)可选。
renderer.renderToStream(vm[, context]): stream.Readable
- Class: BundleRenderer
- bundleRenderer.renderToString:将 bundle 渲染为字符串。上下文对象(context)可选。回调的第一个参数是可能抛出的错误,第二个参数是渲染完毕的字符串。2.5.0+中,回调可选,默认返回 Promise 对象。
bundleRenderer.renderToString([context, callback]): ?Promise<string>
- bundleRenderer.renderToStream:将 bundle 渲染为一个 Node.js 可读流。上下文对象(context)可选。
bundleRenderer.renderToStream([context]): stream.Readable
- Renderer 选项
- template:为整个页面的 HTML 提供一个模板。
- clientManifest:通过此选项提供一个由 vue-server-renderer/client-plugin 生成的客户端构建 manifest 对象。
- inject:控制使用 template 时是否执行自动注入。
- shouldPreload:一个函数,用来控制什么文件应该生成 <link rel=”preload”> 资源预加载提示 (resource hints)。
- shouldPrefetch:一个函数,用来控制对于哪些文件,是需要生成 <link rel=”prefetch”> 资源提示。
- runInNewContext:只用于 createBundleRenderer,是否每次渲染都创建一个新的 V8 上下文并重新执行整个 bundle。
- basedir:只用于 createBundleRenderer,显式地声明 server bundle 的运行目录。运行时将会以此目录为基准来解析 node_modules 中的依赖模块。
- cache:提供组件缓存具体实现。
- directives:对于自定义指令,允许提供服务器端实现。
server/index.js 代码
npm 安装
// Node.js 服务器 —— Express // 引入 express 并创建实例 const express = require("express"); const server = express(); // 使用 fs 插件请求静态文件 const fs = require("fs"); // 引入 vue-server-renderer 并创建渲染器 const vueRenderer = require("vue-server-renderer"); const serverBundle = require('../dist/server/vue-ssr-server-bundle.json'); const clientManifest = require('../dist/client/vue-ssr-client-manifest.json'); const renderer = vueRenderer.createBundleRenderer(serverBundle, { runInNewContext: false, template: fs.readFileSync('../public/index.template.html', 'utf-8'), clientManifest }); // 中间件处理静态文件请求 server.use(express.static('../dist/client', {index: false})) // 监听服务器请求 server.get("*", async (req, res) => { try { // 定义请求上下文信息,服务器渲染的时候会用到此处的一些参数 const context = { url: req.url, title: 'SSR Advanced Test' } // 渲染成 HTML 文档 const html = await renderer.renderToString(context) console.log(html) // 返回给用户 HTML 结构 res.send(html) } catch (error) { console.log(error) res.status(500).send("服务器内部错误") } }); server.listen(3000, ()=>{ console.log("服务器启动成功...") });
评论 抢沙发