Fork me on GitHub

实现本地跨域存储

作者:gauseen
原文: https://github.com/gauseen/blog

什么是跨域?

先看一下 URL 有哪些部分组成,如下:

1
2
3
4
https://github.com:80/gauseen/blog?issues=1#note
\___/ \________/ \_/ \_________/ \______/ \___/
| | | | | |
protocol host port pathname search hash

protocol(协议)、host(域名)、port(端口)有一个地方不同都会产生跨域现象,也被称为客户端同源策略

本地存储受同源策略限制

客户端(浏览器)出于安全性考虑,无论是 localStorage 还是 sessionStorage 都会受到同源策略限制。

那么如何实现跨域存储呢?

window.postMessage()

想要实现跨域存储,先找到一种可跨域通信的机制,没错,就是 postMessage,它可以安全的实现跨域通信,不受同源策略限制。

语法:

1
otherWindow.postMessage('message', targetOrigin, [transfer])
  • otherWindow 窗口的一个引用,如:iframecontentWindow 属性,当前 window 对象,window.open 返回的窗口对象等
  • message 将要发送到 otherWindow 的数据
  • targetOrigin 通过窗口的 targetOrigin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串 "*"(表示无限制)

实现思路

postMessage 可跨域特性,来实现跨域存储。因为多个不同域下的页面无法共享本地存储数据,我们需要找个“中转页面”来统一处理其它页面的存储数据。为了方便理解,画了张时序图,如下:

跨域存储时序图

场景模拟

需求:

有两个不同的域名(http://localhost:6001http://localhost:6002)想共用本地存储中的同一个 token

假设:

http://localhost:6001 对应 client1.html 页面
http://localhost:6002 对应 client2.html 页面
http://localhost:6003 对应 hub.html 中转页面

启动服务:

使用 http-server 启动 3 个本地服务

1
2
3
4
5
6
npm -g install http-server

# 启动 3 个不同端口的服务,模拟跨域现象
http-server -p 6001
http-server -p 6002
http-server -p 6003

简单实现版本

client1.html 页面代码

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
<body>
<!-- 开始存储事件 -->
<button onclick="handleSetItem()">client1-setItem</button>
<!-- iframe 嵌套“中转页面” hub.html -->
<iframe src="http://localhost:6003/hub.html" frameborder="0" id="hub"></iframe>

<script>
const $ = id => document.querySelector(id)
// 获取 iframe window 对象
const ifameWin = $('#hub').contentWindow

let count = 0
function handleSetItem () {
let request = {
// 存储的方法
method: 'setItem',
// 存储的 key
key: 'someKey',
// 需要存储的数据值
value: `来自 client-1 消息:${count++}`,
}
// 向 iframe “中转页面”发送消息
ifameWin.postMessage(request, '*')
}
</script>
</body>

hub.html 中转页面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<body>
<script>
// 映射关系
let map = {
setItem: (key, value) => window.localStorage['setItem'](key, value),
getItem: (key) => window.localStorage['getItem'](key),
}

// “中转页面”监听 ifameWin.postMessage() 事件
window.addEventListener('message', function (e) {
let { method, key, value } = e.data
// 处理对应的存储方法
let result = map[method](key, value)
// 返回给当前 client 的数据
let response = {
result,
}
// 把获取的数据,传递给 client 窗口
window.parent.postMessage(response, '*')
})
</script>
</body>

client2.html 页面代码

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
<body>
<!-- 获取本地存储数据 -->
<button onclick="handleGetItem()">client2-getItem</button>
<!-- iframe 嵌套“中转页面” hub.html -->
<iframe src="http://localhost:6003/hub.html" frameborder="0" id="hub"></iframe>

<script>
const $ = id => document.querySelector(id)
// 获取 iframe window 对象
const ifameWin = $('#hub').contentWindow

function handleGetItem () {
let request = {
// 存储的方法(获取)
method: 'getItem',
// 获取的 key
key: 'someKey',
}
// 向 iframe “中转页面”发送消息
ifameWin.postMessage(request, '*')
}

// 监听 iframe “中转页面”返回的消息
window.addEventListener('message', function (e) {
console.log('client 2 获取到数据啦:', e.data)
})
</script>
</body>

浏览器打开如下地址:

具体效果如下:

跨域存储 Demo 演示

改进版本

分成 2 个 js 文件,一个是客户端页面使用 client.js,另一个是中转页面使用 hub.js

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
// client.js

class Client {
constructor (hubUrl) {
this.hubUrl = hubUrl
// 所有请求的 id 值(累加)
this.id = 0
// 所有请求消息映射
this._requests = {}
// 获取 iframe window 对象
this._iframeWin = this._createIframe(this.hubUrl).contentWindow
this._initListener()
}
//
getItem (key, callback) {
this._requestFn('getItem', {
key,
callback,
})
}
setItem (key, value, callback) {
this._requestFn('setItem', {
key,
value,
callback,
})
}
_requestFn (method, { key, value, callback }) {
// 发消息时,请求对象格式
let req = {
id: this.id++,
method,
key,
value,
}
// 请求 id 和回调函数的映射
this._requests[req.id] = callback
// 向 iframe “中转页面”发送消息
this._iframeWin.postMessage(req, '*')
}
// 初始化监听函数
_initListener () {
// 监听 iframe “中转页面”返回的消息
window.addEventListener('message', (e) => {
let { id, result } = e.data
// 找到“中转页面”的消息对应的回调函数
let currentCallback = this._requests[id]
if (!currentCallback) return
// 调用并返回数据
currentCallback(result)
})
}
// 创建 iframe 标签
_createIframe (hubUrl) {
const iframe = document.createElement('iframe')
iframe.src = hubUrl
iframe.style = 'display: none;'
window.document.body.appendChild(iframe)
return iframe
}
}
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
// hub.js

class Hub {
constructor () {
this._initListener()
this.map = {
setItem: (key, value) => window.localStorage['setItem'](key, value),
getItem: (key) => window.localStorage['getItem'](key),
}
}
// 监听 client ifameWin.postMessage() 事件
_initListener () {
window.addEventListener('message', (e) => {
let { method, key, value, id } = e.data
// 处理对应的存储方法
let result = this.map[method](key, value)
// 返回给当前 client 的数据
let response = {
id,
result,
}
// 把获取的数据,发送给 client 窗口
window.parent.postMessage(response, '*')
})
}
}

页面使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- client1 页面代码 -->

<body>
<button onclick="handleGetItem()">client1-GetItem</button>
<button onclick="handleSetItem()">client1-SetItem</button>

<script src="./lib/client.js"></script>
<script>
const crossStorage = new Client('http://localhost:6003/hub.html')
// 在 client1 中,获取 client2 存储的数据
function handleGetItem () {
crossStorage.getItem('client2Key', (result) => {
console.log('client-1 getItem result: ', result)
})
}

// client1 本地存储
function handleSetItem () {
crossStorage.setItem('client1Key', 'client-1 value', (result) => {
console.log('client-1 完成本地存储')
})
}
</script>
</body>
1
2
3
4
5
6
7
8
<!-- hub 页面代码 -->

<body>
<script src="./lib/hub.js"></script>
<script>
const hub = new Hub()
</script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- client2 页面代码 -->

<body>
<button onclick="handleGetItem()">client2-GetItem</button>
<button onclick="handleSetItem()">client2-SetItem</button>

<script src="./lib/client.js"></script>
<script>
const crossStorage = new Client('http://localhost:6003/hub.html')
// 在 client2 中,获取 client1 存储的数据
function handleGetItem () {
crossStorage.getItem('client1Key', (result) => {
console.log('client-2 getItem result: ', result)
})
}
// client2 本地存储
function handleSetItem () {
crossStorage.setItem('client2Key', 'client-2 value', (result) => {
console.log('client-2 完成本地存储')
})
}
</script>
</body>

总结

以上就实现了跨域存储,也是 cross-storage 开源库的原理。
通过 window.postMessage() api 跨域特性,再配合一个 “中转页面”,来完成所谓的“跨域存储”,实际上并没有真正的在浏览器端实现跨域存储,
这是浏览器的限制,我们无法打破,只能用“曲线救国”的方式,变向来共享存储数据。

所有源码在这里:跨域存储源码

欢迎关注无广告文章公众号:学前端

参考

-------------我是有底线哒-------------