背景
在我们网页刷新的时候,页面上所有数据都会被清空。而在一些网站的搜索上,即使是你关闭了浏览器,下次打开时还是会有数据在页面上,如下图一个简单的搜索记录功能,当用户进行搜索时,所有的记录会被保存起来,不论是刷新还是重启浏览器,搜索的历史记录依旧显示在页面上。
这一系列的需求都可以通过浏览器的存储技术来实现。本篇文章,我们就来学习下浏览器存储技术中的WebStorage
,全面了解它的基础使用和进阶,以及如何利用这些方法实践两个常见场景。介绍使用方法的同时,我也会以封装一个工具类
的方式来统一所有调用方法,学会这一点,可以让你在业务开发调用的时候更加方便。
为什么选择WebStorage?
我们知道,常见的轻量浏览器存储技术包括Cookie
和WebStorage
。那么,我们为什么选择WebStorage
而不是Cookie
呢?
首先,WebStorage
在使用上相比Cookie更友好,不再需要刻意封装成一些工具库来对一些常见的操作进行简化的调用,尽管市面上已经有很多成熟的方案帮我们做了这件事情。
其次,chrome(80+)
浏览器默认屏蔽所有三方Cookie
已经不是什么值得震惊的事情了,随着这一次改动,Cookie
无疑又被斩断了一只有力的手臂。
不了解的小伙伴,强烈安利一手这篇文章,里面非常详细的对其进行了一些分析。当浏览器全面禁用三方 Cookie。
除此之外,使用Cookie
还需要面临以下问题:
- 存放数据太小,
Cookie
的存储大小只有4k
,如果你需要存储的数据非常多,那么很显然并不能够满足你的需求,且一般没有人这么做。 - 每次都会携带在
HTTP请求头
中,会与服务端进行一些交互,当我单纯存储一些本地数据时,很明显会造成性能浪费。
而WebStorage
在浏览器中的主要功能,就是在客户端进行临时和永久的数据存储,不直接参与服务端的通信和交互,因此可以很好地避免一些劫持的安全风险。同时,也具备了良好的存储容量,能胜任绝大部份的应用场景,且每个存储都是挂载在对应的空间当中,彼此独立去管理对应的数据,不会造成串数据和错数据的一些困扰。
基于此,如果有需要存储到本地的一些数据,还是尽可能使用WebStorage
来做为存储的首要选择。
基础使用
在本章节,我会从一个封装工具类的角度带大家学习一些webStorage的基础使用技巧。这里也先分享一个在线的源码地址链接:Storage操作封装实践代码
然后,在浏览器调试工具的Application
菜单当中,左侧可以看到Storage
的调试版,其中就有我们通过API
保存到存储当中的值,可以在这里进行调试。
环境支持 & 初始化
在开始前第一步肯定是需要做一些环境检查,不然在部分不支持这些特性的浏览器下是无法使用的,这个可以在caniuse上查看一些浏览器支持程度。
而在我们的代码中,也要加上一层容错判断,如果需要对其做兼容的话可以进行一个处理降解。如Cookie
或者是IE6中
的userData持久化用户数据
。
下面是一个比较简单的判断,也可以封装成为一个简单的函数来进行调用。如果浏览器不支持则抛出一些错误到控制台当中。
class CustomStorage {
private readStorage: Storage
constructor () {
if (!window) {
throw new Error('当前环境非浏览器,无法消费全局window实例。')
}
if (!window.localStorage) {
throw new Error('当前环境非无法使用localStorage')
}
if (!window.sessionStorage) {
throw new Error('当前环境非无法使用sessionStorage')
}
}
}
当环境支持使用WebStorage
的条件下,就可以初始化默认的一些数据了,在这里选择使用哪个Storage
,同时将配置保存起来。
interface StorageBootStrapConfig {
/** 当前环境 */
mode: 'session' | 'local',
/** 超时时间 */
timeout: number
}
/**
* 初始化Storage的数据
* @param config StorageBootStrapConfig
*/
bootStrap (config: StorageBootStrapConfig): void {
switch (config.mode) {
case 'session':
this.readStorage = window.sessionStorage
break;
case 'local':
this.readStorage = window.localStorage
break;
default:
throwErrorMessage('当前配置的mode未再配置区内,可以检查传入配置。')
break;
}
this.config = config
}
那么,通过bootstrap
来初始化当前的一些配置后,在页面里就可以通过当前实例customStorage
去使用一些函数方法。
import CustomStorage from 'web-storage-db'
const customStorage = new CustomStorage()
customStorage.bootStrap({
mode: 'local',
timeout: 3000,
})
export default customStorage
JSON序列化
对于WebStorage
来说,值的存储是非常依赖JSON的序列化
。如下图:
当存入Object类型
时,存入的数据会变成其类型的字符串,因为WebStorage
的存储只能以字符串
的形式存在,所以我们想要存储引用类型的数据,就需要依赖JSON序列化
的能力了。通过stringify
和parse
等一些方法对值做出处理,就能很好的存储一些引用类型。
但是也有一些JSON.stringify
不友好的类型数据,尽量不要去存储,如undefined
, Function
, Symbol
等等,我在这里也写了一个简单的函数用于检查存储值。
/**
* 判断当前值是否能够呗JSON.stringify识别
* @param data 需要判断的值
* @returns 前参数是否可以string化
*/
export function hasStringify (data: any): boolean {
if (data === undefined) {
return false
}
if (data instanceof Function) {
return false
}
if (isSymbol(data)) {
return false
}
return true
}
其中isSymbol
方法做了一个Symbol
类型值的判断。
/**
* 判断当前类型是否是Symbol
* @param val 需要判断的值
* @returns 当前参数是否是symbol
*/
export function isSymbol(val: any): boolean {
return typeof val === 'symbol'
}
存入数据
如果需要将数据存储到WebStorage
当中,其本身提供一个setItem
的API 来做这件事情,在这里以localStorage
为例子,可以通过以下形式来存入一个值:
// # 原生
window.localStorage.setItem('key', 'value')
// # attribute形式存储
window.localStorage['key1'] = 'value'
window.localStorage.name = 'wangly19'
而我们在使用中,显然不会去使用原生API
的方式处理,绝大部分都会封装成一个工具方法
,来处理一些重复性的工作。就比如在下面的封装中,我就对存储数据的内容做了一层包装,加入了JSON序列化数据
和过期时间
。
/**
* 设置当前
* @param key 设置当前存储key
* @param value 设置当前存储value
*/
setItem(key: string, value) {
if (hasStringify(value)) {
const saveData: StorageSaveFormat = {
timestamp: new Date().getTime(),
data: value
}
console.log(saveData, 'saveData')
this.readStorage.setItem(key, JSON.stringify(saveData))
} else {
throwErrorMessage('需要存储的data不支持JSON.stringify方法,请检查当前数据')
}
}
// 使用
customStorage.setItem('setItem', [1])
读取数据
既然有存入,那么必然会有读取,我们可以通过getItem
或者是Object
的形式进行值的读取。下面,我们就来看看三种方式的实例吧。
window.localStorage.setItem('person', JSON.stringify({
name: 'wangly19',
age: 22
}))
const person = window.localStorage.getItem('person')
JSON.parse(person)
// { name: "wangly19", age: 22 }
上面是普通的使用方式
,而我们封装
时,也会对存入的数据进行一些判断
,将存入的JSON数据做一个解析化的处理
,直接返回解析后的数据
,更加的方便和易于使用。
/**
* 获取数据
* @param key 获取当前数据key
* @returns 存储数据
*/
getItem<T = any>(key: string): T | null {
const content: StorageSaveFormat | null = JSON.parse(this.readStorage.getItem(key))
return content?.data || null
}
// # 使用
customStorage.getItem('setItem') // [1]
移除
对于存储的移除不仅可以使用removeItem
,delete
等操作来对存储中的值进行移除
。
// # removeItem
window.localStorage.removeItem('person')
// # delete
delete window.localStorage.person
delete window.localStorage['peson']
还可以使用clear来清除存储中所有的数据。
window.localStorage.clear()
此外,如果移除某条数据时Storage
没有存储当前key
的数据,那么我们就不需要去执行当前移除数据
的操作。我们来看下面封装的removeItem
方法,我加入了一层值是否存在
的判断来决定是不是真的需要执行移除这步操作。
/**
* 移除一条数据
* @param key 移除key
*/
removeItem(key: string) {
if (this.hasItem(key)) {
this.readStorage.removeItem(key)
}
}
/**
* 清除存储中所有数据
*/
clearAll() {
this.readStorage.clear()
}
长度length
WebStorage
自带length
属性,可以获取当前Storage
的长度。
window.localStorage.length
/**
* 返回当前存储库大小
* @returns number
*/
size(): number {
return this.readStorage.length
}
keys 和 values
看到这里,很多朋友应该知道会怎么实现了吧?没错,通过Object.keys
和Object.values
可以拿到当前Storage
中所有的key
和value
。部分埋点SDK
会有上报Storage
来做数据筛选。
Object.keys(localStorage)
// (4) ["wwwPassLogout", "BIDUPSID", "BDSUGSTORED", "safeIconHis"]
Object.values(localStorage)
// (4) ["0", "30B3EE0AF6EE9F4F89EF16486C288502", "[{\"q\":\"localstorage%20%E8%BF%87%E6%9C%9F%E6%97%B6%…:\"new%20dateshijianchuo\",\"s\":4,\"t\":206989341223}]", ""]
其次就是通过key(index)
方法,可以直接获取某个位置的值。
window.localStorage.key(0)
window.localStorage.key(1)
window.localStorage.key(2)
在工具类
当中,我也对其进行了封装
,可以使用getKeys
, getValues
来获取存储空间的所有Key
和Value
的集合。
/**
* 获取所有key
* @returns 回storage当中所有key集合
*/
getKeys(): Array < string > {
return Object.keys(this.readStorage)
}
/**
* 获取所有value
* @returns 所有数据集合
*/
getValues() {
return Object.values(this.readStorage)
}
// # 使用
customStorage.getKeys()
customStorage.getValues()
是否存在某个属性?
判断当前Storage
中是否存在某个属性,很多同学都是通过getItem
去获取一个值,然后判断value是否存在进行一个判断。
但是很显然,我们能够像操作Object的hasOwnProperty
方法来判断当前是否有这个属性,由于返回的是boolean类型,相对来说更易于理解。
localStorage.key(2)
// "BDSUGSTORED"
localStorage.hasOwnProperty('BDSUGSTORED')
// true
localStorage.hasOwnProperty('1111')
// false
基于此,我也封装了判断存储中是否存在该值的hasItem
方法,用于做一些key
是否在存储中存在的一些判断。
/**
* 判断是否存在该属性
* @param key 需要判断的key
*/
hasItem(key: string): boolean {
return this.readStorage.hasOwnProperty(key)
}
进阶使用
在进阶使用当中,我会介绍一些工作中可能会碰到的问题
,并且给出一些解决方案
。
过期时间
WebStorage
中SessionStorage
的一个周期是当前会话。而localStorage
则如果不手动清除,则不会主动清除存储的数据。
所以,很多时候如果需要过期时间则需要开发者自己去处理,而处理的方式也非常简单暴力。
那就是给予存储值时带一个时间。参考下面代码,通过new Date().getTime()
来取到当前时间,然后设置到存储当中去。
const person = {
// 存储数据
data: {
name: 'wangly19',
age: 22
},
// 过期时间
timestamp: new Date().getTime()
}
window.localStorage.setItem('person', JSON.stringify(person))
获取时间的时候,会进行一个简单的判断,当前时间 - 存储时间 >= 过期时间
,这样就能够在值操作的时候做一些判断处理。
// # 原生
let person = localStorage.getItem('person')
person = JSON.parse(val)
// 这里可以使用一些库在做处理,如`dayjs`
if(new Date().getTime() - person.timestamp > [过期时间]) {
// 数据已经过期的一些操作
} else {
// 正常处理
}
因此,需要在原有的getItem
的方法上,添加一条过期时间的判断,我也直接封装在函数内处理这一份逻辑。
/**
* 获取数据
* @param key 获取当前数据key
* @returns 存储数据
*/
getItem<T = any>(key: string): T | null {
const content: StorageSaveFormat | null = JSON.parse(this.readStorage.getItem(key))
if (content?.timestamp && new Date().getTime() - content.timestamp >= this.config.timeout) {
this.removeItem(key)
return null
}
return content?.data || null
}
监听函数
WebStorage
修改时,会触发浏览器storage
事件。
而在应用中可以使用addEventListener
添加一个storage
事件对其进行绑定。
而这个触发机制可以看下图。在不同窗口对storage
触发的时候会输出当前的event
信息。在event
当中,我们可以拿到触发的url
,新值
, 旧值
, 触发的key
等信息,我们可以通过这个API去做一些浏览器URL监听的事情。
<script>
document.body.innerHTML = '初始化数据'
window.addEventListener("storage", function (event) {
const values = {
url: event.url,
key: event.key,
old: event.oldValue,
new: event.newValue,
}
document.body.innerHTML = JSON.stringify(values)
});
</script>
修改数据
由于原生没有changeItem
这类的方法,因此我们需要自己去做一些方法的封装
来方便我们频繁的需要去修改存储当中数据。
如下面的一个类似于useState
回调的形式来做一些值的修改。
changeItem('name', (oldValue) => {
const name = `update: ${oldValue} update`
return name
})
实现方式也相对比较易懂,通过getItem
先获取数据,然后在通过setItem
设置onChange
回调函数的值,将一个连贯的操作串联起来。
/**
* 修改当前存储内容数据
* @param key 当前存储key
* @param onChange 修改函数
* @param baseValue 基础数据
*/
changeItem<S = any>(
key: string,
onChange: (oldValue: S) => S | null, baseValue?: any
) {
const data = this.getItem<S>(key)
this.setItem(key, onChange(data || baseValue))
}
// # 使用
customStorage.changeItem('key', (oldValue) => {
retutn oldValue + 'newUpadte'
})
空间 & 溢出
如果是重度使用用户,如一些文档构建项目,往往很多都是会往localStorage
中存很多数据,很多开发者都会担心会不会直接溢出
。
所以在这里,也设想了一些解决方案来处理这些问题。
存储状态 & StorageEstimate
在安全的上下文和支持的浏览器下,通过StorageEstimate
可以获取到当前浏览器的一个缓存情况,如:使用多少, 总共多少。
如下代码,首先判断了浏览器是否存在navigator
,然后继续判断了navigator
是否有storage
,最后再去执行estimate
异步获取我们的存储信息。
if (navigator && navigator.storage) {
navigator.storage.estimate().then(estimate => {
console.log(estimate)
});
}
缓存溢出清理
如果是在内存濒临溢出
的场景下,那么我们就需要释放一些空间来做处理后面的数据修改了。
首先我们对带有时间的数据进行汇总排序,如下方法就是将storage
中所有带有timestamp
字段的数据汇总后进行排序。
/**
* 获取当前清除存储空间,并且进行排序
*/
getClearStorage() {
const keys: string[] = Object.keys(this.readStorage)
const db: Array<{
key: string,
data: StorageSaveFormat
}> = []
keys.forEach(name => {
const item = this.getItem(name)
if (item.timestamp) {
db.push({
key: name,
data: item
})
}
})
return db.sort((a, b) => {
return a.data.timestamp - b.data.timestamp
})
}
当拥有了一个排序好的数据列表时,就需要考虑数据清空了,按照时间线将距离当前越久的时间清除。而这个时候,需要理解一个条件:
总大小(quota) - (使用大小)usage > [当前存入大小currentSize]
当我们有一个排序好的存储时,只需要循环判断当前空间是否满足需求即可,如果满足跳出循环。反之继续异步,直到我们的空间够为止。
/**
* 容量清理,直到满足存储大小为止
*/
detectionStorageContext(currentSize: number) {
if (this.usage + currentSize >= this.quota) {
const storage = this.getClearStorage()
for (let { key, data } of storage) {
// 如果满足要求就跳出,还不够就继续清除。
if (this.usage + currentSize < this.quota) break
// 刷新容量大小
this.removeItem(key)
initCacheSize()
}
}
}
最后一步就是在setItem
中执行detectionStorageContext
, 每次更新存储内容都会先判断下是否要溢出
,如果添加或者修改的数据会溢出,那么我就会做一个空间清理
了。
实践场景
本章节,主要讲述了一些简单的WebStorage
的使用场景。
搜索历史
到这里,我们的一个工具类
就已经基本成型了。最后,再回到一开始的案例
中,我们就可以通过工具类中的changItem
迅速的实现这个搜索历史的功能,而不必关心一些数据兼容上的问题。我们需要关注的只是存储值的设置。
事例代码如下:
export default function Search() {
const [searchList, setSearchList] = useState([]);
useEffect(() => {
const data = localStore.getItem('search')
setSearchList(data || [])
}, [])
const onSearch = (value) => {
if (value) {
localStore.changeItem(
'search',
(oldValue) => {
if (oldValue.includes(value)) {
return oldValue;
}
if (oldValue) {
const newValue = [...oldValue, value];
setSearchList(newValue);
console.log(newValue, 'value');
return newValue;
}
if (value) {
setSearchList([value]);
return [value];
}
return [];
},
[],
);
}
};
return (
<div className="demo-app">
<Search
placeholder="请输入搜索内容"
enterButton="Search"
size="large"
suffix={suffix}
onSearch={onSearch}
/>
<div className="tag-wrapper">
{searchList.map((e) => {
return (
<Tag
key={e}
style={{
margin: 10,
}}
color="#108ee9"
>
{e}
</Tag>
);
})}
</div>
</div>
);
}
图片数据
浏览器对于请求是有限制的,而我们项目中绝大部份图片其实是通过后端接口进行返回的,在这里以emoji
表情包做个例子。
我们拿知乎的表情包数据来进行一个模拟,发现一共有73条数据,如果每次刷新网页都请求一次后端数据是一件非常难受的事情,而这些数据显然也不需要存放在Store
当中,在一定的时间中,发生改变的几率很小,那么我们将它放在本地存储
显然是一个不错的选择。
在页面加载时,我会对接口数据请求加一层判断,只有数据为空
时才会请求后端图标数据列表。如果是过期时间的话,获取数据时会清空本地图标数据,然后重新请求后端图标数据,在重新放入缓存中并且更新新的过期时间。
const emojiRef = useRef(localStore.getItem('emoji'));
useEffect(() => {
if (!emojiRef.current) {
fetchEmojiIcon()
}
})
如果你项目中存在大量的资源路径,可以将其放在localStorage
中进行存储,方便需要用到时进行使用。
资源 & 资料
- 当浏览器全面禁用三方 Cookie
- localStorage、sessionStorage、cookie、session几种web数据存储方式对比总结
- 前端存储除了 localStorage 还有啥
- Web Storage (Second Edition)
- 项目实战|缓存处理
总结
本文对WebStorage
中绝大部分使用技巧都做了一些使用的总结,将常用的一些操作存储方法都进行了封装,同时也对工作中经常碰到的一些复杂场景,如过期时间、数据更改、缓存溢出等功能进行了一些叙述,最后将其封装到了工具类 当中,方便在日常开发中进行调用。
最后在对WebStorage
有了一些了解之后,那么我们在后续工作中,是不是可以思考有些数据可以考虑放到存储当中去?在节省资源的同时,也能有更好的性能,同时也缓解了部分服务端的压力。
近期好文
- 我 & 掘金,毕业一年后,我被掘金签约了|2021 年中总结
- 总结TypeScript在项目开发中的应用实践体会
尾注
本文首发于:掘金技术社区
类型:签约文章
作者:wangly19
收藏于专栏:javaScript基础进阶
公众号: ItCodes 程序人生
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!