js存储跨域,localstorage跨域存储解决方案

跨域我知道,存储我知道,那跨域存储你了解多少呢?

什么是跨域?

先看一下 URL 有哪些部分组成,如下:
在这里插入图片描述
protocol(协议)、host(域名)、port(端口)有一个地方不同都会产生跨域现象,也被称为客户端同源策略;

本地存储受同源策略限制

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

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

otherWindow.postMessage(‘message’, targetOrigin, [transfer])

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

语法:

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

实现思路

需求:

有两个不同的域名(http://localhost:6001 和 http://localhost:6002)想共用本地存储中的同一个 token 作为统一登录凭证:

假设:

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

启动服务:

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

npm -g install http-server

# 启动 3 个不同端口的服务,模拟跨域现象
http-server -p 6001
http-server -p 6002
http-server -p 6003
简单实现版本
<!-- client1.html 页面代码 -->

<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>
<!-- middle.html 中转页面代码 -->

<body>
  <script>
    // 映射关系
    let map = {
      setItem: (key, value) => window.localStorage['setItem'](key, value "'setItem'"),
      getItem: (key) => window.localStorage['getItem'](key "'getItem'"),
    }

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

<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>

浏览器打开如下地址:

http://localhost:6001/client1.html
http://localhost:6002/client2.html

改进版本

共拆分成 2 个 js 文件,一个是客户端页面使用 client.js,另一个是中转页面使用 corss-middle.js,具体代码如下:

//client.js

class Client {
	constructor(middlewareUrl) {
		this.middlewareUrl = middlewareUrl;
		this.id = null;
		this._requests = {}; //所有请求消息数据映射
		//获取 iframe window 对象
		this._iframeWin = this._createIframe(this.middlewareUrl).contentWindow
		this._initListener(); //监听
	}

	/**
	 * 获取存储数据
	 * @param {Object} key
	 * @param {Object} callback
	 */
	getItem(key, callback) {
		this._requestFn('get', {
			key,
			callback,
		})
	}

	/**
	 * 更新存储数据
	 * @param {Object} key
	 * @param {Object} value
	 * @param {Object} callback
	 */
	setItem(key, value, callback) {
		this._requestFn('set', {
			key,
			value,
			callback,
		})
	}

	/**
	 * 删除数据
	 * @param {Object} key
	 * @param {Object} callback
	 */
	delItem(key, callback) {
		this._requestFn('delete', {
			key,
			value: null,
			callback,
		})
	}

	/**
	 * 发起请求函数
	 * @param method 请求方式  
	 */
	_requestFn(method, {
		key,
		value,
		callback
	}) {
		// 发消息时,请求对象格式
		let req = {
			id: this.uuid(),
			method,
			key,
			value,
		}

		//请求唯一标识 id 和回调函数的映射
		this._requests[req.id] = callback;
		this._iframeWin.postMessage(req, '*')
	}

	/**
	 * 生成随机ID
	 */
	uuid() {
		return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
			var r = Math.random() * 16 | 0,
				v = c == 'x' ? r : (r & 0x3 | 0x8);
			return v.toString(16);
		});
	}

	/**
	 * 初始化监听函数
	 */
	_initListener() {
		// 监听 iframe “中转页面”返回的消息
		window.addEventListener('message', (e) => {
			let {
				id,
				request,
				reponse
			} = e.data;


			// 找到“中转页面”的消息对应的回调函数
			let currentCallback = this._requests[id]
			if (!currentCallback) return
			// 调用并返回数据
			currentCallback(reponse, e.data);
		})
	}

	/**
	 * 创建 iframe 标签
	 * @param {Object} middlewareUrl
	 * @return Object
	 */
	_createIframe(middlewareUrl) {
		const iframe = document.createElement('iframe')
		iframe.src = middlewareUrl
		iframe.style = 'display: none;'
		window.document.body.appendChild(iframe)
		return iframe
	}
}

//corss-middle.js
class Middle {
	constructor() {
		this.iframeWin = window.parent;
		this.map = {
			/**
			 * 设置数据
			 * @param {Object} key
			 * @param {Object} value
			 */
			setStore(key, value) {
				if (!key) return;
				if (!key instanceof Object) return localStorage.setItem(key, value);
				Object.keys(key).forEach(dataKey => {
					let dataValue = typeof key[dataKey] === 'object' ? JSON.stringify(key[dataKey]) : key[dataKey];
					localStorage.setItem(dataKey, dataValue);
				});
			},

			/**
			 * 获取数据
			 * @param {Object} key
			 */
			getStore(key) {
				if (typeof key === 'string') return localStorage.getItem(key);
				let dataRes = {};
				key.forEach(dataKey => {
					dataRes[dataKey] = localStorage.getItem(dataKey) || null;
				});
				return dataRes;
			},

			/**
			 * 删除数据
			 * @param {Object} key
			 */
			deleteStore(key) {
				let removeKeys = [...key];
				removeKeys.forEach(dataKey => {
					localStorage.removeItem(dataKey);
				});
			},

			/**
			 * 清空
			 */
			clearStore() {
				localStorage.clear();
			}
		};

		this._initListener(); //监听消息
	}

	/**
	 * 监听
	 */
	_initListener() {
		window.addEventListener('message', (e) => {
			let {
				method,
				key,
				value,
				id = "default",
				...res
			} = e.data;

			//取出本地的数据
			let mapFun = this.map[`${method}Store`];

			if (!mapFun) {
				return this.iframeWin.postMessage({
					id,
					request: e.data,
					reponse: 'Request mode error!'
				}, '*');
			}

			//取出本地的数据
			let storeData = mapFun(key, value);

			//发送给父亲
			this.iframeWin.postMessage({
				id,
				request: e.data,
				reponse: storeData
			}, '*');
		})
	}
}

页面使用:

<!-- client1 页面代码 -->

<body>
	<button id="get-data">获取数据哦</button>
	<button id="set-data">设置</button>
	<button id="del-data">删除数据</button>
	<button id="test-connet">测试连接</button>

	<script src="js/client.js"></script>
	<script type="text/javascript">
		const crossStorage = new Client('http://localhost:8080/demo/corss-middle.html')

		//在 client1 中,获取 client2 存储的数据
		document.getElementById('get-data').addEventListener('click', () => {
			crossStorage.getItem(['home'], (result, data) => {
				console.log('client-1 getItem result: ', result)
			})
		}, false);

		//设置数据
		document.getElementById('set-data').addEventListener('click', () => {
			crossStorage.setItem({
				job: 'web前端',
				money: 100
			}, null, (result) => {
				console.log('完成本地存储')
			})
		}, false);
		
		//删除数据
		document.getElementById('del-data').addEventListener('click', () => {
			crossStorage.delItem(['job'], (result) => {})
		}, false);
	</script>
</body>
<!-- corss-middle 页面代码 -->

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
		<script src="js/corss-middle.js"></script>
	</head>
	<body>
		<h1>This is Middle page!</h1>
		<script>
			const corssMiddle = new Middle();
		</script>
	</body>
</html>
<!-- client2 页面代码 -->
	
<body>
	<button id="get-data">获取数据哦</button>
	<button id="set-data">设置</button>
	<button id="del-data">删除数据</button>
	<button id="test-connet">测试连接</button>

	<script src="js/client.js"></script>
	<script type="text/javascript">
		const crossStorage = new Client('http://localhost:8080/demo/corss-middle.html')

		//在 client2 中,获取 client1存储的数据
		document.getElementById('get-data').addEventListener('click', () => {
			crossStorage.getItem(['job', 'money'], (result, data) => {
				console.log('client-1 getItem result: ', result)
			})
		}, false);

		//设置数据
		document.getElementById('set-data').addEventListener('click', () => {
			crossStorage.setItem({
				home: '深圳'
			}, null, (result) => {})
		}, false);
		
		//删除数据
		document.getElementById('del-data').addEventListener('click', () => {
			crossStorage.delItem(['job', 'money', 'home'], (result) => {})
		}, false);
	</script>
</body>
总结

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

小提示: 日常开发中只需要把 corss-middle.html ,corss-middle.js 放在一个端口中,作为跨域存储中间件!

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值