博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
vue服务器端渲染(SSR)实战
阅读量:6880 次
发布时间:2019-06-27

本文共 7905 字,大约阅读时间需要 26 分钟。

什么是服务端渲染(SSR)?

SSR(Server-Side Rendering),在SPA(Single-Page Application)出现之前,网页就是在服务端渲染的。服务器接收到客户端请求后,将数据和模板拼接成完整的页面响应到客户端,客户端将响应结果渲染出来。如果用户需要浏览新的页面,则需要重复这个过程。随着Angular、React和Vue的兴起,SPA开始流行,单页面应用可以在不重载整个页面的情况下,通过ajax和服务器进行交互,高效更新部分页面,这无疑带来了良好的用户体验。然而,对于需要SEO、追求首屏速度的页面,使用SPA是糟糕的。如果我们想使用Vue,又需要考虑到SEO、首屏渲染速度,那该怎么办?好在Vue是支持的,接下来我们主要说的是Vue的服务端渲染。

Vue SSR适用场景及解决的问题

我们主要在管理后台系统和内嵌H5电商页中使用Vue,对于管理后台系统,不需要考虑SEO和首屏渲染时间,所以是否用SPA的方式其实问题不大。而对于电商页,虽然不需要SEO,但是首屏渲染变得十分重要。一般的SPA页面打开时,HTML大体的结构如下:

  
复制代码

这种情况下,HTML和JS加载成功后通过JS再发起请求,再将响应的内容填入到div容器中,这就存在页面最开始白屏的问题。服务端渲染将这个过程放在了服务端,请求获取响应后服务端将HTML填充好直接返回给浏览器,浏览器将整个完整的HTML直接渲染出来。显而易见,服务端渲染少了在浏览器加载的过程,解决了页面最开始白屏的问题,明显的提高了首屏渲染的速度。

目前我们主要在电商导购页、挖客分享页中使用Vue的SSR,接下来我们主要讲SSR的实现。

实现原理

实现流程

如上图所示有两个入口文件Server entry和Client entry,分别经webpack打包成服务端用的Server Bundle和客户端用的Client Bundle。

服务端:当Node Server收到来自客户端的请求后, BundleRenderer 会读取Server Bundle,并且执行它,而 Server Bundle实现了数据预取并将填充数据的Vue实例挂载在HTML模版上,接下来BundleRenderer将HTML渲染为字符串,最后将完整的HTML返回给客户端。

客户端:浏览器收到HTML后,客户端加载了Client Bundle,通过app.$mount('#app')的方式将Vue实例挂载在服务端返回的静态HTML上。如:

复制代码

data-server-rendered 特殊属性,让客户端 Vue 知道这部分 HTML 是由 Vue 在服务端渲染的,并且应该以激活模式()进行挂载。

目录结构

.├── build│   ├── setup-dev-server.js          # dev服务器端设置 增加中间件支持│   ├── webpack.base.config.js       # 基本配置│   ├── webpack.client.config.js     # 客户端配置│   └── webpack.server.config.js     # 服务端配置├── cache_key.js                     # 根据参数判断是否从缓存中获取├── package.json                     # 项目依赖├── process.debug.json               # debug环境下的pm2配置文件├── process.json                     # 生产环境下pm2配置文件├── server.js                        # express 服务端入口文件├── src│   ├── api│   │   ├── create-api-client.js	 # 客户端请求相关配置│   │   ├── create-api-server.js	 # 服务器请求相关配置│   │   └── index.js                 # api请求│   ├── app.js                       # 主入口文件│   ├── config                       # 相关配置│   ├── entry-client.js              # 客户端入口文件│   ├── entry-server.js              # 服务端入口文件│   ├── router                       # 路由│   ├── store                        # store│   ├── templates                    # 模版│   └── views复制代码

相关文件

server.js

// 创建express应用const app = express()// 读取模版文件const template = fs.readFileSync(resolve('./src/templates/index.template.html'), 'utf-8')// 调用vue-server-renderer的createBundleRenderer方法创建渲染器,并设置HTML模板,之后将服务端预取的数据填充至模板中function createRenderer (bundle, options) {  return createBundleRenderer(bundle, Object.assign(options, {    template,	 basedir: resolve('./dist'),    runInNewContext: false  }))}let rendererlet readyPromiseif (!isDev) {  // 生产环境下,引入由webpack vue-ssr-webpack-plugin插件生成的server bundle  const bundle = require('./dist/vue-ssr-server-bundle.json')  // 引入由 vue-server-renderer/client-plugin 生成的客户端构建 manifest 对象。此对象包含了 webpack 整个构建过程的信息,从而可以让 bundle renderer 自动推导需要在 HTML 模板中注入的内容。  const clientManifest = require('./dist/vue-ssr-client-manifest.json')  // vue-server-renderer创建bundle渲染器并绑定server bundle  renderer = createRenderer(bundle, {    clientManifest  })} else {  // 开发环境下,使用dev-server来通过回调把内存中的bundle文件取回  // 通过dev server的webpack-dev-middleware和webpack-hot-middleware实现客户端代码的热更新  readyPromise = require('./build/setup-dev-server')(app, (bundle, options) => {    renderer = createRenderer(bundle, options)  })}// 设置静态资源访问const serve = (path, cache) => express.static(resolve(path), {  maxAge: cache && isDev ? 0 : 1000 * 60 * 60 * 24 * 30})// 相关中间件 压缩响应文件 处理静态资源等app.use(...)// 设置缓存时间const microCache = LRU({  maxAge: 1000 * 60 * 1})const isCacheable = req => useMicroCachefunction render (req, res) {  const s = Date.now()  res.setHeader('Content-Type', 'text/html')  // 错误处理  const handleError = err => {}  // 根据path和query获取cacheKey  let cacheKey = getCacheKey(req.path, req.query)  // 生产环境下默认开启缓存  const cacheable = isCacheable(req)  if (cacheable) {    const hit = microCache.get(cacheKey)    if (hit) {    // 从缓存中获取      console.log(`cache hit! key: ${cacheKey} query: ${
JSON.stringify(req.query)}`) return res.end(hit) } } // 设置请求的url const context = { title: '', url: req.url, } // 将Vue实例渲染为字符串,传入上下文对象。 renderer.renderToString(context, (err, html) => { if (err) { return handleError(err) } res.end(html) // 设置缓存 if (cacheable) { if (!isProd) { console.log(`set cache, key: ${cacheKey}`) } microCache.set(cacheKey, html) } if (!isProd) { console.log(`whole request: ${
Date.now() - s}ms`) } })}// 启动一个服务并监听8080端口app.get('*', !isDev ? render : (req, res) => { readyPromise.then(() => render(req, res))})const port = process.env.PORT || 8080const server = http.createServer(app)server.listen(port, () => { console.log(`server started at localhost:${port}`)})复制代码

整个流程大致如下:

  1. 创建渲染器,设置渲染模版、绑定Server Bundle
  2. 依次装载一系列Express中间件,用于压缩响应、处理静态资源等
  3. 渲染器将装载好的Vue的实例渲染为字符串,响应到客户端,并设置缓存(以cacheKey为标识)
  4. 再次访问时以cacheKey为标识,判断是否从缓存中获取

entry.server.js

import { createApp } from './app'export default context => {  return new Promise((resolve, reject) => {    const { app, router, store } = createApp()    const { url, req } = context    const fullPath = router.resolve(url).route.fullPath    if (fullPath !== url) {      return reject({ url: fullPath })    }	// 切换路由到请求的url    router.push(url)	 // 在路由完成初始导航时调用,可以解析所有的异步进入钩子和路由初始化相关联的异步组件,有效确保服务端渲染时服务端和客户端输出的一致。    router.onReady(() => {    // 获取该路由相匹配的Vue components      const matchedComponents = router.getMatchedComponents()      if (!matchedComponents.length) {        reject({ code: 404 })      }	 // 执行匹配组件中的asyncData      Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({        store,        route: router.currentRoute,        req      }))).then(() => {        // 在所有预取钩子(preFetch hook) resolve 后,        // 我们的 store 现在已经填充入渲染应用程序所需的状态。        // 当我们将状态附加到上下文,        // 并且 `template` 选项用于 renderer 时,        // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。		  context.state = store.state        if (router.currentRoute.meta) {          context.title = router.currentRoute.meta.title        }        // 返回一个初始化完整的Vue实例        resolve(app)      }).catch(reject)    }, reject)  })}复制代码

entry-client.js

import 'es6-promise/auto'import { createApp } from './app'const { app, router, store } = createApp()// 由于服务端渲染时,context.state 作为 window.__INITIAL_STATE__ 状态,自动嵌入到最终的 HTML 中。在客户端,在挂载到应用程序之前,state为window.__INITIAL_STATE__。if (window.__INITIAL_STATE__) {  store.replaceState(window.__INITIAL_STATE__)}router.onReady(() => {	// 添加路由钩子函数,用于处理 asyncData.	// 在初始路由 resolve 后执行,	// 以便我们不会二次预取(double-fetch)已有的数据。	// 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve。  router.beforeResolve((to, from, next) => {
const matched = router.getMatchedComponents(to) const prevMatched = router.getMatchedComponents(from) // 我们只关心之前没有渲染的组件 // 所以我们对比它们,找出两个匹配列表的差异组件 let diffed = false const activated = matched.filter((c, i) => { return diffed || (diffed = prevMatched[i] !== c) }) const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _) if (!asyncDataHooks.length) { return next() } Promise.all(asyncDataHooks.map(hook => hook({ store, route: to }))) .then(() => { next() }) .catch(next) }) // 挂载在DOM上 app.$mount('#app')})复制代码

遇到的问题

1. 本地存储

以往在使用SPA时,我们一般使用localStorage和sessionStorage进行部分信息的本地存储,有时候发起请求的时候需要带上这些信息。然而在使用SSR时,我们在asyncData这个钩子中发起请求获取数据,此时并不能获取到window对象下的localStorage这个对象。 我们将信息存储在cookie中,在asyncData获取数据时,通过req.headers获取cookie。

2. 避开服务端与浏览器差异

这个问题其实和第一个问题有些类似,服务端和浏览器最大的差别在于有无window对象。我们可以通过判断去避开:

// 解决移动端300ms延迟问题if (typeof window !== "undefined") {  const Fastclick = require('fastclick')  Fastclick.attach(document.body)}复制代码

其实更好的解决方式是在entry-client.js中:

import FastClick from 'fastclick'FastClick.attach(document.body)复制代码

3. not matching

[vue warn]The client-side rendered virtual DOM tree is not matching server-rendered content复制代码

这个问题是服务端与客户端渲染的HTML不一致导致的。很大可能是出现{

{ msg }}这样的写法中的多余空格导致的,我们要尽力避免在template中使用多余的空格。

转载地址:http://jybbl.baihongyu.com/

你可能感兴趣的文章
python 基础 9.4 游标
查看>>
es6 modules 和commonjs
查看>>
前后台交互
查看>>
LINQ&EF任我行(二)--LinQ to Object (转)
查看>>
Python之旅.第五章.面向对象
查看>>
Unity坐标系 左手坐标系 图
查看>>
python获取昨日日期
查看>>
13.1.2 拷贝赋值运算符、析构函数、三/五法则、阻止拷贝
查看>>
2013年蓝桥杯题目与解答
查看>>
HTML5仿微信公众号界面
查看>>
海康威视 - 萤石云开放平台 js 版
查看>>
关于分销平台
查看>>
剑指offer---12-**--数值的整数次方
查看>>
PAT - L2-010. 排座位(并查集)
查看>>
HDU - 5269【SBBBBBB Trie】
查看>>
sql server 日志文件结构及误操作数据找回
查看>>
JUnit 3一个例子就懂
查看>>
Mongodb相关 (Shell命令 / mongoose)
查看>>
Web API的Log问题
查看>>
leetcode Second Highest Salary
查看>>