在某些开发场合下,有些接口请求是不会变化或很少发生变化,我们希望能把接口返回的数据进行缓存,不用频繁去请求该数据,避免浪费请求资源。那么我们该如何实现呢,本文以Angular环境为例实现简易版cache.service,其它环境自行修改接口请求方案即可。cache.service具有的功能如下:
cache.service.ts
import { Inject, Injectable, OnDestroy } from '@angular/core'; import { BehaviorSubject, Observable, of } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import { addSeconds } from 'date-fns'; import type { NzSafeAny } from 'ng-zorro-antd/core/types'; import { CacheNotifyResult, CacheNotifyType, ICache, ICacheStore } from './interface'; import { DC_STORE_STORAGE_TOKEN } from './local-storage-cache.service'; import { DsmCacheConfig } from './cache.type'; import { BaseHttp } from '@app/core/http/baseHttp'; @Injectable({ providedIn: 'root' }) export class CacheService implements OnDestroy { private readonly memory: Map<string, ICache> = new Map<string, ICache>(); private readonly notifyBuffer: Map<string, BehaviorSubject<CacheNotifyResult>> = new Map< string, BehaviorSubject<CacheNotifyResult> >(); private meta: Set<string> = new Set<string>(); private freqTick = 3000; private freqTime: NzSafeAny; private cog: DsmCacheConfig; constructor( @Inject(DC_STORE_STORAGE_TOKEN) private store: ICacheStore, private http: BaseHttp ) { this.cog = { mode: 'promise', reName: "", prefix: '', meta_key: '__cache_meta' } this.loadMeta(); this.startExpireNotify(); } private deepGet(obj: NzSafeAny, path: string[], defaultValue?: NzSafeAny): NzSafeAny { if (!obj) return defaultValue; if (path.length <= 1) { const checkObj = path.length ? obj[path[0]] : obj; return typeof checkObj === 'undefined' ? defaultValue : checkObj; } return path.reduce((o, k) => o[k], obj) || defaultValue; } // #region meta private pushMeta(key: string): void { if (this.meta.has(key)) return; this.meta.add(key); this.saveMeta(); } private removeMeta(key: string): void { if (!this.meta.has(key)) return; this.meta.delete(key); this.saveMeta(); } private loadMeta(): void { const ret = this.store.get(this.cog.meta_key!); if (ret && ret.v) { (ret.v as string[]).forEach(key => this.meta.add(key)); } } private saveMeta(): void { const metaData: string[] = []; this.meta.forEach(key => metaData.push(key)); this.store.set(this.cog.meta_key!, { v: metaData, e: 0 }); } getMeta(): Set<string> { return this.meta; } // #endregion // #region set /** * 缓存对象是 `Observable`, for example: * - `set('data/1', this.http.get('data/1')).subscribe()` * - `set('data/1', this.http.get('data/1'), { expire: 10 }).subscribe()` */ set<T>(key: string, data: Observable<T>, options?: { type?: 's'; expire?: number,isSave?: (res) => boolean }): Observable<T>; /** * 缓存对象是 `Observable`, for example: * - `set('data/1', this.http.get('data/1')).subscribe()` * - `set('data/1', this.http.get('data/1'), { expire: 10 }).subscribe()` */ set(key: string, data: Observable<NzSafeAny>, options?: { type?: 's'; expire?: number,isSave?: (res) => boolean }): Observable<NzSafeAny>; /** * 缓存对象是普通数据, for example: * - `set('data/1', 1)` * - `set('data/1', 1, { expire: 10 })` */ set(key: string, data: unknown, options?: { type?: 's'; expire?: number,isSave?: (res) => boolean }): void; /** * 缓存对象是普通数据,自定义缓存模式 for example : * - `set('data/1', 1, { type: 'm' })` * - `set('data/1', 1, { type: 'm', expire: 10 })` */ set(key: string, data: unknown, options: { type: 'm' | 's'; expire?: number,isSave?: (res) => boolean }): void; /** * 缓存对象 */ set( key: string, data: NzSafeAny | Observable<NzSafeAny>, options: { /** 存储类型,'m' 表示内存,'s' 表示持久 */ type?: 'm' | 's'; /** * 过期时间,单位 `秒` */ expire?: number; /** 是否持久化保存,对接口返回数据 自定义判断是否要持久化 */ isSave?: (res) => boolean } = {} ): NzSafeAny { if(options.isSave && !options.isSave(data)){ return } let e = 0; const { type, expire } = this.cog; options = { type, expire, ...options }; if (options.expire) { e = addSeconds(new Date(), options.expire).valueOf(); } if (!(data instanceof Observable)) { this.save(options.type!, key, { v: data, e }); return; } return data.pipe( tap((v: NzSafeAny) => { this.save(options.type!, key, { v, e }); }) ); } private save(type: 'm' | 's', key: string, value: ICache): void { if (type === 'm') { this.memory.set(key, value); } else { this.store.set(this.cog.prefix + key, value); this.pushMeta(key); } this.runNotify(key, 'set'); } // #endregion // #region get /** 获取缓存数据 */ get<T>( key: string, options?: { mode: 'promise'; type?: 'm' | 's'; expire?: number; request?:DsmHttpRequest; isSave?: (res) => boolean; } ): Observable<T>; /** 获取缓存数据 */ get( key: string, options?: { mode: 'promise'; type?: 'm' | 's'; expire?: number; request?:DsmHttpRequest; isSave?: (res) => boolean; } ): Observable<NzSafeAny>; /** 获取缓存数据 */ get( key: string, options: { mode: 'none'; type?: 'm' | 's'; expire?: number; request?:DsmHttpRequest; isSave?: (res) => boolean; } ): NzSafeAny; get( key: string, options: { mode?: 'promise' | 'none'; type?: 'm' | 's'; expire?: number; request?:DsmHttpRequest; isSave?: (res) => boolean; } = {} ): Observable<NzSafeAny> | NzSafeAny { const isPromise = options.mode !== 'none' && this.cog.mode === 'promise'; const value = this.memory.has(key) ? (this.memory.get(key) as ICache) : this.store.get(this.cog.prefix + key); if (!value || (value.e && value.e > 0 && value.e < new Date().valueOf())) { if (isPromise) { return (this.cog.request ? this.cog.request(key) : this.http.request(options.request)).pipe( map((ret: NzSafeAny) => this.deepGet(ret, this.cog.reName as string[], null)), tap(v => this.set(key, v, { type: options.type as NzSafeAny, expire: options.expire,isSave:options.isSave })) ); } return null; } return isPromise ? of(value.v) : value.v; } /** 获取缓存数据,若 `key` 不存在或已过期则返回 null */ getNone<T>(key: string): T; /** 获取缓存数据,若 `key` 不存在或已过期则返回 null */ getNone(key: string): NzSafeAny { return this.get(key, { mode: 'none' }); } /** * 获取缓存,若不存在则设置持久化缓存 `Observable` 对象 */ tryGet<T>(key: string, data: Observable<T>, options?: { type?: 's'; expire?: number }): Observable<T>; /** * 获取缓存,若不存在则设置持久化缓存 `Observable` 对象 */ tryGet(key: string, data: Observable<NzSafeAny>, options?: { type?: 's'; expire?: number }): Observable<NzSafeAny>; /** * 获取缓存,若不存在则设置持久化缓存基础对象 */ tryGet(key: string, data: unknown, options?: { type?: 's'; expire?: number }): NzSafeAny; /** * 获取缓存,若不存在则设置指定缓存类型进行缓存对象 */ tryGet(key: string, data: unknown, options: { type: 'm' | 's'; expire?: number }): NzSafeAny; /** * 获取缓存,若不存在则设置缓存对象 */ tryGet( key: string, data: NzSafeAny | Observable<NzSafeAny>, options: { /** 存储类型,'m' 表示内存,'s' 表示持久 */ type?: 'm' | 's'; /** * 过期时间,单位 `秒` */ expire?: number; } = {} ): NzSafeAny { const ret = this.getNone(key); if (ret === null) { if (!(data instanceof Observable)) { this.set(key, data, options as NzSafeAny); return data; } return this.set(key, data as Observable<NzSafeAny>, options as NzSafeAny); } return of(ret); } // #endregion // #region has /** 是否缓存 `key` */ has(key: string): boolean { return this.memory.has(key) || this.meta.has(key); } // #endregion // #region remove private _remove(key: string, needNotify: boolean): void { if (needNotify) this.runNotify(key, 'remove'); if (this.memory.has(key)) { this.memory.delete(key); return; } this.store.remove(this.cog.prefix + key); this.removeMeta(key); } /** 移除缓存 */ remove(key: string): void { this._remove(key, true); } /** 清空所有缓存 */ clear(): void { this.notifyBuffer.forEach((_v, k) => this.runNotify(k, 'remove')); this.memory.clear(); this.meta.forEach(key => this.store.remove(this.cog.prefix + key)); } // #endregion // #region notify /** * 设置监听频率,单位:毫秒且最低 `20ms`,默认:`3000ms` */ set freq(value: number) { this.freqTick = Math.max(20, value); this.abortExpireNotify(); this.startExpireNotify(); } private startExpireNotify(): void { this.checkExpireNotify(); this.runExpireNotify(); } private runExpireNotify(): void { this.freqTime = setTimeout(() => { this.checkExpireNotify(); this.runExpireNotify(); }, this.freqTick); } private checkExpireNotify(): void { const removed: string[] = []; this.notifyBuffer.forEach((_v, key) => { if (this.has(key) && this.getNone(key) === null) removed.push(key); }); removed.forEach(key => { this.runNotify(key, 'expire'); this._remove(key, false); }); } private abortExpireNotify(): void { clearTimeout(this.freqTime); } /** * 监听通知 * @param key * @param type * @returns */ private runNotify(key: string, type: CacheNotifyType): void { if (!this.notifyBuffer.has(key)) return; this.notifyBuffer.get(key)!.next({ type, value: this.getNone(key) }); } /** * `key` 监听,当 `key` 变更、过期、移除时通知,注意以下若干细节: * * - 调用后除再次调用 `cancelNotify` 否则永远不过期 * - 监听器每 `freq` (默认:3秒) 执行一次过期检查 */ notify(key: string): Observable<CacheNotifyResult> { if (!this.notifyBuffer.has(key)) { const change$ = new BehaviorSubject<CacheNotifyResult>(this.getNone(key)); this.notifyBuffer.set(key, change$); } return this.notifyBuffer.get(key)!.asObservable(); } /** * 取消 `key` 监听 */ cancelNotify(key: string): void { if (!this.notifyBuffer.has(key)) return; this.notifyBuffer.get(key)!.unsubscribe(); this.notifyBuffer.delete(key); } /** `key` 是否已经监听 */ hasNotify(key: string): boolean { return this.notifyBuffer.has(key); } /** 清空所有 `key` 的监听 */ clearNotify(): void { this.notifyBuffer.forEach(v => v.unsubscribe()); this.notifyBuffer.clear(); } // #endregion ngOnDestroy(): void { this.memory.clear(); this.abortExpireNotify(); this.clearNotify(); } }
import { HttpClient, HttpParams } from "@angular/common/http" import { Injectable } from "@angular/core"; import { environment } from "@env/environment"; import * as Qs from 'qs'; type METHOD = "delete" | "get" | "head" | "jsonp" | "options" | "patch" | "post" | "put" type DsmHttpRequest = { url:string method?: METHOD data?:any isFormData?: boolean withToken?: boolean options?:any } @Injectable({ providedIn: 'root' }) export class BaseHttp { constructor(private http: HttpClient) { } public getParams(data: any, isJsonp = false): HttpParams { let params = new HttpParams(); for (const key in data) { let val: any = data[key] if (typeof val == 'function') { continue; } params = params.set(key, val) } if(isJsonp){ params = params.set('callback','JSON_CALLBACK') } return params; } public request({url, method = "get", data, isFormData = false, withToken = true,options = {}}:DsmHttpRequest) { url = this.handleUrlPrefix(url) let header: { [name: string]: string } = {} if (method == "post" || method == "put") { if (isFormData) { header['Content-Type'] = 'application/x-www-form-urlencoded' } else { header['Content-Type'] = 'application/json' } } if(environment.environment !== "production"){ header['principal'] = '5TPh_Xr5R8CGD2tIBhGJLg' } header['Accept'] = '*/*' // 是否需要向headers追加token if (withToken) { header['token'] = "xxx" } options['headers'] = header if(method === "get" || method === "delete" || method === "head" || method === "options"){ let obj = Object.assign({}, options, { params: this.getParams(data) }) return this.http[method](url, obj) }else if (method === "jsonp"){ return this.http[method](url, 'JSON_CALLBACK') }else{ if (isFormData) { data = Qs.stringify(data, { arrayFormat: 'repeat' }) } return this.http[method](url, data, options) } } public get({url,data = {},withToken = true,options = {}}:DsmHttpRequest) { return this.request({url,method:"get",data,isFormData:false,withToken,options}) } public delete({url,data = {},withToken = true,options = {}}:DsmHttpRequest) { return this.request({url,method:"delete",data,isFormData:false,withToken,options}) } /** * * @param url 请求api接口路径 * @param data 请求参数对象 * @param isFormData 是否以表单体提交,默认false 该参数影响 Content-Type 的参数值,isFormData = true,则 Content-Type = 'application/x-www-form-urlencoded' 否则 Content-Type = 'application/json;charset=UTF-8' * @param withToken 是否在header追加token 默认true * @returns */ public post({url,data = {},isFormData = false, withToken = true,options = {}}:DsmHttpRequest) { return this.request({url,method:"post",data,isFormData,withToken,options}) } public put({url,data = {},isFormData = false, withToken = true}:DsmHttpRequest) { return this.request({url,method:"put",data,isFormData,withToken}) } public patch(url: string, data = {}, isFormData = false, withToken = true) { return this.request({url,method:"patch",data,isFormData,withToken}) } /** * 处理url前缀问题 * @param url * @returns */ public handleUrlPrefix(url:string){ const urlPrefix = environment.urlPrefix; if(url.startsWith('./')){ url = url.substring(1) } if(!url.startsWith('/')){ url = "/" + url } const ignore = ["/portal","/asset"] if(!ignore.includes("/" + url.split("/")[1])){ url = url.startsWith('/') ? (urlPrefix + url) : (urlPrefix + '/' + url) } return url } public convertRes2Blob(response: any): Error | null { if (!response.headers.has("content-disposition")) { const blob = new Blob([response.data], { type: 'application/octet-stream' }) const f = new FileReader() f.readAsText(blob, "UTF-8") f.onload = (evt: any) => { console.log(evt); const re = evt.target.result console.log(re); const result = JSON.parse(re) return result } } const fileName = this.getFileName(response) const blob = new Blob([response.body], { type: 'application/octet-stream' }) if (typeof window.navigator.msSaveBlob !== 'undefined') { window.navigator.msSaveBlob(blob, fileName) return null } else { const blobUrl = window.URL.createObjectURL(blob) const tempLink = document.createElement('a') tempLink.style.display = 'none' tempLink.href = blobUrl tempLink.setAttribute('download', fileName) document.body.appendChild(tempLink) tempLink.click() document.body.removeChild(tempLink) window.URL.revokeObjectURL(blobUrl) return null } } private getFileName(response: any) { debugger const encode = response.headers.get('content-type')?.match(/charset=(.*)/) ? response.headers.get('content-type').match(/charset=(.*)/)[1] : null let fileName: string = response.headers.get('content-disposition').match(/filename=(.*)/)[1].replaceAll("\"", "") if (encode && encode == 'ISO8859-1') { const fn = escape(fileName) fileName = decodeURI(escape(fileName)).replace(new RegExp("%3A", "gm"), ":") } else { fileName = decodeURI(fileName) } return fileName } }
import type { NzSafeAny } from 'ng-zorro-antd/core/types'; export interface ICache { v: NzSafeAny; /** 过期时间戳,`0` 表示不过期 */ e: number; } export interface ICacheStore { get(key: string): ICache | null; set(key: string, value: ICache): boolean; remove(key: string): void; } export type CacheNotifyType = 'set' | 'remove' | 'expire'; export interface CacheNotifyResult { type: CacheNotifyType; value?: NzSafeAny; }
import { Observable } from 'rxjs'; export interface DsmCacheConfig { /** * 缓存模式, 默认值: `promise` * - `promise` 可以获取远程数据 * - `none` 正常模式 */ mode?: 'promise' | 'none'; /** * 对返回数据重命名default: ``, for example: * - `null` 把接口返回内容整体缓存 * - `list` 接口返回体应该符合如下格式 `{ list: [] }` * - `result.list` The response body should be `{ result: { list: [] } }` */ reName?: string | string[]; /** * 缓存流向 * - `m` 缓存到内存 * - `s` 缓存到`localStorage` */ type?: 'm' | 's'; /** * 过期时间,单位 秒 */ expire?: number; /** * 持久化key的前缀,防止key冲突,默认`` */ prefix?: string; /** * 持久化数据的键名集合 default: `__cache_meta` */ meta_key?: string; /** * 自定义请求体 * * Custom request */ request?: (key: string) => Observable<unknown>; }
import { Platform } from '@angular/cdk/platform'; import { inject, InjectionToken } from '@angular/core'; import { ICache, ICacheStore } from './interface'; export const DC_STORE_STORAGE_TOKEN = new InjectionToken<ICacheStore>('DC_STORE_STORAGE_TOKEN', { providedIn: 'root', factory: () => new LocalStorageCacheService(inject(Platform)) }); export class LocalStorageCacheService implements ICacheStore { constructor(private platform: Platform) {} get(key: string): ICache | null { if (!this.platform.isBrowser) { return null; } return JSON.parse(localStorage.getItem(key) || 'null') || null; } set(key: string, value: ICache): boolean { if (!this.platform.isBrowser) { return true; } localStorage.setItem(key, JSON.stringify(value)); return true; } remove(key: string): void { if (!this.platform.isBrowser) { return; } localStorage.removeItem(key); } }
import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { CacheService } from '@app/core/cache-service'; import { HttpRequest } from '@app/core/httpRequest'; @Injectable({ providedIn: 'root' }) export class AccountAssetService extends AssetBaseService { constructor(public req: HttpRequest, cache:CacheService) { } /** 通过字典类型获取字典表 */ public getDictListByTypeApi(dictionaryCode) { return new Promise((resolve,reject) => { this.cache.get(dictionaryCode,{ mode: 'promise', request:{ url: "sysManager/configDictionaryManageMent/findDictionaryValueListByCode", data: { dictionaryCode } }, expire:60 * 30, // 过期时间30分钟 isSave: res => res.resultStat === "0" }).subscribe(data => { return resolve(data) },err => { reject(err) }) }) } }
当缓存key状态变化时,发起通知
import { Component, OnDestroy } from '@angular/core'; import { CacheService } from '@app/cache'; import { NzMessageService } from 'ng-zorro-antd/message'; import { Subscription } from 'rxjs'; @Component({ selector: 'cache-service-simple', template: ` <p>value: {{ value | json }}</p> <div class="pt-sm"> Basic: <button nz-button (click)="srv.set(key, newValue)">Set</button> <button nz-button (click)="value = srv.getNone(key)">Get</button> <button nz-button (click)="srv.remove(key)">Remove</button> <button nz-button (click)="srv.clear()">Clear</button> </div> <div class="pt-sm"> Notify: <button nz-button (click)="registerNotify()">Register</button> <button nz-button (click)="unRegisterNotify()">UnRegister</button> </div> `, }) export class CacheServiceSimpleComponent implements OnDestroy { value: any; key = 'demo'; private notify$?: Subscription; get newValue(): number { return +new Date(); } constructor(public srv: CacheService, private msg: NzMessageService) {} registerNotify(): void { if (this.notify$) this.notify$.unsubscribe(); this.notify$ = this.srv.notify(this.key).subscribe(res => { if (res == null) { this.msg.success('register success'); return; } this.msg.warning(`"${this.key}" new status: ${res.type}`); }); } unRegisterNotify(): void { this.srv.cancelNotify(this.key); } ngOnDestroy(): void { if (this.notify$) this.notify$.unsubscribe(); } }


本文作者:千寻
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!