import { ScopeContext, uuidV4 } from '@tencent/merlin-core'; import { BehaviorReporter } from './BehaviorReporter'; import { DEFAULT_PAGE_KEY } from './utils'; export interface PageItem { name: string; accessId: string; step: number; refAccessId?: string; refPageName?: string; fromElementId?: string; extInfo?: Record; } export interface PageInfo { name: string; extInfo?: Record; } /** 注意,目前不适用于异步 storage 环境 */ export class PageManager { /** 当前页面 TODO: 改为 protected */ currentPage?: PageItem; constructor(protected reporter: BehaviorReporter, protected pageKey = DEFAULT_PAGE_KEY) { try { // 删除旧版本的 Web 页面栈,如果 __ml_storage__queue 已经删掉,其他子项却没有删干净,其实会有问题,先不处理 if (globalThis.localStorage?.getItem('__ml_storage__queue')) { Object.keys(globalThis.localStorage).forEach((k) => { if (k === '__ml_storage__queue' || k.endsWith('__ml_storage__page')) { globalThis.localStorage.removeItem(k); } }); } } catch (e: any) { // 隐私模式下 iframe 访问 storage 会被拦截 reporter.errorHandler(e); } } /** * 将当前页面推入页面栈 * * from ~ to 场景下,应当在 to 的页面进行 push,确保 linkedData 正确更新 * @param item * @param fromElementId */ push(item: PageInfo, fromElementId?: string) { this.reporter.readLinkedData(); // 更新 linkedData,取到 contextId const prevPage = this.load(); // 多窗口共享页面栈的情况下,每次更新页面栈前需要同步一下状态 if (prevPage) { this.currentPage = { name: item.name, accessId: uuidV4(), step: prevPage.step + 1, refAccessId: prevPage.accessId, refPageName: prevPage.name, fromElementId, extInfo: item.extInfo, }; } else { this.currentPage = { name: item.name, accessId: uuidV4(), step: 1, extInfo: item.extInfo, }; } this.save(); } /** 拿到当前页面信息 */ getPageInfo(): ScopeContext { if (!this.currentPage) { throw new Error('currentPage not found'); } return { pageName: this.currentPage.name, refPageName: this.currentPage.refPageName, accessId: this.currentPage.accessId, refAccessId: this.currentPage.refAccessId, fromElementId: this.currentPage.fromElementId, step: this.currentPage.step, extInfo: this.currentPage.extInfo, }; } /** 更新当前页面的 extInfo */ updatePageInfo(extInfo?: Record) { if (!this.currentPage) { // TODO: 是不是该统一处理对外暴露的方法? this.reporter.errorHandler(new Error('currentPage not found')); return; } this.currentPage.extInfo = extInfo; } init() { this.load(); } get contextId() { return this.reporter.scopeInfo.contextId; } load(): PageItem | undefined { if (!this.reporter.supportSyncStorage) { // 不支持同步存储,直接返回 return undefined; } const contextIdQueue = this.reporter.getStorageItem(this.pageKey, []); // 删除不存在于当前回收队列中的页面栈 TODO: 对其他 storage 机制如何处理? try { if (localStorage && contextIdQueue) { Object.keys(localStorage).forEach((k) => { if (k.startsWith(`${this.pageKey}_`) && !contextIdQueue.includes(k.replace(`${this.pageKey}_`, ''))) { this.reporter.log(`删除遗留页面栈 ${k}`); this.reporter.removeStorageItem(k); } }); } } catch (e: any) { // 隐私模式下 iframe 访问 storage 会被拦截 this.reporter.errorHandler(e); } // 运行时类型校验,确保是预期的 PageItem 对象 const mayBePageItem = this.reporter.getStorageItem(this.getStorageKey(this.contextId), undefined); if ( mayBePageItem && typeof mayBePageItem === 'object' && Object.prototype.hasOwnProperty.call(mayBePageItem, 'accessId') && Object.prototype.hasOwnProperty.call(mayBePageItem, 'name') && Object.prototype.hasOwnProperty.call(mayBePageItem, 'step') ) { return mayBePageItem; } return undefined; } protected getStorageKey(contextId: string | undefined = this.contextId) { return `${this.pageKey}_${contextId || ''}`; } protected save() { if (!this.reporter.supportSyncStorage || !this.currentPage || !this.contextId) return; try { /** 页面栈队列 */ const contextIdQueue = this.reporter.getStorageItem(this.pageKey, []); this.reporter.log('读取回收队列', contextIdQueue); // 若当前 contextId 已存在于队列中,更新对应值后放到队尾 if (contextIdQueue.includes(this.contextId)) { // 若当前 contextId 不在队尾,将其移动到队尾 if (contextIdQueue.indexOf(this.contextId) !== contextIdQueue.length - 1) { contextIdQueue.splice(contextIdQueue.indexOf(this.contextId), 1); contextIdQueue.push(this.contextId); this.reporter.log('contextId 更新至队尾', contextIdQueue); this.reporter.setStorageItem(this.pageKey, contextIdQueue); } // 已经在队尾了,不需要操作队列 } else { if (contextIdQueue.length >= this.reporter.pageStackStorageLimit) { // 队列超过上限,队首出队 const contextIdToBeRemoved = contextIdQueue.shift(); if (contextIdToBeRemoved) { this.reporter.log('存储超过上限,出队', contextIdToBeRemoved); this.reporter.removeStorageItem(this.getStorageKey(contextIdToBeRemoved)); } } // 加入新 contextId 并更新队列 contextIdQueue.push(this.contextId); this.reporter.log('更新队列', contextIdQueue); this.reporter.setStorageItem(this.pageKey, contextIdQueue); } // 加入/更新新的 contextId 栈,不存储 extInfo this.reporter.log('写入新的 contextId', this.contextId); this.reporter.setStorageItem(this.getStorageKey(this.contextId), { name: this.currentPage.name, accessId: this.currentPage.accessId, step: this.currentPage.step, refAccessId: this.currentPage.refAccessId, fromElementId: this.currentPage.fromElementId, refPageName: this.currentPage.refPageName, }); } catch (e: any) { // 22 === DOMException.QUOTA_EXCEEDED_ERR if (e?.code === 22 || e?.message?.toLowerCase().includes('quota')) { // 处理 storage 满了的情况:强制将队列减半,以防阻塞业务中 storage 的使用 const contextIdQueue = this.reporter.getStorageItem(this.pageKey, []); const remain = Math.floor(contextIdQueue.length / 2); const newContextIdQueue = this.recycleStorage(contextIdQueue, remain); // TODO: 重试 this.reporter.setStorageItem(this.pageKey, newContextIdQueue); } } } /** 回收 storage 中的页面栈,保留最近 remainCount 项 */ private recycleStorage(contextIdQueue: string[], remainCount: number) { if (contextIdQueue.length <= remainCount) return contextIdQueue; this.reporter.log(`回收页面栈开始,当前队列长度 ${contextIdQueue.length}`); const newContextIdQueue = [...contextIdQueue]; const contextIdsToRemove = newContextIdQueue.splice(remainCount); contextIdsToRemove.forEach((contextId) => { // 逐个删除对应 storage 项 this.reporter.log(`删除页面栈 ${contextId}`); this.reporter.removeStorageItem(this.getStorageKey(contextId)); }); this.reporter.log(`回收页面栈结束,当前队列长度 ${newContextIdQueue.length}`); return newContextIdQueue; } }