toxic-johann / toxic-johann.github.io

my blog
6 stars 0 forks source link

【2016-11-02】thinkjs 和 vuejs 2.1.x 版本的 ssr 问题 #31

Open toxic-johann opened 7 years ago

toxic-johann commented 7 years ago

你有没有SSR

最近在开发vue+thinkjs的项目,经常被人问这个问题,所以今天我们来一本正经地谈一下我的SSR经历。

SSR全称server-side render,即服务端渲染。

首先我们要先回答一个问题。

为什么需要SSR

在我们未使用框架前,我们编写HTML和JavaScript文件。按照正常的流式加载顺序,CSS和HTML会首先被加载。那么这保证了,哪怕JavaScript没有加载完毕,用户也能阅读到完整的页面。而在老式浏览器或者JavaScript被禁止的情况下,用户也能看到较为正常的页面(当然交互可能不太正常)。而搜索引擎也能根据我们生成的HTML文件知道我们的网页是什么内容的。

当我们使用框架后,问题来了。

框架的本质是前端JavaScript渲染。

框架流程

一般来说我们所谓的模板是这样的。

使用框架后的HTML文件

一旦JavaScript加载速度较慢或者失败,我们看到的页面会是这样子的。

JavaScript被禁止后的页面

那么自然就更谈不上SEO了。

SSR是干什么的呢?

纵然前端框架日新月异,但是现阶段真正能够做到SSR的框架只有具有virtual-dom的框架,那自然就是React和Vue.js了。所谓SSR,就是将框架的代码在服务端环境下(一般是node环境)生成为HTML片段代码。于是加载流程如下图。

服务端渲染后)

这就保证了用户所加载的HTML文件是完整的目标HTML文件,大大改进了首屏体验和SEO效果。

怎么进行SSR

Vue.js提供了两种模式的SSR,一种是renderer,另一种是bundleRenderer。在前端工程化流行的今天,我们开发的时候都喜欢用webpack进行处理,所以我毫不犹豫的选择了bundleRenderer。

bunderRender原理

接着我们就可以按照npm上的说明开始操作。

首先在你的wepback配置上添加target: 'node'`output: { libraryTarget: 'commonjs2' } `来把你的代码编译成可用于后端加载的代码。

然后将你的代码改造为相应的入口文件。

// server-entry.js 
// 引入你的Vue应用
import MountVessel from './index_index/main.vue'
// 生成vue应用实例
const app = new Vue(MountVessel)
// 返回一个包裹好的promise,如果需要填数据,则在你的根组件上添加填数据方法并调用之
export default context => {
  return app.preFetch(context).then(() => {
    return app
  })
}

例如我的根组件的就有如下方法

export default {
  methods: {
    // 取数据
    preFetch (context) {
      return new Promise((resolve, reject) => {
          // 填数据=-=
          this.preSet(context)
          resolve()
        }
      })
    }
}

然后呢,因为我是用thinkjs,所以我就在相应的action添加渲染代码。

// 获取代码文件路径
const filePath = path.join(__dirname,'xx.js')
// 读取文件
const code = fs.readFileSync(usercenterPath, 'utf8')
// 生成一个bundle renderer
const bundleRenderer = require('vue-server-renderer').createBundleRenderer(code)
// 渲染之,data里面是你需要提供的数据
bundleRenderer.renderToString(data, (err, html) => {
  // 将得到的HTML片段拼接到模板上
  this.assign({html})
})

是不是很简单?

于是你打开页面,百分之九十的情况下,你会发现报错了……

而这些错误通常是“window is undefined”,“document is undefined”等等。

这是因为,我们的代码是在node环境下进行处理的,而在node环境中并没有像window、document等各种浏览器才有的对象。所以我们要进行相应的处理。

一般来说,我们只需要处理在SSR过程中才调用的函数即可。

这些函数包括beforeCreate、created、data、computed,可以再官网查阅。

如果你是使用es6的import/export写法,还有部分仅前端可用的插件也会报错。

例如jQuery,你可以更改成以下方法引入:

if(inBrowser) {
  let jQuery = require('jquery')
}

当你包裹完毕后,再访问相应的页面。你应该能看到你想要的HTML了。

服务端渲染后的页面

网上的教程一般到这里就结束了。

然后你就懵逼了。

那不就只是纯HTML咯?我的酷炫的JavaScript交互呢!

是的,所以我们还要进行一次客户端渲染。有的教程会建议你写多个客户端入口文件,其实不必,我们只要将上方的server-entry.js改一下就好鸟。

// entry.js 
// 引入你的Vue应用
import MountVessel from './index_index/main.vue'
// 生成vue应用实例
const app = new Vue(MountVessel)
// 如果是在浏览器下,进行客户端渲染
if(inBrowser) {
  app.preFetch().then(() => {
    app.$mount('[server-rendered="true"]')
  })
}
// 返回一个包裹好的promise,如果需要填数据,则在你的根组件上添加填数据方法并调用之
export default context => {
  return app.preFetch(context).then(() => {
    return app
  })
}

客户端正常渲染都会在根组件处添加server-rendered属性,我们只要找寻这个属性将客户端实例挂载在上方就好了。

那为什么我们前端也要进行preFetch呢?

因为我们要保证前端的首屏和服务端渲染出来的结构是一致的,否则就会重新渲染不一致的部分。照样子会造成闪烁的效果。

所以我的preFetch方法一般是这么写的

preFetch (context) {
  return new Promise((resolve, reject) => {
    if(inBrowser) {
      // 前端使用,主动拉取相关的信息然后填充
      Promise.all(getDataA(), getDataB()])
      .then(([dataA, dataB] = []) =>{
        this.preSet({dataA, dataB})
        resolve()
      }, fail => {
        resolve()
      })
    } else if(context) {
      // 后端使用,将获得的数据自动填充
      this.preSet(context)
      resolve()
    }
  })
}

第一次好慢怎么办

当我们都弄完之后,我们再次测试。

忽然发现,咦,怎么加载好像很慢。

然后刷新一下,发现呀,快了好多。

这是因为第一次时要生成完整的HTML片段代码,而第二次可以把第一次生成后的HTML碎片代码直接拼接即可。

那么就意味着,第一个访问的客户会很慢!

这当然是不行的,咱们要每个客户平等对待,所以我们在thinkjs里面先渲染一次即可。

这时候打开thinks的src/common/bootstrap/global.js,添加如下代码。

// 获取代码文件路径
const filePath = path.join(__dirname,'xx.js')
// 读取文件
const code = fs.readFileSync(usercenterPath, 'utf8')
// 生成一个bundle renderer
global.bundleRenderer = require('vue-server-renderer').createBundleRenderer(code)
// 渲染之,data里面是你需要提供的数据
global.bundleRenderer.renderToString(data, (err, html) => {
  // 将得到的HTML片段拼接到模板上
  this.assign({html})
})

那么我们在action里面也相应的使用global进行渲染即可。