@wy
2018-06-21T14:45:37.000000Z
字数 5139
阅读 1091
未分类
最近看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服务端渲染的体验。