无论数据存储在 localStorage 还是 sessionStorage ,它们都特定于页面的协议。
由于localStorage
是基于当前访问源(origin)的本地存储空间,所以当我们在 a.jakeyu.top
中存储一段数据,并想要在 b.jakeyu.top
中读取数据的时候是无法取到的。
最近遇到这样的需求,考虑过 cookie 方案,但是可能存储大量的数据,cookie 不可行。最终我们使用iframe
来实现,我觉得这是一个很有趣的方法。
思路
a.jakeyu.top
和 b.jakeyu.top
通过 iframe
加载同一个域名的页面,并使用 postMessage 和 iframe
中的页面进行通信,这样就可以实现跨域名存取 localStorage。
缺点是 postMessage
是基于回调的,所以所有 api 都是异步的。不过我们有 promise
,可以让使用方式优雅一些。
实现
父级页面
创建 iframe
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function createIframe() { const iframeInBody = document.querySelector('#iframe') as HTMLIFrameElement;
if (iframeInBody) { return iframeInBody; }
const iframe = document.createElement('iframe'); iframe.setAttribute('id', '#iframe'); iframe.src = 'https://jakeyu.top/localstorage'; iframe.style.display = 'none';
document.body.insertAdjacentElement('beforeend', iframe);
return iframe; }
|
核心 Class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| class localStorage { iframe: HTMLIFrameElement;
isReady: Boolean;
waitMap: Map<string, Function>;
beforeReady: [Function?];
constructor() { this.listenMessage();
this.isReady = false;
this.beforeReady = [];
this.iframe = createIframe();
this.waitMap = new Map(); }
setItem(key: string, value: any) { const eventType = 'set'; const randomKey = this.getRandomString(eventType);
return new Promise((resolve) => { this.waitMap.set(randomKey, resolve);
this.postMessage({ eventType, key, value, randomKey }); }); }
listenMessage() { window.addEventListener('message', this.receiveMessage.bind(this), false); }
receiveMessage(event: MessageEvent) { const { data = {} } = event; if (typeof data === 'string') return;
const { eventType, randomKey, value } = data;
if (eventType === 'return') { const handler = this.waitMap.get(randomKey);
if (handler) { handler(value); this.waitMap.delete(randomKey); } } else if (eventType === 'ready') { this.isReady = true;
while (this.beforeReady.length) { const fun = this.beforeReady.shift() as Function; fun(); } } }
getRandomString(eventKey: string) { let randomString = ''; let eventKeyRandom = '';
do { randomString = makeRandomString(5); eventKeyRandom = `${eventKey}_${randomString}`; } while (this.waitMap.has(eventKeyRandom));
return eventKeyRandom; }
postMessage(params: Record<string, string>) { if (this.isReady) { (this.iframe.contentWindow as Window).postMessage(params, '*'); } else { this.beforeReady.push(() => { (this.iframe.contentWindow as Window).postMessage(params, '*'); }); } } }
|
iframe 页面
iframe 页面只需要通过 postMessage 和父级页面进行通信,所以并不需要 ui。
ready
页面加载完成时,需要通知父页面,并执行 before 栈中的函数。
1 2 3 4 5 6
| window.parent.postMessage( { eventType: 'ready', }, '*' );
|
监听消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| function receiveMessage(event) { const eventType = get(event, 'data.eventType', ''); const key = get(event, 'data.key', ''); const value = get(event, 'data.value', ''); const randomKey = get(event, 'data.randomKey', '');
if(eventType === 'set') { localStorage.setItem(key, value);
window.parent.postMessage( { eventType: 'return', value, randomKey, error, }, '*' ); } }
window.addEventListener('message', receiveMessage, false);
|
使用
在 a.jekeyu.top
中存储数据
1
| new localStorage().setItem('name', 'jake')
|
在 b.jekeyu.top
中存储数据
1
| const name = await new localStorage().getItem('name')
|