@wy
2018-06-21T06:45:37.000000Z
字数 5139
阅读 1227
未分类
最近看Vue文档和官方给的例子,配置了Vue的服务端渲染,一开始是一脸懵的,根本不知道从何入手,在摸索的过程中不断出问题,然后试图解决,就这样匍匐前进中一步步的调试通了。特此记录我对服务端渲染的认知以及配置过程。如有问题,欢迎一起讨论学习。
首先先搞明白什么是服务端渲染,服务端渲染其实就是在后端把页面的 HTML 结构拼成字符串的形式,一次性的返回给客户端浏览器。
这里说的渲染可不是像
当然在这个过程中也会有数据的参与,
先来看一个在请求之后,服务端返回 Html 结构的例子。
使用 express 模块启动服务,无论访问哪一个路径,都返回一段已经拼接好的html结构:
const express = require('express');const app = express();app.get('*',(req,res) => {res.status(200);res.setHeader('Content-Type', 'text/html;charset=utf-8;')// 模拟数据,这段数据可以去数据库查询得到或者请求接口得到let list = [{name: 'Vue'},{name: 'React'},{name: 'Node.js'}]// 在服务端拼接上结构var liHtml = list.map((item) => `<li>${item.name}</li>`)// 向客户端返回整个文档结构res.end(`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>服务端返回的html结构</title></head><body><div><h2>这段html结构直接从服务端返回,访问路径为:${req.url}</h2><ul>${liHtml.join('')}</ul></div></body></html>`);})app.listen(4000,()=>{console.log('启动成功')})
这样在访问时候,返回的是已经生成好的 HTML 结构。如果涉及到数据可以直接通过查询数据库后,生成所需要的 HTML 结构,最终返回到浏览器,浏览器只需要负责解析渲染,而不需要通过javascript动态的生成HTML结构。这样避免了在javascript加载速度慢,或者需要处理的数据庞大而带来的生成HTML结构耗时,导致首屏会出现一闪而过的空白。
以上使用的是ES6的模板字符串来模拟模板引擎渲染,在服务端可以选择 ejs、jade、handler这样的模板引擎。
在浏览器打开源码后,服务端返回的就是一个完整的页面:
现在的开发方式大多都要前后端分离,前端负责将数据在UI页面展示,后端负责响应前端所需要的数据。对前端来说,页面的结构需要通过数据动态生成,首先要先向后端发送请求,拿到数据,执行javascript语句生成结构,插入到DOM中,浏览器开始解析渲染。这个过程还是需要消耗一定的时间,所以在没有渲染好之前,会出现首屏空白现象(当然可以加上loading菊花图,让体验变得更好)
写一个前后端分离的例子,体验一下:
后端代码,使用 express模块搭建:
const express = require('express');const app = express();// 设置静态文件访问目录app.use(express.static('public'))// 响应给前端请求的结构app.get('/api/list',(req,res) => {// 模拟数据let list = [{name: 'Vue'},{name: 'React'},{name: 'Node.js'}]res.send({success: true,list,url: req.url})})app.get('*',(req,res) => {res.status(200);res.setHeader('Content-Type', 'text/html;charset=utf-8;')// 只发送给前端一个页面res.sendFile(__dirname+ '/index.html');})app.listen(4000,()=>{console.log('启动成功')})
向前端发送的HTML页面,代码如下:
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>vue-ssr</title></head><body><div id="app"></div><script src="/test.js"></script></body></html>
从以上代码中可以看出来,当请求时,后端只向前端发送一个 index.html 页面,这个页面中只有一个空内容的 div 元素,页面中不会显示任何内容,这就是首屏出现空白的原因,因为没有任何元素显示。那么在这之后,页面中怎么有内容展示了呢?这要说到 test.js 中代码的作用:
// 向后端发请求获取数据fetch('/api/list').then((data) => {return data.json(); // json字符串转为可操作的对象}).then(({list,url}) => {// 拿到数据后渲染在页面中var app = document.getElementById('app');var newUl = document.createElement('ul')var liHtml = list.map((item) => `<li>${item.name}</li>`);newUl.innerHTML = liHtml.join('');app.innerHTML = `<h2>这段html结构在客户端通过javascript生成,访问路径为:${url}</h2>`app.appendChild(newUl)})
在文件 test.js 中,通过 fetch 向后端发送请求,拿到数据,然后生成拼接一些列的 HTML 结构,插入到页面在已经存在的元素挂载点id为 app 的div上,这样就显示出了内容。
现在模拟的数据量不是很庞大,请求也很快,所以在测试时候,那一闪而过的首页空白时间短的可以忽略不计,我们可以使用定时器来延迟一下模拟延长渲染的时间:
// 省略其他代码...setTimeout(() => {app.innerHTML = `<h2>这段html结构在客户端通过javascript生成,访问路径为:${url}</h2>`app.appendChild(newUl)},1000)...// 省略其他代码
在一秒之内页面是空白一片,一秒之后出现了内容。
在浏览器打开源码后查看,服务端返回的页面只有一个空的div标签:
按照正常使用 Vue 的写项目的流程,使用 new Vue 启动整个应用,代码如下:
const vueApp = new Vue({data: {message: 'hello,vue-ssr'},template: `<div><h1>欢迎学习vue-ssr</h1><p>{{message}}</p></div>`})
注意上面的代码在选项对象传入时,是没有传入挂载点选项 el 的,因为在服务端是不需要挂载点进行展示的,而是要把这段启动应用的程序转成HTML结构字符串。这就需要安装一个专门做服务端渲染的模块,由vue官方提供,安装模块:
npm i vue-server-renderer -S
安装模块后引入使用,会暴露一个函数 createRenderer , 通过这个方法创建 Renderer 实例,使用如下:
const { createRenderer } = require('vue-server-renderer')
const renderer = createRenderer({ /* 选项 */ })
现在就有了一个 renderer 实例可以使用,调用其下面的方法 renderToString 将 new Vue 根实例传入,返回值为一个 Promise 对象,拿到HTML字符串
vueServerRender.renderToString(vueApp).then((html) => {console.log(html)}).catch(err => console.log(err))
得到的HTML字符串为:
<div data-server-rendered="true"><h1>欢迎学习vue-ssr</h1> <p>hello,vue-ssr</p></div>
其中 data-server-rendered="true" 是一个标示,代表的是在服务端渲染出来的结构,这个后面要和客户端的代码混合会使用到。
以上的方式类似于模板引擎提供的函数一样,最终把数据和模板结合在一起,返回结合后的HTML字符串,最终可以将此字符串直接返回到客户端浏览器。
使用 express 创建服务的完整代码如下,创建 index.js 文件:
const express = require('express');const app = express();const Vue = require('vue');const {createRenderer} = require('vue-server-renderer')const vueServerRender = createRenderer()// 无论访问那个路由都走进来app.get('*',(req,res) => {res.status(200);res.setHeader('Content-Type', 'text/html;charset=utf-8;')// 实例const vueApp = new Vue({data:{message: 'hello,vue-ssr'},template: `<div><h1>欢迎学习vue-ssr</h1><p>{{message}}</p></div>`})vueServerRender.renderToString(vueApp).then((html) => {// 向客户端返回页面HTML结构res.end(`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>vue-ssr</title></head><body>${html}</body></html>`);}).catch(err => console.log(err))})app.listen(4000,()=>{console.log('启动成功')})
注意上面的代码,在每一次访问都会创建一个新的实例,这防止公用一个实例数据间的交叉污染,比如第一个用户访问时候产生了一个数据为 hello , 如果是用的同一个实例的话,第二个用户也会访问到 hello 这个数据,所以保证每一次都返回的是全新的实例。但这样访问量过大时候,会非常耗内存,好在是有缓存可以使用,可以把一些页面缓存一下,规定一个过期时间,在规定的时间内访问的都是同一个结合后的 HTML结构。
也可以使用HTML模板,看起来更加简单些,在根目录下创建 index.html:
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>vue-ssr</title></head><body><!--vue-ssr-outlet--></body></html>
模板中的注释部分 < !--vue-ssr-outlet--> 必须要添加上,将来实例转换后的HTML结构会替换在这个位置,在服务端代码需要稍作如下改动:
const path = require('path');const {createRenderer} = require('vue-server-renderer')const vueServerRender = createRenderer({//配置选项,读取模板的内容template: require('fs').readFileSync(path.join(__dirname,"./index.html"),'utf-8')})... // 代码省略// 发送到客户端浏览器vueServerRender.renderToString(vueApp).then((html) => {res.end(html);}).catch(err => console.log(err))
此时就完成了初步的vue服务端渲染的体验。