概述
随着前端技术栈和工具链的迭代成熟,前端工程化、模块化也已成为了当下的主流技术方案。
在这波前端技术浪潮中,涌现了诸如 React、Vue、Angular 等基于客户端渲染的前端框架。
这类框架所构建的 **单页应用(SPA)**的优点:
**单页应用(SPA)**也有一些很大的缺陷,其中主要涉及到以下两点:
- 首屏渲染时间过长
- 与传统服务端渲染直接获取服务端渲染好的 HTML 不同
- 单页应用使用 JavaScript 在客户端生成 HTML 来呈现内容
- 用户需要等待客户端 JS 解析执行完成才能看到页面
- 这就使得首屏加载时间变长,从而影响用户体验。
- 不利于SEO
- 当搜索引擎爬取网站 HTML 文件时,单页应用的 HTML 没有内容
- 因为它需要通过客户端 JS 解析执行才能生成网页内容
- 而目前的主流的搜索引擎对于这一部分内容的抓取还不是很好
为了解决这两个缺陷,业界借鉴了传统的服务端直出 HTML 方案。
提出在服务器端执行前端框架 (React/Vue/Angular)代码生成网页内容。
然后将渲染好的网页内容返回给客户端,客户端只需要负责展示就可以了。
为了获得更好的用户体验,同时还会在客户端将来自服务端渲染的内容激活为一个 SPA 应用,也就是说之后的页面内容交互都是通过客户端渲染处理。
这种方式我们通常称之为现代化的服务端渲染,也叫同构渲染。
简而言之就是:
- 通过服务端渲染首屏直出,解决首屏渲染慢以及不利于 SEO 问题
- 通过客户端渲染接管页面内容交互得到更好的用户体验
所谓的同构指的就是服务端构建渲染 + 客户端构建渲染。
同理,这种方式构建的应用称之为服务端渲染应用或者是同构应用(isomorphic web apps)。
什么是渲染
这里所说的渲染指的是:把【数据】 +【模板】拼接到一起。
例如对于前端开发最常见的一种场景就是:
请求后端接口数据,然后将数据通过模板绑定语 法绑定到页面中,终呈现给用户。这个过程就是我们这里所指的渲染。
数据:
{
"message": "Hello world"
}
模板:
<h1>{{ message }}</h1>
渲染(数据+模板)结果:
<h1>Hello world</h1>
渲染的本质其实就是字符串的解析替换,实现的方式有很多种。
但是这里要关注的并不是如何渲染,而是在哪里渲染的问题?
传统的服务端渲染(SSR)
服务端渲染(SSR):Server Side Rendering。
早期,传统的 Web 应用,页面渲染都是在服务端完成的。
即服务端运行过程中将所需的数据结合页面模板渲染为 HTML,响应给客户端浏览器。
所以浏览器呈现出来的直接就是包含内容的页面。
这种方式的代表性技术有:ASP、PHP、JSP,再到后来的一些相对高级一点的服务端框架配合一些模板引擎。
该渲染模式下的流程交互图:
从流程图可以看出,渲染 是在服务端完成的。
使用 nodejs 来了解传统服务端渲染方式
搭建web服务
安装依赖:
npm i express
创建后端服务文件 index.js
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello world')
})
app.listen(3000, () => console.log('running...'))
使用 node 执行这个文件,启动web服务。
稍后要频繁修改这个文件,所以可以使用 nodemon 执行(需要全局安装nodemon)。
启动完成后,在浏览器访问 http://localhost:3000 可以查看来自服务端的响应内容。
Response 返回的文本内容,就是服务端发送的响应内容。
渲染页面内容
接着要做的就是把模板结合数据渲染到页面中,主要内容是:
- 获取页面模板
- 获取数据
- 执行渲染:数据 + 模板 = 最终渲染结果
- 把渲染结果发送到客户端
准备工作
首先创建模板文件和数据文件:
模板文件(index.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>传统的服务端渲染</title>
</head>
<body>
<h1>传统的服务端渲染示例</h1>
</body>
</html>
数据文件(data.json),自行填充数据:
{
"posts": [
{
"id": 0,
"title": "",
"body": ""
},
]
}
修改index.js:
const express = require('express')
const fs = require('fs')
const app = express()
app.get('/', (req, res) => {
const templateStr = fs.readFileSync('./index.html', 'utf-8')
const data = JSON.parse(fs.readFileSync('./data.json', 'utf-8'))
console.log(data)
res.send(templateStr)
})
app.listen(3000, () => console.log('running...'))
访问页面:
- 页面显示的是模板内容
- IDE 控制台打印 data 的结果
使用模板引擎
模板引擎用于字符串解析替换,也就是实现【数据+模板=渲染结果】。
npm i art-template
art-template 提供一个render方法,接收两个参数:
- source - 模板源码字符串
- data - 一个数据对象
返回字符串替换后的结果。
修改 index.js:
const express = require('express')
const fs = require('fs')
const template = require('art-template')
const app = express()
app.get('/', (req, res) => {
const templateStr = fs.readFileSync('./index.html', 'utf-8')
const data = JSON.parse(fs.readFileSync('./data.json', 'utf-8'))
const html = template.render(templateStr, data)
res.send(html)
})
app.listen(3000, () => console.log('running...'))
修改模板,在里面使用数据:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>传统的服务端渲染</title>
</head>
<body>
<h1>传统的服务端渲染示例</h1>
{{ each posts }}
<div class="article">
<h2>{{ $value.title }}</h2>
<artile>{{ $value.body }}</article>
</div>
{{ /each }}
</body>
</html>
效果:
请求页面(http://localhost:3000/),返回的就是页面内容。
总结和不足
现在,这个页面是一个动态页面,它受数据和模板影响。
一旦数据或模板改变,页面的结果也会改变。
而不像静态网站一样,需要为每个新的内容,写一个新的页面。
这也就是早的网页渲染方式,也就是动态网站的核心工作步骤。
在这样的一个工作过程中,因为页面中的内容不是固定的,它有一些动态的内容。
在当下这种网页越来越复杂的情况下,这种渲染模式存在很多明显的不足:
- 前后端代码完全耦合在一起,不利于开发和维护
- 前端没有足够发挥空间,无法充分利用现在前端生态下的一些更优秀的方案
- 由于内容都是在服务端动态生成的,服务端压力大
- 相比目前流行的 SPA 应用来说,用户体验一般
但是不得不说,在网页应用并不复杂的情况下,这种方式也是可取的。
客户端渲染(CSR)
客户端渲染(CSR):Client Side Rendering。
随着客户端 Ajax 技术的普及,服务端渲染的那些不足得到了有效的解决。
Ajax 技术可以使得客户端动态获取数据变为可能,也就是说原本的服务端渲染工作,也可以交给 **客户端 **处理。
客户端动态获取数据的目的,也就是为了解决服务端渲染的那些不足:
- 前后端分离
- 解决前后端代码耦合,不利于开发和维护的问题
- 后端负责数据处理,提供接口
- 前端负责视图渲染处理
- 页面渲染交给用户的客户端,减少服务端压力
- 可以开发 SPA 单页面应用程序,无刷新体验更优
基于客户端渲染的 SPA 应用的基本工作流程:
空白HTML 不是完全空白,只是没有实质的页面内容,它包含一些引导性的JS脚本。
客户端请求到页面后,会加载执行里面的JS脚本,渲染页面内容。
如果有动态数据请求,会通过ajax发起请求获取数据。
服务端只返回数据,客户端拿到数据后,动态渲染到页面中。
整个过程中:
- 服务端【后端】 - 只需要负责【数据处理】
- 客户端【前端】 - 负责【页面渲染】
- 将接口数据渲染到页面
- 【前端】更为独立,不再受限制于【后端】,可以选择任意的技术方案或框架来处理页面渲染
这就是所谓的 前后端分离。
这样可以更合理的划分项目代码 和 人员职责,极大的提高了开发效率和可维护性。
客户端渲染不足
客户端渲染也存在一些明显的不足,主要是:
- 首屏渲染慢
- 因为html中没有内容,必须等到 JS 脚本加载并执行完才能呈现页面内容。
- 不利于SEO
- 因为html中没有内容,所以对于目前的搜索引擎爬虫来说,页面中没有任何有用的信息,自然无法提取关键词,进行索引了。
解决方案:使用服务端渲染,严格来说是现代化的服务端渲染,也叫同构渲染。
为什么客户端渲染首屏渲染慢
因为html中没有内容,必须等到 JS 脚本加载并执行完才能呈现页面内容。
可以通过开发人员工具降低网速查看对比结果。
例如:有个页面,需要展示数据库的文章列表。
假设每个请求都耗时 2秒,过程如下
- 客户端渲染
- 页面请求:首先请求页面内容(耗时2秒)
- 资源请求:然后加载页面中的脚本(并行加载,耗时2秒)
- 动态请求:脚本中有Ajax请求服务端获取文章列表数据(耗时2秒)
- 最终渲染到页面
- 传统服务端渲染
- 页面请求:请求页面(耗时2秒)
- 服务端会读取数据库的文章列表,并解析模板中,最终直出返回给客户端
- 展示服务端响应结果
可以看到 客户端渲染 要做的事情很多(3次请求),并且受客户端网速影响很大。
而传统的服务端渲染的网速只受服务器影响,并且直接返回处理完的页面。
为什么客户端渲染不利于SEO
SEO就是搜索引擎可以获取网站的信息,搜索网站的关键字就可以搜到网站。
搜索引擎是通过程序获取指定的网页内容,分析里面的内容,进行收录、排名等。
nodejs模拟一个搜索引擎:
const http = require('http')
http.get('http://localhost:8080/', res => {
let data = ''
res.on('data', chunk => {
data += chunk
})
res.on('end', () => {
console.log(data)
})
})
node 执行这个脚本查看对比结果:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>传统的服务端渲染</title>
</head>
<body>
<h1>传统的服务端渲染示例</h1>
<div class="article">
<h2>Collier Strickland</h2>
<artile>Proident exercitation veniam adipisicing laborum elit Lorem cupidatat occaecat culpa id deserunt. Sint enim labore eu quis nisi eiusmod dolor enim. Aliqua magna duis occaecat commodo adipisicing adipisicing. Mollit ad deserunt sunt magna aliquip deserunt ullamco voluptate elit. Occaecat reprehenderit sunt aute do Lorem cupidatat est aliqua velit. Eu anim do non adipisicing nisi.
</article>
</div>
<div class="article">
<h2>England Joyce</h2>
<artile>Ullamco enim velit ullamco esse. Ex consectetur deserunt labore culpa duis occaecat fugiat Lorem esse excepteur. Minim consectetur irure nisi velit anim tempor. Magna veniam laboris enim cupidatat nisi pariatur esse. Sint ea duis ex excepteur est ex
commodo cillum. Deserunt ut officia deserunt velit culpa sint sint minim.
</article>
</div>
<div class="article">
<h2>Leonor Castaneda</h2>
<artile>Pariatur quis cupidatat consectetur ullamco et proident velit officia dolore nisi ullamco deserunt. Voluptate amet aliquip commodo officia ipsum cupidatat aute. Labore et id reprehenderit voluptate reprehenderit aute duis proident esse reprehenderit. Irure pariatur ut ipsum enim sit anim occaecat fugiat aliquip adipisicing. Consectetur
esse consectetur ad laboris adipisicing minim laborum aute. Nisi aliquip enim ea incididunt consequat eiusmod. Laborum ipsum aliqua quis voluptate minim sit deserunt enim voluptate aute.
</article>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/favicon.ico">
<title>客户端渲染</title>
<link href="/js/about.js" rel="prefetch"><link href="/js/app.js" rel="preload" as="script"><link href="/js/chunk-vendors.js" rel="preload" as="script"></head>
<body>
<noscript>
<strong>We're sorry but vue-csr doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="text/javascript" src="/js/chunk-vendors.js"></script><script type="text/javascript" src="/js/app.js"></script></body>
</html>
客户端渲染的页面内容必须通过解析执行JS脚本。
搜索引擎就是一个程序,不是浏览器。
它只会获取网页的html字符串,不会像浏览器一样加载解析JS脚本、请求获取动态数据、渲染页面等。
所以客户端渲染(SPA)的SEO获取不到有用的网站信息,不利于SEO。
传统 Web 应用 vs SPA 应用
| SPA 单页应用 | 传统 Web 应用 |
---|
组成 | 一个外壳页面和多个页面片段组成 | 多个完整页面构成 | 资源共用 | 共用,只需在外壳部分加载 | 不共用,每个页面都需要重新加载 | 刷新方式 | 页面局部刷新或更改 | 整页刷新 | 用户体验 | 页面片段间的切换快,用户体验良好 | 页面切换加载缓慢,流畅度不够,用户体验比较差 | 转场动画 | 容易实现 | 无法实现 | 数据传递 | 容易 | 依赖 url 传参、或 cookie、localStorage 等 | 搜索引擎优化(SEO) | 需要单独方案、实现较为困难、不利于 SEO 检索,可利用服务器端渲染(SSR)优化 | 实现方法简易 | 使用范围 | 高要求的体验度、追求界面流畅的应用 | 适用于追求高度支持搜索引擎的应用 | 开发成本 | 较高,常需借助专业的框架 | 较低,但页面重复代码多 | 维护成本 | 相对容易 | 相对复杂 | 服务端压力 | 小 | 大 |
何时使用传统 Web 应用程序
- 应用程序的客户端要求简单,甚至要求只读
- 应用程序需在不支持 JavaScript 的浏览器中工作
- 团队不熟悉 JavaScript 或 TypeScript 开发技术
何时使用 SPA
- 应用程序必须公开具有许多功能的丰富用户界面
- 团队熟悉 JavaScript、TypeScript 等开发
- 应用程序已为其他(内部或公共)客户端公开 API
此外,SPA 框架还需要更强的体系结构和安全专业知识。相较于传统 Web 应用程序,SPA 框架需要进行频繁的更新和使用新框架,因此改动更大。
相较于传统 Web 应用,SPA 应用程序在配置自动化生成和部署过程以及利用部署选项(如容器)方面的难度更大。
使用 SPA 方法改进用户体验时必须权衡这些注意事项。
现代化的服务端渲染 - 同构渲染
解决客户端渲染不足的办法就是传统的服务端渲染和客户端渲染结合使用,称作现代化的服务端渲染,也叫同构渲染。
同构渲染 = SSR + CSR
同构渲染流程:
- 客户端发起页面请求
- 服务端渲染首屏内容 + 生成客户端 SPA 程序运行所需的相关资源(包含SPA程序运行所需要的JS脚本)
- 服务端将生成的首屏资源发送给客户端
- 客户端直接展示服务端渲染好的首屏内容
- 客户端执行首屏内容中的脚本,接管页面,生成(激活)SPA应用
- 之后客户端所有的交互都由客户端SPA处理
首屏渲染在服务端进行,称作首屏直出
- 利用服务端渲染的优点,解决 SEO 和 首屏渲染慢的问题
保留了客户端渲染单页面应用的体验
如何实现同构渲染?
- 使用 Vue、React 等前端框架提供的官方解决方案
- 使用第三方解决方案,流行的有:
- React 生态的 Next.js
- Vue 生态的 Nuxt.js
通过 Vue 生态的 Nuxt.js 体验同构渲染
Nuxt.js 是一个基于 Vue.js 生态开发的一个第三方服务端渲染框架,通过它可以轻松的构建现代化的服务端渲染应用。
启动 Nuxt
安装 nuxt
npm i nuxt
package.json 添加 nuxt 的启动脚本:
{
"scripts": {
"dev": "nuxt"
}
}
新建 pages 目录,在里面创建 index.vue。
<template>
<h1>Home</h1>
</template>
npm run dev 启动 nuxt,打开访问地址(默认3000端口)。
nuxt 会根据 pages 目录自动生成路由配置,index.vue 对应的就是首页(/ )。
nuxt 也会自动创建vue实例,加载路由组件。
本节暂不关心nuxt的使用,只关注同构渲染的体验。
查看页面响应结果
丰富 index.vue 的内容,查看页面请求过程和响应结果。
首先安装 axios(npm i axios )
新建 static/data.json 存储前面示例中的json数据。
注意:目录名必须是 static,用于通过/data.json 地址请求。
{
"title": "Nuxt 同构渲染",
"posts": [
{
"id": 0,
"title": "Collier Strickland"
},
{
"id": 1,
"title": "England Joyce"
},
{
"id": 2,
"title": "Leonor Castaneda"
}
]
}
修改 pages/index.vue:
Nuxt 封装了 vue,可以通过.vue文件编写页面内容,但是方式有些改变:
<template>
<div>
<h1>{{title}}</h1>
<ul>
<li v-for="item in posts" :key="item.id">{{item.title}}</li>
</ul>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'Home',
async asyncData () {
const { data } = await axios({
method: 'GET',
url: 'http://localhost:3000/data.json'
})
return {
title: data.title,
posts: data.posts
}
}
}
</script>
再次访问页面,右键查看页面源代码或者查看请求页面的响应结果可以看到:
页面返回的内容就是渲染好的内容,不需要客户端再通过脚本渲染页面内容。
这样就解决了首屏渲染慢 和 SEO的问题。
asyncData
- 基本用法
- 它会将 asyncData 返回的数据融合组件 data 方法返回的数据一并给组件
- 调用时机:服务端渲染期间和客户端路由更新之前
- 注意事项:
- 只能在页面组件(pages目录下的组件)中使用
- 没有this,因为它是在组件初始化之前被调用的
- 使用场景:
- 当想要动态页面内容有利于 SEO 或者是提升首屏渲染速度的时候,就在 asyncData中发送请求获取数据
- 如果是非异步数据或者普通数据,则正常初始化到 data 中即可
同构渲染应用的交互流程
页面交互还是由客户端负责渲染。
新增页面(pages/about.vue),用于切换页面查看交互效果。
<template>
<h1>About</h1>
</template>
新建目录和文件:layouts/default.vue
<template>
<div>
<ul>
<li>
<nuxt-link to="/">Home</nuxt-link>
</li>
<li>
<nuxt-link to="/about">About</nuxt-link>
</li>
</ul>
<nuxt />
</div>
</template>
layouts/default.vue 会作为所有页面的父模板
- 相当于所有页面的父组件
- 子组件会渲染到
<nuxt /> - 注意:layouts 和 default.vue 的名称是Nuxt 固定的
再次访问页面,点击链接切换页面,查看效果:
- 点击链接,页面内容切换,并没有刷新页面
- 查看Network请求日志,切换页面,也没有发起页面请求
- 切换到首页,还是会发起获取
/data.json 数据的请求
- 这是因为首页再次渲染(非首屏渲染),也要重新获取数据,但这时渲染由客户端负责,所以这个请求就由客户端发起
- 此时这个页面的内容就是客户端渲染
- 首屏还是服务端渲染
- 直接通过url访问about页面(或手动刷新about页面),页面请求返回的还是服务端渲染好的页面内容
- 客户端在首屏加载完,通过脚本接管页面,把页面激活为SPA应用,之后的交互由客户端负责
这就是同构渲染(现代化服务端渲染)的SPA应用。
同构渲染应用的优缺点
优点
缺点
1、开发条件有限
- 浏览器特定的代码只能在某些生命周期钩子函数中使用
- 一些外部扩展库可能需要特殊处理才能运行,例如
- 在纯浏览器环境中才能运行的扩展库,需要在服务端运行
- 在服务端才能运行的扩展库,需要在客户端运行
- 需要为特殊依赖某个环境的代码作区分
2、涉及构建和部署的要求更多
| 客户端渲染 | 同构渲染 |
---|
构建 | 仅构建客户端应用即可 | 需要构建两个端 | 部署 | 可以部署在任意web服务器中 | 只能部署在Node.js Server |
部署:
- 客户端渲染构建的应用是完全静态单页面应用程序(SPA),可以部署在任何静态文件服务器上。
- 同构渲染应用都是用前端JS框架(Vue、React等)编写的,所以应用一般只能部署在JS运行环境中(浏览器 或 Nodejs)。
3、更多的服务端负载
- 在Node中渲染完整的应用程序,相比仅仅提供静态文件的服务器,需要大量占用CPU资源
- 如果应用在高流量(并发量非常高)的环境下使用,需要准备相应的服务器负载和采用缓存策略
- 需要更多的服务端渲染优化工作处理
服务端渲染使用建议
- 首屏渲染速度是否真的重要?
- 主要取决于内容到达时间(time-to-content)对应用程序的重要程度
- 是否真的需求SEO?
- 如果有出于效益的考虑,例如需要搜索引擎收录的硬性需求,不得不启用服务端渲染
例如:
- 后台管理系统,首屏加载速度不重要,且不需要搜索引擎收录,可以不启用服务端渲染。
- 一个门户网站,首页需要立即渲染,并且希望在搜索引擎搜索关键字找到对应的站内页面,这就需要考虑启用服务端渲染了。
|