Open toxic-johann opened 7 years ago
最近在开发vue+thinkjs的项目,经常被人问这个问题,所以今天我们来一本正经地谈一下我的SSR经历。
SSR全称server-side render,即服务端渲染。
首先我们要先回答一个问题。
在我们未使用框架前,我们编写HTML和JavaScript文件。按照正常的流式加载顺序,CSS和HTML会首先被加载。那么这保证了,哪怕JavaScript没有加载完毕,用户也能阅读到完整的页面。而在老式浏览器或者JavaScript被禁止的情况下,用户也能看到较为正常的页面(当然交互可能不太正常)。而搜索引擎也能根据我们生成的HTML文件知道我们的网页是什么内容的。
当我们使用框架后,问题来了。
框架的本质是前端JavaScript渲染。
一般来说我们所谓的模板是这样的。
一旦JavaScript加载速度较慢或者失败,我们看到的页面会是这样子的。
那么自然就更谈不上SEO了。
纵然前端框架日新月异,但是现阶段真正能够做到SSR的框架只有具有virtual-dom的框架,那自然就是React和Vue.js了。所谓SSR,就是将框架的代码在服务端环境下(一般是node环境)生成为HTML片段代码。于是加载流程如下图。
)
这就保证了用户所加载的HTML文件是完整的目标HTML文件,大大改进了首屏体验和SEO效果。
Vue.js提供了两种模式的SSR,一种是renderer,另一种是bundleRenderer。在前端工程化流行的今天,我们开发的时候都喜欢用webpack进行处理,所以我毫不犹豫的选择了bundleRenderer。
接着我们就可以按照npm上的说明开始操作。
首先在你的wepback配置上添加target: 'node'和`output: { libraryTarget: 'commonjs2' } `来把你的代码编译成可用于后端加载的代码。
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属性,我们只要找寻这个属性将客户端实例挂载在上方就好了。
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进行渲染即可。
最近在开发vue+thinkjs的项目,经常被人问这个问题,所以今天我们来一本正经地谈一下我的SSR经历。
SSR全称server-side render,即服务端渲染。
首先我们要先回答一个问题。
为什么需要SSR
在我们未使用框架前,我们编写HTML和JavaScript文件。按照正常的流式加载顺序,CSS和HTML会首先被加载。那么这保证了,哪怕JavaScript没有加载完毕,用户也能阅读到完整的页面。而在老式浏览器或者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。
接着我们就可以按照npm上的说明开始操作。
首先在你的wepback配置上添加
target: 'node'
和`output: { libraryTarget: 'commonjs2' }
`来把你的代码编译成可用于后端加载的代码。然后将你的代码改造为相应的入口文件。
例如我的根组件的就有如下方法
然后呢,因为我是用thinkjs,所以我就在相应的action添加渲染代码。
是不是很简单?
于是你打开页面,百分之九十的情况下,你会发现报错了……
而这些错误通常是“window is undefined”,“document is undefined”等等。
这是因为,我们的代码是在node环境下进行处理的,而在node环境中并没有像window、document等各种浏览器才有的对象。所以我们要进行相应的处理。
一般来说,我们只需要处理在SSR过程中才调用的函数即可。
这些函数包括beforeCreate、created、data、computed,可以再官网查阅。
如果你是使用es6的import/export写法,还有部分仅前端可用的插件也会报错。
例如jQuery,你可以更改成以下方法引入:
当你包裹完毕后,再访问相应的页面。你应该能看到你想要的HTML了。
网上的教程一般到这里就结束了。
然后你就懵逼了。
那不就只是纯HTML咯?我的酷炫的JavaScript交互呢!
是的,所以我们还要进行一次客户端渲染。有的教程会建议你写多个客户端入口文件,其实不必,我们只要将上方的server-entry.js改一下就好鸟。
客户端正常渲染都会在根组件处添加
server-rendered
属性,我们只要找寻这个属性将客户端实例挂载在上方就好了。那为什么我们前端也要进行preFetch呢?
因为我们要保证前端的首屏和服务端渲染出来的结构是一致的,否则就会重新渲染不一致的部分。照样子会造成闪烁的效果。
所以我的preFetch方法一般是这么写的
第一次好慢怎么办
当我们都弄完之后,我们再次测试。
忽然发现,咦,怎么加载好像很慢。
然后刷新一下,发现呀,快了好多。
这是因为第一次时要生成完整的HTML片段代码,而第二次可以把第一次生成后的HTML碎片代码直接拼接即可。
那么就意味着,第一个访问的客户会很慢!
这当然是不行的,咱们要每个客户平等对待,所以我们在thinkjs里面先渲染一次即可。
这时候打开thinks的src/common/bootstrap/global.js,添加如下代码。
那么我们在action里面也相应的使用global进行渲染即可。