Open chiwent opened 5 years ago
参考一段服务端渲染的js脚本:
// FROM: https://juejin.im/post/5a9ca40b6fb9a028b77a4aac
const fs = require('fs')
const path = require('path')
const Koa = require('koa')
const KoaRuoter = require('koa-router')
const serve = require('koa-static')
const { createBundleRenderer } = require('vue-server-renderer')
const LRU = require('lru-cache')
const resolve = file => path.resolve(__dirname, file)
const app = new Koa()
const router = new KoaRuoter()
const template = fs.readFileSync(resolve('./src/index.template.html'), 'utf-8')
function createRenderer (bundle, options) {
return createBundleRenderer(
bundle,
Object.assign(options, {
template,
cache: LRU({
max: 1000,
maxAge: 1000 * 60 * 15
}),
basedir: resolve('./dist'),
runInNewContext: false
})
)
}
let renderer
const bundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
renderer = createRenderer(bundle, {
clientManifest
})
/**
* 渲染函数
* @param ctx
* @param next
* @returns {Promise}
*/
function render (ctx, next) {
ctx.set("Content-Type", "text/html")
return new Promise (function (resolve, reject) {
const handleError = err => {
if (err && err.code === 404) {
ctx.status = 404
ctx.body = '404 | Page Not Found'
} else {
ctx.status = 500
ctx.body = '500 | Internal Server Error'
console.error(`error during render : ${ctx.url}`)
console.error(err.stack)
}
resolve()
}
const context = {
title: 'Vue Ssr 2.3',
url: ctx.url
}
renderer.renderToString(context, (err, html) => {
if (err) {
return handleError(err)
}
console.log(html)
ctx.body = html
resolve()
})
})
}
app.use(serve('/dist', './dist', true))
app.use(serve('/public', './public', true))
router.get('*', render)
app.use(router.routes()).use(router.allowedMethods())
const port = process.env.PORT || 8089
app.listen(port, '0.0.0.0', () => {
console.log(`server started at localhost:${port}`)
})
一般的,我们会在preFetch中预加载数据到vuex中,然后组件对其中的vuex state进行渲染(当然,你也可以不将数据放到vuex中,那样在钩子函数完成数据请求也是可以的)。在数据预获取时,需要注意数据的对称性,假设组件中依赖的vuex数据缺失,将导致组件的渲染失败。另外,需要注意前面的preFetch仅在一级组件中有效,在子组件中是不会调用的。
在vue的生命周期函数中,beforeCreate
和created
会在服务端和客户端执行,其他钩子都在客户端执行,所以,如果在beforeCreate
和created
,或是直接在vuex和router入口文件中使用了storage、document以及window这些在浏览器端js才有的属性或对象时,就会报错。为了避免这个问题,应该在客户端渲染的钩子中执行。
在vue入口文件引用一些第三方组件时,会提示window
和document
为undefined,这是因为组件的渲染经过了服务端。此时我们应该注意区分服务端和浏览器端的渲染,一般的我们会在服务端和客户端的webpack配置文件中,设置环境变量,比如:
// webpack.client.config.js
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"client"',
})
// webpack.server.config.js
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"',
})
通过这个环境变量,我们可以判断上下文环境是处于客户端还是服务器端,当判定在客户端时,我们就可以引入或渲染(v-if)第三方组件:
<mavon-editor v-model="mdVal" v-if="isBrowser"></mavon-editor>
<script>
var mavonEditor = require("mavon-editor");
import "mavon-editor/dist/css/index.css";
export default {
components: {
mavonEditor
},
data() {
return {
mdVal: '',
isBrowser:
process.env.VUE_ENV === "client" || process.env.VUE_ENV !== "server",
}
}
}
</script>
如果要简单一点的方式,可以在组件内用路由监听或者路由钩子对document.title
赋值,另外一种方法可以在entry-server.js
中拿到context
属性,它是node服务端文件返回的请求上下文对象,可以取到相关的属性,然后再设置对应标题即可。
Vue SSR上手
本文将介绍如何使用
vue-server-renderer
进行服务端渲染,很多内容是搬运自官方文档,可以看作是笔记吧,对vue ssr的过程和原理有大致的描述。Vue SSR比起Vue SPA的优势:
我们都知道,浏览器在刚开始访问SPA应用时,服务器会返回一个基本的html骨架和一些js文件,html文件内没有网页的主体内容,需要浏览器解析js并渲染到页面中。这样,搜索引擎不仅不能抓取到关键信息,首屏加载时js还会阻塞页面渲染,导致白屏现象。在这种情况下,我们可以用服务端渲染进行优化。
上手前的注意点:
当然,坑点不仅仅是以上的内容,更多内容见后续
先来一个最简单的demo
先保证已经安装了插件:
npm install vue vue-server-renderer --save
开始一步步来完成吧:
和node配合使用:
然后我们创建一个模板文件:
<!--vue-ssr-outlet-->
标记的位置就是注入HTML文档的地方。我们还可以在模板中使用插值,比如上述的
meta
,我们可以这样注入:模板还支持一些高级特性(搬运至vue ssr指南):
当然,我们正常的开发不可能像上面这个demo那样简单粗暴,就像正常的vue开发也不会在一个html文件里面引用vue开发。
SSR的流程
由于vue ssr是同构的,所以在客户端和服务端都要在入口文件中创建vue实例,通过webpack分别进行打包,生成client bundle和server bundle。前者是客户端标记,等待拿到服务端渲染完成的数据后,混入完成初始化渲染;后者会在服务端上运行并生成预渲染的HTML字符串,再发送给客户端以完成初始化渲染。
vue ssr的常见目录结构 常用的webpack配置文件结构如下:
同时,还需要创建客户端的入口文件
entry-client.js
和服务端的入口文件entry-server.js
,以及通用的入口文件app.js
。setup-dev-server.js 这里的配置比较复杂,而且定义的自由度较高,不过多阐述。
server.js 这是整个ssr项目的入口文件。具体功能见上面的描述。定义的自由度较高,下面值放出一段最基础的配置(没有用到HMR),不过多阐述:
常见的优化参考下面:
缓存
流式渲染
手动资源注入 在
server.js
文件中,如果提供了template模板,那么资源注入是自动的。但是也可以选择不提供模板,手动注入,详情见:构建配置app.js
:通用入口文件的基本职责是创建vue实例,然后将其模块化导入到客户端和服务端的入口文件。所以,在通用入口文件中,我们将创建一个工程函数。并且,vuex和vue-router也在此创建实例(还需要引入vuex-router-sync进行store和router的同步)。 如下:
服务端每次请求渲染时,都会重写执行createApp方法,初始化store、router,不然数据不会更新。
entry-client.js
:它的主要工作其实很简单,就是创建vue实例,并挂载到DOM。
它的基本结构如下:
这里有个陌生的概念
window.__INITIAL_STATE__
,我们会在后续谈到。entry-server.js
:服务端每次渲染时,都会调用该入口文件,它原本的工作是创建vue实例,但是在此我们可以赋予它更多内容,比如服务端路由匹配和数据预获取:
上面函数的参数
context
等同于node中的ctx
,是一个全局上下文环境对象。数据获取和状态
在服务端渲染生成html前,我们需要预先获取并解析依赖的数据。同时,在客户端挂载(mounted)之前,需要获取和服务端完全一致的数据,否则客户端会因为数据不一致导致混入失败。如果在
beforeCreate
或created
时执行请求,由于这两个生命周期函数会在服务端执行(也就只有这两个vue生命周期函数会在服务端执行了,其他vue生命周期函数都是客户端中执行),且请求是异步的
,导致请求发出后,数据还没有返回,渲染就结束了。为了解决这个问题,预获取的数据要存储在状态管理器(store)中,以保证数据一致性。vue ssr有一个名为
asyncData
的函数,用来请求数据,它需要在服务端入口文件中预先配置,需要返回一个promise,等待所有请求都完成再渲染组件。然后在单独的视图组件中调用该方法,asyncData方法会在组件(限于页面组件)每次加载之前被调用。它可以在服务端或路由更新之前被调用。在这个方法被调用的时候,第一个参数被设定为当前页面的上下文对象,你可以利用 asyncData方法来获取数据并返回给当前组件。注意,由于asyncData方法是在组件 初始化 前被调用的,所以在方法内是没有办法通过 this 来引用组件的实例对象。也可以将数据预获取放在路由钩子完成,比如:
逻辑配置组件的数据预获取
服务端的数据预获取
还记得前面提到的
window.__INITIAL_STATE__
吗?当我们预获取数据完成,就会将context.state
作为window.__INITIAL_STATE__
的状态,自动混入客户端:客户端的数据预获取
客户端数据预获取的方式有两种:在路由导航之前获取,在匹配待渲染的视图后再获取
beforeMount
中的,当路由导航触发后,立即切换视图,因此有更快的响应速度。 但是,在渲染视图时不能得到完整的数据,所以需要条件判断:使用以上哪种方式取决于用户体验场景,但是无论是哪种,在路由组件复用的情况下,更改路由params或query,也需要调用
asyncData
:vue ssr的坑
一套代码,两套执行环境
在vue的生命周期函数中,只有
beforeCreate
和created
会在ssr过程中执行,其他的生命周期函数只会在客户端执行。所以应该避免在这两个生命周期函数中产生全局副作用的代码,比如定时器。同时,由于前端代码会在后端中执行,而Node.js和浏览器JavaScript有区别,导致在前端视图中的部分JavaScript属性或方法在执行时会报错。比如在使用一些插件的时候会提示window
或document
是undefined
,在这种情况下,可以用vue-no-ssr
让相关组件不走ssrcookie不可用
关于vue ssr不可用的解决方案,可以参考:再说Vue SSR的Cookies问题
参考:
Vue SSR指南
解密Vue SSR
Vue SSR Demo
一个极简版本的 VUE SSR demo
带你走近Vue服务器端渲染(VUE SSR)
基于vue-ssr服务端渲染入门详解
理解vue ssr原理,自己搭建简单的ssr框架