跨域名存取localStorage

无论数据存储在 localStorage 还是 sessionStorage ,它们都特定于页面的协议。

由于localStorage是基于当前访问源(origin)的本地存储空间,所以当我们在 a.jakeyu.top 中存储一段数据,并想要在 b.jakeyu.top 中读取数据的时候是无法取到的。

最近遇到这样的需求,考虑过 cookie 方案,但是可能存储大量的数据,cookie 不可行。最终我们使用iframe来实现,我觉得这是一个很有趣的方法。

思路

a.jakeyu.topb.jakeyu.top 通过 iframe 加载同一个域名的页面,并使用 postMessageiframe 中的页面进行通信,这样就可以实现跨域名存取 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;

// iframe 是否加载完成
isReady: Boolean;

// 同时调用方法时,需要在 iframe 回调之后执行 reslove
waitMap: Map<string, Function>;

// 在 iframe ready之前调用方法,需要保存一下,ready后执行
beforeReady: [Function?];

constructor() {
this.listenMessage();

this.isReady = false;

this.beforeReady = [];

this.iframe = createIframe();

this.waitMap = new Map();
}

/**
* 设置数据
* @param key
* @param value
*/
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() {
// 接收 iframe 消息
window.addEventListener('message', this.receiveMessage.bind(this), false);
}

/**
* 处理接收消息
* @param event
*/
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();
}
}
}

/**
* 获取随机字符串
* @param eventKey
*/
getRandomString(eventKey: string) {
let randomString = '';
let eventKeyRandom = '';

do {
randomString = makeRandomString(5);
eventKeyRandom = `${eventKey}_${randomString}`;
} while (this.waitMap.has(eventKeyRandom));

return eventKeyRandom;
}

/**
* 向iframe中发送消息
* @param params
*/
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', '');
// 数据 key
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')