⚠️本文为掘金社区首发签约文章,未获受权禁止转载前端
在咱们网页刷新的时候,页面上全部数据都会被清空。而在一些网站的搜索上,即便是你关闭了浏览器,下次打开时仍是会有数据在页面上,以下图一个简单的搜索记录功能,当用户进行搜索时,全部的记录会被保存起来,不管是刷新仍是重启浏览器,搜索的历史记录依旧显示在页面上。java
这一系列的需求均可以经过浏览器的存储技术来实现。本篇文章,咱们就来学习下浏览器存储技术中的WebStorage
,全面了解它的基础使用和进阶,以及如何利用这些方法实践两个常见场景。介绍使用方法的同时,我也会以封装一个工具类
的方式来统一全部调用方法,学会这一点,可让你在业务开发调用的时候更加方便。web
咱们知道,常见的轻量浏览器存储技术包括Cookie
和WebStorage
。那么,咱们为何选择WebStorage
而不是Cookie
呢?面试
首先,WebStorage
在使用上相比Cookie更友好,再也不须要刻意封装成一些工具库来对一些常见的操做进行简化的调用,尽管市面上已经有不少成熟的方案帮咱们作了这件事情。chrome
其次,chrome(80+)
浏览器默认屏蔽全部三方Cookie
已经不是什么值得震惊的事情了,随着这一次改动,Cookie
无疑又被斩断了一只有力的手臂。 不了解的小伙伴,强烈安利一手这篇文章,里面很是详细的对其进行了一些分析。当浏览器全面禁用三方 Cookie。typescript
除此以外,使用Cookie
还须要面临如下问题:bootstrap
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
复制代码
对于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()
}
复制代码
WebStorage
自带length
属性,能够获取当前Storage
的长度。
window.localStorage.length
/** * 返回当前存储库大小 * @returns number */
size(): number {
return this.readStorage.length
}
复制代码
看到这里,不少朋友应该知道会怎么实现了吧?没错,经过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
则若是不手动清除,则不会主动清除存储的数据。
关键词,面试会问: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
能够获取到当前浏览器的一个缓存状况,如:使用多少, 总共多少。
以下代码,首先判断了浏览器是否存在navigator
,而后继续判断了navigator
是否有storage
,最后再去执行estimate
异步获取咱们的存储信息。
if (navigator && navigator.storage) {
navigator.storage.estimate().then(estimate => {
console.log(estimate)
});
}
复制代码
该Web API须要当前项目在https下。获取到的quota(存储总量)相对来讲在3M左右,在开发场景下,这绝对是一个安全的内存范围。
若是是在内存濒临溢出
的场景下,那么咱们就须要释放一些空间来作处理后面的数据修改了。 首先咱们对带有时间的数据进行汇总排序,以下方法就是将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]
当咱们有一个排序好的存储时,只须要循环判断当前空间是否知足需求便可,若是知足跳出循环。反之继续异步,直到咱们的空间够为止。
initCacheSize单纯对容量数据最一个刷新。获取新的容量数据。
/** * 容量清理,直到知足存储大小为止 */
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
中进行存储,方便须要用到时进行使用。
本文对WebStorage
中绝大部分使用技巧都作了一些使用的总结,将经常使用的一些操做存储方法都进行了封装,同时也对工做中常常碰到的一些复杂场景,如过时时间、数据更改、缓存溢出等功能进行了一些叙述,最后将其封装到了工具类 当中,方便在平常开发中进行调用。
最后在对WebStorage
有了一些了解以后,那么咱们在后续工做中,是否是能够思考有些数据能够考虑放到存储当中去?在节省资源的同时,也能有更好的性能,同时也缓解了部分服务端的压力。
若是本文对你有帮助,但愿可以给我点一赞支持一下。
本文首发于:掘金技术社区 类型:签约文章 做者:wangly19 收藏于专栏:javaScript基础进阶 公众号: ItCodes 程序人生