跨域名存取localStorage

无论数据存储在 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;

// 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')