您现在的位置是:网站首页> 编程资料编程资料
如何使用localstorage代替cookie实现跨域共享数据问题html5的localstorage详解HTML5 LocalStorage 本地存储刷新值还在HTML5 localStorage使用总结HTML5本地存储localStorage、sessionStorage基本用法、遍历操作、异常HTMl5的存储方式sessionStorage和localStorage详解
2023-10-16
270人已围观
简介 这篇文章主要介绍了如何使用localstorage代替cookie实现跨域共享数据问题,本文给大家带来了实现方案,使用postmessage和localstorage进行数据跨域共享问题,感兴趣的朋友一起看看吧
一,背景

因为网站系统的日益庞大,不同域名业务,甚至不同合作方网站的cookie可能或多或少需要进行共享使用,遇到这个情况的时候,大家一般想到的是使用登录中心分发cookie状态再进行同步进行解决,成本较高而且实施起来比较复杂和麻烦。
因为cookie在跨域的情况下,浏览器根本不允许互相访问的限制,为了突破这个限制,所以有了以下这个实现方案,使用postmessage和localstorage进行数据跨域共享。
原理比较简单,但是遇到的坑也不少,这里梳理一下,做个备份。
二,API设计
背景中说过我们使用localstorage来代替cookie,本身localstorage和cookie就有一些使用上的区别,比如localstorage的容量更大,但是不存在过期时间,虽然容量大,但在不同的浏览器上也都有空间上限,操作不好很容易崩溃,还有就是postmessage虽然支持跨域,安全问题和api的异步化也给使用带来了一些麻烦,我们如何把这个模块设计的更易用呢?
先看下我设计的API:
import { crosData } from 'base-tools-crossDomainData'; var store = new crosData({ iframeUrl:"somefile.html", //共享iframe地址,iframe有特殊要求,详见模板文件 expire:'d,h,s' //单位天,小时,秒 默认过期时间,也可以种的时候覆盖 }); store.set('key','val',{ expire:'d,h,s' //option 可带过期时间,覆盖expire }).then((data)=>{ //异步方法,如果种失败,会进入catch事件 //data {val:'val',key:'key',domain:'domain'}; }).catch((err)=>{ console.log(err); }); store.get('key',{ domain:'(.*).sina.cn' //可以指定域名,也可以使用(.*)来匹配正则字符串,返回的val信息会带着domain信息,不填写则返回本域的 }).then((vals)=>{ console.log(val) //异步获取存储数据,可能多个,是个数组 [{},{}] }).catch((err)=>{ }); store.clear('key').then().catch(); //只清楚当前域下的key,不允许清除其他域下的key,只能读一个模块上手快不快主要看api,所以对于一个数据共享模块,我认为支持set,get,clear这3个方法就ok了,因为postmessage本身是个一来一回的异步的行为,包装成promise的肯定更为合适和易用。因为localstorage不支持过期时间,所以需要一个全局的过期时间配置,当然也可以在set的时候进行单独配置,而get的时候我们可以指定获取某个域下的数据或者多个域下的数据,因为key名可能重复,但是域只有一个。这里就牵扯到了数据的管理,后边单独来说,最后clear和set的api只能种本域的数据,不可以操作其他域下的数据,get被允许。
下面我们看一下,client端的设置和API:
crosData
你可以灵活在任何一个域下的一个html文档中,引入client的js sdk,然后通过全局属性的方式配置一个你允许被种到这个文档所在域下的domain白名单,支持正则,然后lz是是否启动lz-string压缩,至于什么是lz压缩后边我再介绍。
到这里,一个比较通用的API设计就完成了,下面我们看一下实现原理和具体的一些问题。
三,实现原理
说起来好想蛮简单的,但是写起来其实并不是,我们首先需要知道postMessage怎么用,这个属于很常见的一个API了,他有一个要点这里告诉大家,就是postMessage只能在iframe中或者使用window.open这种开启新页面的方式进行互相通讯,当然这里我们首先就要创建一个隐藏的iframe,进行跨域。
懒得拿工具画图了,因为流程比较清晰,这里拿文字复述一下整个通讯流程,首先父页面创建一个隐藏的iframe,然后当执行set,get,clear等command的时候,通过postMessage来进行消息广播,子页面接收到消息后,解析命令,数据和回调id(postMessage无法传递函数和引用,兼容问题导致,最好只传string类型,所以还需要对data做stringify)。然后当子页面处理完localstorage的操作后,再通过postMessage把对应的cbid和data返回给父页面,父页面监听message事件,处理结果。
四,编码
嗯,所以说没几行,我们下面开始进行编码了:
首先介绍一下我们用到的第三方包都有什么,为什么要用:
1,url-parse 对url进行parse解析,主要用他里面的origin属性,因为postMessage本身对origin就有严格的验证,我们要支持白名单和域名管理也需要。
2,ms 对时间简写做毫秒转换的工具库。
3, lz-string 对字符串做压缩用的工具包,这里给大家科普一下LZ压缩算法,首先了解LZ需要先了解RLZ,Run Length Encoding ,是一个针对无损压缩的非常简单的算法。它用重复字节和重复的次数来简单描述来代替重复的字节。LZ 压缩算法的背后是使用 RLE 算法用先前出现的相同字节序列的引用来替代。简单的讲, LZ 算法被认为是字符串匹配的算法。例如:在一段文本中某字符串经常出现,并且可以通过前面文本中出现的字符串指针来表示。
lz-string本身有优势就是可以大大的减小你的储存量,本身5MB的localstorage如果用来支持多域名的数据保存,很快就会被压缩用完,但是lz-string本身比较慢,消耗比较大,大家平时在工作中如果对传输数据量有大小要求的话可以尝试使用这个压缩算法来优化字符串长度,默认是不开启的。
4,store2 本身localstorage的api比较简陋,为了减少代码逻辑复杂度,这里选了一个比较流行的localstorage的实现库来进行store的操作。
说完了第三方包我们下面看一下父页面的js怎么来写:
class crosData { constructor(options) { supportCheck(); this.options = Object.assign({ iframeUrl: '', expire: '30d' }, options); this.cid = 0; this.cbs = {}; this.iframeBeforeFuns = []; this.parent = window; this.origin = new url(this.options.iframeUrl).origin; this.createIframe(this.options.iframeUrl); addEvent(this.parent, 'message', (evt) => { var data = JSON.parse(evt.data); var origin = evt.origin || evt.originalEvent.origin; //我只接收我打开的这个iframe的message,其他的都是不合法的,直接报错 if (origin !== this.origin) { reject('illegal origin!'); return; } if (data.err) { this.cbs[data.cbid].reject(data.err); } else { this.cbs[data.cbid].resolve(data.ret); } delete this.cbs[data.cbid]; }); } createIframe(url) { addEvent(document, 'domready', () => { var frame = document.createElement('iframe'); frame.style.cssText = 'width:1px;height:1px;border:0;position:absolute;left:-9999px;top:-9999px;'; frame.setAttribute('src', url); frame.onload = () => { this.child = frame.contentWindow; this.iframeBeforeFuns.forEach(item => item()); } document.body.appendChild(frame); }); } postHandle(type, args) { return new Promise((resolve, reject) => { var cbid = this.cid; var message = { cbid: cbid, origin: new url(location.href).origin, action: type, args: args } this.child.postMessage(JSON.stringify(message), this.origin); this.cbs[cbid] = { resolve, reject } this.cid++; }); } send(type, args) { return new Promise(resolve => { if (this.child) { return this.postHandle(type, args).then(resolve); } else { var self = this; this.iframeBeforeFuns.push(function() { self.postHandle(type, args).then(resolve); }); } }) } set(key, val, options) { options = Object.assign({ expire: ms(this.options.expire) }, options); return this.send('set', [key, val, options]); } get(key, options) { options = Object.assign({ domain: new url(location.href).origin }, options); return this.send('get', [key, options]); } clear(key) { return this.send('clear', [key]); } }大概方法就这么几个,这里有几个关键点,我说一下。
1,get,set,clear方法都是统一的调用的send方法,只不过对options部分做了补齐。
2,send方法返回一个promise对象,如果iframe已经onload成功,则直接调用postHandle方法进行postMessage操作,如果iframe还在加载中,则把当前的操作推到iframeBeforeFuns数组中,用函数包裹,等待iframe onload结束后统一调用,函数包裹的也是postHandle方法。
3,postHandle方法,在发送请求前包装data,生成cbid,origin,action和args,cbs对象保存了每个cbid下的resolve和reject,等待子页面的postMessage返回后处理。因为postMessage不能保留引用,不能传函数,所以这里选择这个方法来进行关联。
4,constructor比较好理解,当这个类被初始化的时候,我们定义了我们需要的一些options的属性,创建iframe,然后监听message事件,处理子页面返回的消息。
5,在父页面的message事件中,我们要校验,给我发消息的必须是我打开的这个窗口iframe,否则报错,然后根据data中的err标识来让cbs中的resolve和reject进行执行。
6,createIframe方法中,iframe onload中的回调处理创建前 缓存的调用方法,这里注意使用了domready,因为可能body还没解析就会进行sdk的执行。
下面是child部分的代码:
class iframe { set(key, val, options, origin) { //检查val大小,不能超过20k. val = val.toString(); val = this.lz ? lzstring.compressToUTF16(val) : val; var valsize = sizeof(val, 'utf16'); //localStorage 储存使用utf16编码计算字节 if (valsize > this.maxsize) { return { err: 'your store value : "' + valstr + '" size is ' + valsize + 'b, maxsize :' + this.maxsize + 'b , use utf16' } } key = `${this.prefix}_${key},${new url(origin).origin}`; var data = { val: val, lasttime: Date.now(), expire: Date.now() + options.expire }; store.set(key, data); //大于最大储存个数,删除最后一次更新的 if (store.size() > this.storemax) { var keys = store.keys(); keys = keys.sort((a, b) => { var item1 = store.get(a), item2 = store.get(b); return item2.lasttime - item1.lasttime; }); var removesize = Math.abs(this.storemax - store.size()); while (removesize) { store.remove(keys.pop()); removesize--; } } return { ret: data } } get(key, options) { var message = {}; var keys = store.keys(); var regexp = new RegExp('^' + this.prefix + '_' + key + ',' + options.domain + '$'); message.ret = keys.filter((key) => { return regexp.test(key); }).map((storeKey) => { var data = store.get(storeKey); data.key = key; data.domain = storeKey.split(',')[1]; if (data.expire < Date.now()) { store.remove(storeKey); return undefined; } else { //更新lasttime; store.set(storeKey, { val: data.val, lasttime: Date.now(), expire: data.expire }); } data.val = this.lz ? lzstring.decompressFromUTF16(data.val) : data.val; return data; }).filter(item => { return !!item; //过滤undefined }); return message; } clear(key, origin) { store.remove(`${this.prefix}_${key},${origin}`); return {}; } clearOtherKey() { //删除不合法的key var keys = store.keys(); var keyReg = new RegExp('^' + this.prefix); keys.forEach(key => { if (!keyReg.test(key)) { store.remove(key); } }); } constructor(safeDomain, lz) { supportCheck(); this.safeDomain = safeDomain || /.*/; this.prefix = '_cros'; this.clearOtherKey(); if (Object.prototype.toString.call(this.safeDomain) !== '[object RegExp]') { throw new Error('safeDomain must be regexp'); } this.lz = lz; this.storemax = 100; this.maxsize = 20 * 1024; //字节 addEvent(window, 'message', (evt) => { var data = JSON.parse(evt.data); var originHostName = new url(evt.origin).hostname; var origin = evt.origin, action = data.action, cbid = data.cbid, args = data.args; //合法的广播 if (evt.origin === data.origin && this.safeDomain.test(originHostName)) { args.push(origin); var whiteAction = ['set', 'get', 'clear']; if (whiteAction.indexOf(action) > -1) { var message = this[action].apply(this, args); message.cbid = cbid; window.top.postMessage(JSON.stringify(message), origin); } } else { window.top.postMessage(JSON.stringify({ cbid: cbid, err: 'Illegal domain' }), origin); } }); } }代码也不多,这里简单说一下各个方法的用处和组织关系:
1,constructor部分,上面的类里也进行浏览器特性支持检查,然后定义了store的prefix值,最大个数和每一个key的maxsize等属性。然后我们创建message通道,等待父页面调用。
2,在message中,我们对发送广播的origin进行检查,然后对调用的方法进行检查,调用对应的set,get,clear方法,然后把执行的结果拿到,绑定cbid,最后再postMessage发送回父页面。
3,clearOtherKey 删除不合法的一些store数据,只保留符合格式的数据。
4,set方法中对每一条的数据做size校验,lz压缩,保存的data中包含了val,key,过期时间以及更新时间(用于LRU计算)。
5,set方法中,如果储存的ls个数超过了最大限制,这个时候需要进行删除操作, LRU是Least Recently Used的缩写,即最近最少使用。我们通过遍历所有的key值,对key值做一个排序,通过lasttime,然后进行keys数组的pop操作,拿到堆栈尾部的需要被清除的key,然后逐个删除。
6,get方法中,我们通过遍历所有的key值,匹配到我们需要拿到的domain的域的key,然后把返回值中的key进行拆解(我们储存时是 key,domain的格式),因为api要求返回多个符合的值,我们对过期的数据最后再做一个filter,然后使用lz解压缩val值,保证用户拿到的是正确结果。
以上就是我们的一个整体实现编码过程和review,下面说一说遇到的坑。
五,一些遇到的坑提示:
本文由神整理自网络,如有侵权请联系本站删除!
本站声明:
1、本站所有资源均来源于互联网,不保证100%完整、不提供任何技术支持;
2、本站所发布的文章以及附件仅限用于学习和研究目的;不得将用于商业或者非法用途;否则由此产生的法律后果,本站概不负责!
相关内容
- 教你如何一步一步用Canvas写一个贪吃蛇H5 canvas实现贪吃蛇小游戏
- html5/css3响应式页面开发总结 CSS3移动端vw+rem不依赖JS实现响应式布局的方法CSS banner图响应式居中显示的方法详解使用CSS3的@media来编写响应式的页面 jQuery和CSS3响应式轮播插件jcSlider纯CSS3大转盘抽奖示例代码(响应式、可配置)CSS3 media queries + jQuery实现响应式导航CSS 响应式布局系统的实例代码
- 传统HTML页面实现模块化加载的方法在HTML里加载摄像头的方法基于HTML代码实现图片碎片化加载功能html5用video标签流式加载的实现HTML5 图片预加载的示例代码HTML页面缩小后显示滚动条的示例代码h5页面背景图很长要有滚动条滑动效果的实现HTML5实现直播间评论滚动效果的代码html+css实现滚动到元素位置显示加载动画效果
- HTML5 和小程序实现拍照图片旋转、压缩和上传功能HTML5页面嵌入小程序没有返回按钮及返回页面空白的问题微信小程序之html5 canvas绘图并保存到系统相册基于Jscex +HTML5 Canvas 制作的抽奖小程序HTML5跳转小程序wx-open-launch-weapp的示例代码
- 详解canvas drawImage()方法绘制图片不显示的问题HTML5 Canvas API中drawImage()方法的使用实例canvas绘制图片drawImage使用方法
- 全民英雄 仙女龙怎么样 仙女龙技能详解_手机游戏_游戏攻略_
- 我叫MT 黑沼泽副本艾尔努斯打法攻略推荐阵容_手机游戏_游戏攻略_
- 全民英雄 怎么提高酒馆上限的方法_手机游戏_游戏攻略_
- 我叫MT 3.1精英能源计算者如何刷 阵容推荐_手机游戏_游戏攻略_
- 爸爸去哪儿游戏时装大全以及这些时装作用_手机游戏_游戏攻略_
