随着前端技术栈和工具链的迭代成熟,前端工程化、模块化也已成为了当下的主流技术方案。
在这波前端技术浪潮中,涌现了诸如 React、Vue、Angular 等基于客户端渲染的前端框架。
这类框架所构建的 **单页应用(SPA)**的优点:
**单页应用(SPA)**也有一些很大的缺陷,其中主要涉及到以下两点:
为了解决这两个缺陷,业界借鉴了传统的服务端直出 HTML 方案。
提出在服务器端执行前端框架 (React/Vue/Angular)代码生成网页内容。
然后将渲染好的网页内容返回给客户端,客户端只需要负责展示就可以了。
为了获得更好的用户体验,同时还会在客户端将来自服务端渲染的内容激活为一个 SPA 应用,也就是说之后的页面内容交互都是通过客户端渲染处理。
这种方式我们通常称之为现代化的服务端渲染,也叫同构渲染。
简而言之就是:
所谓的同构指的就是服务端构建渲染 + 客户端构建渲染。
同理,这种方式构建的应用称之为服务端渲染应用或者是同构应用(isomorphic web apps)。
这里所说的渲染指的是:把【数据】 +【模板】拼接到一起。
例如对于前端开发最常见的一种场景就是:
请求后端接口数据,然后将数据通过模板绑定语 法绑定到页面中,终呈现给用户。这个过程就是我们这里所指的渲染。
数据:
{
"message": "Hello world"
}
模板:
<h1>{{ message }}h1>
渲染(数据+模板)结果:
<h1>Hello worldh1>
渲染的本质其实就是字符串的解析替换,实现的方式有很多种。
但是这里要关注的并不是如何渲染,而是在哪里渲染的问题?
服务端渲染(SSR):Server Side Rendering。
早期,传统的 Web 应用,页面渲染都是在服务端完成的。
即服务端运行过程中将所需的数据结合页面模板渲染为 HTML,响应给客户端浏览器。
所以浏览器呈现出来的直接就是包含内容的页面。
这种方式的代表性技术有:ASP、PHP、JSP,再到后来的一些相对高级一点的服务端框架配合一些模板引擎。
该渲染模式下的流程交互图:
从流程图可以看出,渲染 是在服务端完成的。
安装依赖:
# 可以快速创建一个web服务
npm i express
创建后端服务文件 index.js
// index.js
const express = require('express')
// 创建express实例
const app = express()
// 添加路由
app.get('/', (req, res) => {
// GET访问网站根路径时,向客户端发送一个响应
res.send('Hello world')
})
// 监听端口号,启动web服务
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:
// index.js
const express = require('express')
const fs = require('fs')
// 创建express实例
const app = express()
// 添加路由
app.get('/', (req, res) => {
// 1. 获取页面模板
// fs读取文件需要指定编码,否则返回的是一个Buffer
const templateStr = fs.readFileSync('./index.html', 'utf-8')
// 2. 获取数据,并将json字符串转化成对象
const data = JSON.parse(fs.readFileSync('./data.json', 'utf-8'))
console.log(data)
res.send(templateStr)
})
// 监听端口号,启动web服务
app.listen(3000, () => console.log('running...'))
访问页面:
模板引擎用于字符串解析替换,也就是实现【数据+模板=渲染结果】。
# 服务端模板引擎
npm i art-template
art-template 提供一个render方法,接收两个参数:
返回字符串替换后的结果。
修改 index.js:
// index.js
const express = require('express')
const fs = require('fs')
const template = require('art-template')
// 创建express实例
const app = express()
// 添加路由
app.get('/', (req, res) => {
// 1. 获取页面模板
// fs读取文件需要指定编码,否则返回的是一个Buffer
const templateStr = fs.readFileSync('./index.html', 'utf-8')
// 2. 获取数据
// 将json字符串转化成对象
const data = JSON.parse(fs.readFileSync('./data.json', 'utf-8'))
// 3. 渲染:数据 + 模板 = 最终渲染结果
const html = template.render(templateStr, data)
// 4. 把渲染结果发送给客户端
res.send(html)
})
// 监听端口号,启动web服务
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/),返回的就是页面内容。
现在,这个页面是一个动态页面,它受数据和模板影响。
一旦数据或模板改变,页面的结果也会改变。
而不像静态网站一样,需要为每个新的内容,写一个新的页面。
这也就是早的网页渲染方式,也就是动态网站的核心工作步骤。
在这样的一个工作过程中,因为页面中的内容不是固定的,它有一些动态的内容。
在当下这种网页越来越复杂的情况下,这种渲染模式存在很多明显的不足:
但是不得不说,在网页应用并不复杂的情况下,这种方式也是可取的。
客户端渲染(CSR):Client Side Rendering。
随着客户端 Ajax 技术的普及,服务端渲染的那些不足得到了有效的解决。
Ajax 技术可以使得客户端动态获取数据变为可能,也就是说原本的服务端渲染工作,也可以交给 **客户端 **处理。
客户端动态获取数据的目的,也就是为了解决服务端渲染的那些不足:
基于客户端渲染的 SPA 应用的基本工作流程:
空白HTML 不是完全空白,只是没有实质的页面内容,它包含一些引导性的JS脚本。
客户端请求到页面后,会加载执行里面的JS脚本,渲染页面内容。
如果有动态数据请求,会通过ajax发起请求获取数据。
服务端只返回数据,客户端拿到数据后,动态渲染到页面中。
整个过程中:
这就是所谓的 前后端分离。
这样可以更合理的划分项目代码 和 人员职责,极大的提高了开发效率和可维护性。
客户端渲染也存在一些明显的不足,主要是:
解决方案:使用服务端渲染,严格来说是现代化的服务端渲染,也叫同构渲染。
因为html中没有内容,必须等到 JS 脚本加载并执行完才能呈现页面内容。
可以通过开发人员工具降低网速查看对比结果。
例如:有个页面,需要展示数据库的文章列表。
假设每个请求都耗时 2秒,过程如下
可以看到 客户端渲染 要做的事情很多(3次请求),并且受客户端网速影响很大。
而传统的服务端渲染的网速只受服务器影响,并且直接返回处理完的页面。
SEO就是搜索引擎可以获取网站的信息,搜索网站的关键字就可以搜到网站。
搜索引擎是通过程序获取指定的网页内容,分析里面的内容,进行收录、排名等。
nodejs模拟一个搜索引擎:
// http 用于创建一个web服务器
// 也可以用于请求另一个服务地址
const http = require('http')
// http://localhost:3000/ 是模拟的传统服务端渲染示例
// http://localhost:8080/ 是本地运行的Vue CLI 创建的 SPA 应用
// http.get('http://localhost:3000/', res => {
http.get('http://localhost:8080/', res => {
let data = ''
// 接收数据会触发data事件
res.on('data', chunk => {
// 如果数据量比较大,会不断触发data事件,传递数据块
// 拼接数据块
data += chunk
})
// 所有数据接收完毕以后触发end事件
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 Stricklandh2>
<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 Joyceh2>
<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 Castanedah2>
<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。
SPA 单页应用 | 传统 Web 应用 | |
---|---|---|
组成 | 一个外壳页面和多个页面片段组成 | 多个完整页面构成 |
资源共用 | 共用,只需在外壳部分加载 | 不共用,每个页面都需要重新加载 |
刷新方式 | 页面局部刷新或更改 | 整页刷新 |
用户体验 | 页面片段间的切换快,用户体验良好 | 页面切换加载缓慢,流畅度不够,用户体验比较差 |
转场动画 | 容易实现 | 无法实现 |
数据传递 | 容易 | 依赖 url 传参、或 cookie、localStorage 等 |
搜索引擎优化(SEO) | 需要单独方案、实现较为困难、不利于 SEO 检索,可利用服务器端渲染(SSR)优化 | 实现方法简易 |
使用范围 | 高要求的体验度、追求界面流畅的应用 | 适用于追求高度支持搜索引擎的应用 |
开发成本 | 较高,常需借助专业的框架 | 较低,但页面重复代码多 |
维护成本 | 相对容易 | 相对复杂 |
服务端压力 | 小 | 大 |
此外,SPA 框架还需要更强的体系结构和安全专业知识。相较于传统 Web 应用程序,SPA 框架需要进行频繁的更新和使用新框架,因此改动更大。
相较于传统 Web 应用,SPA 应用程序在配置自动化生成和部署过程以及利用部署选项(如容器)方面的难度更大。
使用 SPA 方法改进用户体验时必须权衡这些注意事项。
解决客户端渲染不足的办法就是传统的服务端渲染和客户端渲染结合使用,称作现代化的服务端渲染,也叫同构渲染。
同构渲染 = SSR + CSR
同构渲染流程:
首屏渲染在服务端进行,称作首屏直出
- 利用服务端渲染的优点,解决 SEO 和 首屏渲染慢的问题
保留了客户端渲染单页面应用的体验
Nuxt.js 是一个基于 Vue.js 生态开发的一个第三方服务端渲染框架,通过它可以轻松的构建现代化的服务端渲染应用。
安装 nuxt
npm i nuxt
package.json 添加 nuxt 的启动脚本:
{
"scripts": {
"dev": "nuxt"
}
// ...
}
新建 pages 目录,在里面创建 index.vue。
<template>
<h1>Homeh1>
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',
// asyncData:Nuxt 中特殊提供的一个钩子函数,专门用于获取页面服务端渲染的数据
async asyncData () {
const { data } = await axios({
method: 'GET',
// '/data.json' 默认会访问服务器的80端口,所以这里要指定前面的地址
url: 'http://localhost:3000/data.json'
})
// asyncData 返回的数据,会和 Vue 实例的 data 中的数据合并到一起,供页面使用
return {
title: data.title,
posts: data.posts
}
}
}
script>
再次访问页面,右键查看页面源代码或者查看请求页面的响应结果可以看到:
页面返回的内容就是渲染好的内容,不需要客户端再通过脚本渲染页面内容。
这样就解决了首屏渲染慢 和 SEO的问题。
页面交互还是由客户端负责渲染。
新增页面(pages/about.vue),用于切换页面查看交互效果。
<template>
<h1>Abouth1>
template>
新建目录和文件:layouts/default.vue
<template>
<div>
<ul>
<li>
<nuxt-link to="/">Homenuxt-link>
li>
<li>
<nuxt-link to="/about">Aboutnuxt-link>
li>
ul>
<nuxt />
div>
template>
layouts/default.vue 会作为所有页面的父模板
再次访问页面,点击链接切换页面,查看效果:
/data.json
数据的请求
这就是同构渲染(现代化服务端渲染)的SPA应用。
1、开发条件有限
2、涉及构建和部署的要求更多
客户端渲染 | 同构渲染 | |
---|---|---|
构建 | 仅构建客户端应用即可 | 需要构建两个端 |
部署 | 可以部署在任意web服务器中 | 只能部署在Node.js Server |
部署:
3、更多的服务端负载
例如: