前端性能优化原理与实践(二)
从 Cookie 到 Web Storage、IndexDB
Cookie
Cookie
的本职工作并非本地存储,而是“维持状态”
。
在 Web
开发的早期,人们亟需解决的一个问题就是状态管理的问题:HTTP 协议
是一个无状态协议,服务器接收客户端的请求,返回一个响应,故事到此就结束了,服务器并没有记录下关于客户端的任何信息。那么下次请求的时候,如何让服务器知道“我是我”
呢?
在这样的背景下,Cookie
应运而生。
Cookie
说白了就是一个存储在浏览器里的一个小小的文本文件,它附着在 HTTP
请求上,在浏览器和服务器之间“飞来飞去”。它可以携带用户信息,当服务器检查Cookie
的时候,便可以获取到客户端的状态。
Cookie的性能劣势
-
Cookie
不够大,Cookie
是有体积上限的,它最大只能有4KB
。当Cookie
超过4KB
时,它将面临被裁切的命运。这样看来,Cookie
只能用来存取少量的信息。 -
过量的
Cookie
会带来巨大的性能浪费,Cookie
是紧跟域名的。我们通过响应头里的Set-Cookie
指定要存储的Cookie
值。默认情况下,domain
被设置为设置Cookie
页面的主机名,我们也可以手动设置domain
的值:
Set-Cookie: name=xiuyan; domain=xiuyan.me
同一个域名下的所有请求,都会携带Cookie
。大家试想,如果我们此刻仅仅是请求一张图片或者一个 CSS
文件,我们也要携带一个Cookie
跑来跑去(关键是Cookie
里存储的信息我现在并不需要),这是一件多么劳民伤财的事情。Cookie
虽然小,请求却可以有很多,随着请求的叠加,这样的不必要的 Cookie
带来的开销将是无法想象的。
Web Storage
存储容量大:Web Storage
根据浏览器的不同,存储容量可以达到5-10M
之间。
仅位于浏览器端,不与服务端发生通信。
Web Storage 核心 API 使用示例
Web Storage
保存的数据内容和Cookie
一样,是文本内容,以键值对的形式存在。Local Storage
与 Session Storage
在 API
方面无异,这里我们以localStorage
为例:
- 存储数据:
setItem()
localStorage.setItem('user_name', 'xiuyan')
- 读取数据:
getItem()
localStorage.getItem('user_name')
- 删除某一键名对应的数据:
removeItem()
localStorage.removeItem('user_name')
- 清空数据记录:
clear()
localStorage.clear()
IndexDB
IndexDB
是一个运行在浏览器上的非关系型数据库。既然是数据库了,那就不是5M
、10M
这样小打小闹级别了。理论上来说,IndexDB
是没有存储上限的(一般来说不会小于 250M
)。它不仅可以存储字符串,还可以存储二进制数据。
- 打开/创建一个
IndexDB
数据库(当该数据库不存在时,open
方法会直接创建一个名为xiaoceDB
新数据库)。
// 后面的回调中,我们可以通过event.target.result拿到数据库实例
let db
// 参数1位数据库名,参数2为版本号
const request = window.indexedDB.open("xiaoceDB", 1)
// 使用IndexDB失败时的监听函数
request.onerror = function(event) {
console.log('无法使用IndexDB')
}
// 成功
request.onsuccess = function(event){
// 此处就可以获取到db实例
db = event.target.result
console.log("你打开了IndexDB")
}
- 创建一个
object store
(object store
对标到数据库中的“表”
单位)。
// onupgradeneeded事件会在初始化数据库/版本发生更新时被调用,我们在它的监听函数中创建object store
request.onupgradeneeded = function(event){
let objectStore
// 如果同名表未被创建过,则新建test表
if (!db.objectStoreNames.contains('test')) {
objectStore = db.createObjectStore('test', { keyPath: 'id' })
}
}
- 构建一个事务来执行一些数据库操作,像增加或提取数据等。
// 创建事务,指定表格名称和读写权限
const transaction = db.transaction(["test"],"readwrite")
// 拿到Object Store对象
const objectStore = transaction.objectStore("test")
// 向表格写入数据
objectStore.add({id: 1, name: 'xiuyan'})
- 通过监听正确类型的事件以等待操作完成。
// 操作成功时的监听函数
transaction.oncomplete = function(event) {
console.log("操作成功")
}
// 操作失败时的监听函数
transaction.onerror = function(event) {
console.log("这里有一个Error")
}
服务端渲染的运行机制
相对于服务端渲染,同学们普遍对客户端渲染接受度更高一些,所以我们先从大家喜闻乐见的客户端渲染说起。
客户端渲染
客户端渲染模式下,服务端会把渲染需要的静态文件发送给客户端,客户端加载过来之后,自己在浏览器里跑一遍 JS
,根据 JS
的运行结果,生成相应的DOM
。这种特性使得客户端渲染的源代码总是特别简洁:
<!doctype html>
<html>
<head>
<title>我是客户端渲染的页面</title>
</head>
<body>
<div id='root'></div>
<script src='index.js'></script>
</body>
</html>
根节点下到底是什么内容呢?你不知道,我不知道,只有浏览器把 index.js
跑过一遍后才知道,这就是典型的客户端渲染。
页面上呈现的内容,你在html
源文件里里找不到——这正是它的特点。
服务端渲染
服务端渲染的模式下,当用户第一次请求页面时,由服务器把需要的组件或页面渲染成HTML
字符串,然后把它返回给客户端。客户端拿到手的,是可以直接渲染然后呈现给用户的HTML
内容,不需要为了生成 DOM
内容自己再去跑一遍 JS
代码。
使用服务端渲染的网站,可以说是“所见即所得”
,页面上呈现的内容,我们在html
源文件里也能找到。
该示例直接将 Vue
实例整合进了服务端的入口文件中:
const Vue = require('vue')
// 创建一个express应用
const server = require('express')()
// 提取出renderer实例
const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => {
// 编写Vue实例(虚拟DOM节点)
const app = new Vue({
data: {
url: req.url
},
// 编写模板HTML的内容
template: `<div>访问的 URL 是: {{ url }}</div>`
})
// renderToString 是把Vue实例转化为真实DOM的关键方法
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
// 把渲染出来的真实DOM字符串插入HTML模板中
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`)
})
})
server.listen(8080)
实际项目比这些复杂很多,但万变不离其宗。强调的只有两点:
- 一是这个
renderToString()
方法; - 二是把转化结果
“塞”
进模板里的这一步。这两个操作是服务端渲染的灵魂操作。
在虚拟 DOM横行的当下,服务端渲染不再是早年JSP
里简单粗暴的字符串拼接过程,它还要求这一端要具备将虚拟 DOM
转化为真实 DOM
的能力。与其说是“把 JS 在服务器上先跑一遍”
,不如说是“把 Vue、React 等框架代码先在 Node 上跑一遍”
。