這篇文章給大家介紹怎么在使用localstorage代替cookie實(shí)現(xiàn)跨域共享數(shù)據(jù),內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。
網(wǎng)站設(shè)計(jì)制作過(guò)程拒絕使用模板建站;使用PHP+MYSQL原生開(kāi)發(fā)可交付網(wǎng)站源代碼;符合網(wǎng)站優(yōu)化排名的后臺(tái)管理系統(tǒng);做網(wǎng)站、成都網(wǎng)站制作收費(fèi)合理;免費(fèi)進(jìn)行網(wǎng)站備案等企業(yè)網(wǎng)站建設(shè)一條龍服務(wù).我們是一家持續(xù)穩(wěn)定運(yùn)營(yíng)了十年的創(chuàng)新互聯(lián)網(wǎng)站建設(shè)公司。
一,背景
因?yàn)榫W(wǎng)站系統(tǒng)的日益龐大,不同域名業(yè)務(wù),甚至不同合作方網(wǎng)站的cookie可能或多或少需要進(jìn)行共享使用,遇到這個(gè)情況的時(shí)候,大家一般想到的是使用登錄中心分發(fā)cookie狀態(tài)再進(jìn)行同步進(jìn)行解決,成本較高而且實(shí)施起來(lái)比較復(fù)雜和麻煩。
因?yàn)閏ookie在跨域的情況下,瀏覽器根本不允許互相訪問(wèn)的限制,為了突破這個(gè)限制,所以有了以下這個(gè)實(shí)現(xiàn)方案,使用postmessage和localstorage進(jìn)行數(shù)據(jù)跨域共享。
二,API設(shè)計(jì)
背景中說(shuō)過(guò)我們使用localstorage來(lái)代替cookie,本身localstorage和cookie就有一些使用上的區(qū)別,比如localstorage的容量更大,但是不存在過(guò)期時(shí)間,雖然容量大,但在不同的瀏覽器上也都有空間上限,操作不好很容易崩潰,還有就是postmessage雖然支持跨域,安全問(wèn)題和api的異步化也給使用帶來(lái)了一些麻煩,我們?nèi)绾伟堰@個(gè)模塊設(shè)計(jì)的更易用呢?
先看下我設(shè)計(jì)的API:
import { crosData } from 'base-tools-crossDomainData'; var store = new crosData({ iframeUrl:"somefile.html", //共享iframe地址,iframe有特殊要求,詳見(jiàn)模板文件 expire:'d,h,s' //單位天,小時(shí),秒 默認(rèn)過(guò)期時(shí)間,也可以種的時(shí)候覆蓋 }); store.set('key','val',{ expire:'d,h,s' //option 可帶過(guò)期時(shí)間,覆蓋expire }).then((data)=>{ //異步方法,如果種失敗,會(huì)進(jìn)入catch事件 //data {val:'val',key:'key',domain:'domain'}; }).catch((err)=>{ console.log(err); }); store.get('key',{ domain:'(.*).sina.cn' //可以指定域名,也可以使用(.*)來(lái)匹配正則字符串,返回的val信息會(huì)帶著domain信息,不填寫(xiě)則返回本域的 }).then((vals)=>{ console.log(val) //異步獲取存儲(chǔ)數(shù)據(jù),可能多個(gè),是個(gè)數(shù)組 [{},{}] }).catch((err)=>{ }); store.clear('key').then().catch(); //只清楚當(dāng)前域下的key,不允許清除其他域下的key,只能讀
一個(gè)模塊上手快不快主要看api,所以對(duì)于一個(gè)數(shù)據(jù)共享模塊,我認(rèn)為支持set,get,clear這3個(gè)方法就ok了,因?yàn)閜ostmessage本身是個(gè)一來(lái)一回的異步的行為,包裝成promise的肯定更為合適和易用。因?yàn)閘ocalstorage不支持過(guò)期時(shí)間,所以需要一個(gè)全局的過(guò)期時(shí)間配置,當(dāng)然也可以在set的時(shí)候進(jìn)行單獨(dú)配置,而get的時(shí)候我們可以指定獲取某個(gè)域下的數(shù)據(jù)或者多個(gè)域下的數(shù)據(jù),因?yàn)閗ey名可能重復(fù),但是域只有一個(gè)。這里就牽扯到了數(shù)據(jù)的管理,后邊單獨(dú)來(lái)說(shuō),最后clear和set的api只能種本域的數(shù)據(jù),不可以操作其他域下的數(shù)據(jù),get被允許。
下面我們看一下,client端的設(shè)置和API:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>crosData</title> </head> <body> <script> window.CROS = { domain:/(.*).sina.cn/, //或者你允許的域名,支持正則和*通配符 lz:false //是否開(kāi)啟lz壓縮val字符 }; </script> <script src="http://cdn/sdk.js"></script> </body> </html>
你可以靈活在任何一個(gè)域下的一個(gè)html文檔中,引入client的js sdk,然后通過(guò)全局屬性的方式配置一個(gè)你允許被種到這個(gè)文檔所在域下的domain白名單,支持正則,然后lz是是否啟動(dòng)lz-string壓縮,至于什么是lz壓縮后邊我再介紹。
到這里,一個(gè)比較通用的API設(shè)計(jì)就完成了,下面我們看一下實(shí)現(xiàn)原理和具體的一些問(wèn)題。
三,實(shí)現(xiàn)原理
說(shuō)起來(lái)好想蠻簡(jiǎn)單的,但是寫(xiě)起來(lái)其實(shí)并不是,我們首先需要知道postMessage怎么用,這個(gè)屬于很常見(jiàn)的一個(gè)API了,他有一個(gè)要點(diǎn)這里告訴大家,就是postMessage只能在iframe中或者使用window.open這種開(kāi)啟新頁(yè)面的方式進(jìn)行互相通訊,當(dāng)然這里我們首先就要?jiǎng)?chuàng)建一個(gè)隱藏的iframe,進(jìn)行跨域。
懶得拿工具畫(huà)圖了,因?yàn)榱鞒瘫容^清晰,這里拿文字復(fù)述一下整個(gè)通訊流程,首先父頁(yè)面創(chuàng)建一個(gè)隱藏的iframe,然后當(dāng)執(zhí)行set,get,clear等command的時(shí)候,通過(guò)postMessage來(lái)進(jìn)行消息廣播,子頁(yè)面接收到消息后,解析命令,數(shù)據(jù)和回調(diào)id(postMessage無(wú)法傳遞函數(shù)和引用,兼容問(wèn)題導(dǎo)致,最好只傳string類(lèi)型,所以還需要對(duì)data做stringify)。然后當(dāng)子頁(yè)面處理完localstorage的操作后,再通過(guò)postMessage把對(duì)應(yīng)的cbid和data返回給父頁(yè)面,父頁(yè)面監(jiān)聽(tīng)message事件,處理結(jié)果。
四,編碼
嗯,所以說(shuō)沒(méi)幾行,我們下面開(kāi)始進(jìn)行編碼了:
首先介紹一下我們用到的第三方包都有什么,為什么要用:
1,url-parse 對(duì)url進(jìn)行parse解析,主要用他里面的origin屬性,因?yàn)閜ostMessage本身對(duì)origin就有嚴(yán)格的驗(yàn)證,我們要支持白名單和域名管理也需要。
2,ms 對(duì)時(shí)間簡(jiǎn)寫(xiě)做毫秒轉(zhuǎn)換的工具庫(kù)。
3, lz-string 對(duì)字符串做壓縮用的工具包,這里給大家科普一下LZ壓縮算法,首先了解LZ需要先了解RLZ,Run Length Encoding ,是一個(gè)針對(duì)無(wú)損壓縮的非常簡(jiǎn)單的算法。它用重復(fù)字節(jié)和重復(fù)的次數(shù)來(lái)簡(jiǎn)單描述來(lái)代替重復(fù)的字節(jié)。LZ 壓縮算法的背后是使用 RLE 算法用先前出現(xiàn)的相同字節(jié)序列的引用來(lái)替代。簡(jiǎn)單的講, LZ 算法被認(rèn)為是字符串匹配的算法。例如:在一段文本中某字符串經(jīng)常出現(xiàn),并且可以通過(guò)前面文本中出現(xiàn)的字符串指針來(lái)表示。
lz-string本身有優(yōu)勢(shì)就是可以大大的減小你的儲(chǔ)存量,本身5MB的localstorage如果用來(lái)支持多域名的數(shù)據(jù)保存,很快就會(huì)被壓縮用完,但是lz-string本身比較慢,消耗比較大,大家平時(shí)在工作中如果對(duì)傳輸數(shù)據(jù)量有大小要求的話可以嘗試使用這個(gè)壓縮算法來(lái)優(yōu)化字符串長(zhǎng)度,默認(rèn)是不開(kāi)啟的。
4,store2 本身localstorage的api比較簡(jiǎn)陋,為了減少代碼邏輯復(fù)雜度,這里選了一個(gè)比較流行的localstorage的實(shí)現(xiàn)庫(kù)來(lái)進(jìn)行store的操作。
說(shuō)完了第三方包我們下面看一下父頁(yè)面的js怎么來(lái)寫(xiě):
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; //我只接收我打開(kāi)的這個(gè)iframe的message,其他的都是不合法的,直接報(bào)錯(cuò) 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]); } }
大概方法就這么幾個(gè),這里有幾個(gè)關(guān)鍵點(diǎn),我說(shuō)一下。
1,get,set,clear方法都是統(tǒng)一的調(diào)用的send方法,只不過(guò)對(duì)options部分做了補(bǔ)齊。
2,send方法返回一個(gè)promise對(duì)象,如果iframe已經(jīng)onload成功,則直接調(diào)用postHandle方法進(jìn)行postMessage操作,如果iframe還在加載中,則把當(dāng)前的操作推到iframeBeforeFuns數(shù)組中,用函數(shù)包裹,等待iframe onload結(jié)束后統(tǒng)一調(diào)用,函數(shù)包裹的也是postHandle方法。
3,postHandle方法,在發(fā)送請(qǐng)求前包裝data,生成cbid,origin,action和args,cbs對(duì)象保存了每個(gè)cbid下的resolve和reject,等待子頁(yè)面的postMessage返回后處理。因?yàn)閜ostMessage不能保留引用,不能傳函數(shù),所以這里選擇這個(gè)方法來(lái)進(jìn)行關(guān)聯(lián)。
4,constructor比較好理解,當(dāng)這個(gè)類(lèi)被初始化的時(shí)候,我們定義了我們需要的一些options的屬性,創(chuàng)建iframe,然后監(jiān)聽(tīng)message事件,處理子頁(yè)面返回的消息。
5,在父頁(yè)面的message事件中,我們要校驗(yàn),給我發(fā)消息的必須是我打開(kāi)的這個(gè)窗口iframe,否則報(bào)錯(cuò),然后根據(jù)data中的err標(biāo)識(shí)來(lái)讓cbs中的resolve和reject進(jìn)行執(zhí)行。
6,createIframe方法中,iframe onload中的回調(diào)處理創(chuàng)建前 緩存的調(diào)用方法,這里注意使用了domready,因?yàn)榭赡躡ody還沒(méi)解析就會(huì)進(jìn)行sdk的執(zhí)行。
下面是child部分的代碼:
class iframe { set(key, val, options, origin) { //檢查val大小,不能超過(guò)20k. val = val.toString(); val = this.lz ? lzstring.compressToUTF16(val) : val; var valsize = sizeof(val, 'utf16'); //localStorage 儲(chǔ)存使用utf16編碼計(jì)算字節(jié) 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); //大于最大儲(chǔ)存?zhèn)€數(shù),刪除最后一次更新的 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; //過(guò)濾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; //字節(jié) 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); } }); } }
代碼也不多,這里簡(jiǎn)單說(shuō)一下各個(gè)方法的用處和組織關(guān)系:
1,constructor部分,上面的類(lèi)里也進(jìn)行瀏覽器特性支持檢查,然后定義了store的prefix值,最大個(gè)數(shù)和每一個(gè)key的maxsize等屬性。然后我們創(chuàng)建message通道,等待父頁(yè)面調(diào)用。
2,在message中,我們對(duì)發(fā)送廣播的origin進(jìn)行檢查,然后對(duì)調(diào)用的方法進(jìn)行檢查,調(diào)用對(duì)應(yīng)的set,get,clear方法,然后把執(zhí)行的結(jié)果拿到,綁定cbid,最后再postMessage發(fā)送回父頁(yè)面。
3,clearOtherKey 刪除不合法的一些store數(shù)據(jù),只保留符合格式的數(shù)據(jù)。
4,set方法中對(duì)每一條的數(shù)據(jù)做size校驗(yàn),lz壓縮,保存的data中包含了val,key,過(guò)期時(shí)間以及更新時(shí)間(用于LRU計(jì)算)。
5,set方法中,如果儲(chǔ)存的ls個(gè)數(shù)超過(guò)了最大限制,這個(gè)時(shí)候需要進(jìn)行刪除操作, LRU是Least Recently Used的縮寫(xiě),即最近最少使用。我們通過(guò)遍歷所有的key值,對(duì)key值做一個(gè)排序,通過(guò)lasttime,然后進(jìn)行keys數(shù)組的pop操作,拿到堆棧尾部的需要被清除的key,然后逐個(gè)刪除。
6,get方法中,我們通過(guò)遍歷所有的key值,匹配到我們需要拿到的domain的域的key,然后把返回值中的key進(jìn)行拆解(我們儲(chǔ)存時(shí)是 key,domain的格式),因?yàn)閍pi要求返回多個(gè)符合的值,我們對(duì)過(guò)期的數(shù)據(jù)最后再做一個(gè)filter,然后使用lz解壓縮val值,保證用戶(hù)拿到的是正確結(jié)果。
以上就是我們的一個(gè)整體實(shí)現(xiàn)編碼過(guò)程和review,下面說(shuō)一說(shuō)遇到的坑。
五,一些遇到的坑
因?yàn)樯厦嬷唤o了主代碼,并不是完整代碼,因?yàn)楸旧磉壿嫳容^清晰,花一點(diǎn)時(shí)間都可以寫(xiě)出來(lái)的。下面說(shuō)說(shuō)有什么坑的地方。
1,計(jì)算localstorage的儲(chǔ)存值。
因?yàn)槲覀兌贾烙?MB的限制,所以每一條數(shù)據(jù)最大要求不能超過(guò)20*1024 字節(jié),對(duì)于字節(jié)的計(jì)算,localstorage要使用utf16的編碼進(jìn)行轉(zhuǎn)換,參考這篇文章: JS計(jì)算字符串所占字節(jié)數(shù)
2,兼容性
ie8下postMessage最好都傳字符串,事件需要抹平處理,JSON需要抹平處理。
3,創(chuàng)建iframe時(shí)的異步處理
這里之前做了個(gè)一個(gè)setTimeout的遞歸等待,后來(lái)更改成了上面的實(shí)現(xiàn)方法,通過(guò)onload后統(tǒng)一處理promise的reslove,保證promise api的統(tǒng)一。
4,數(shù)據(jù)保存時(shí),空間復(fù)雜度 vs 時(shí)間復(fù)雜度。
第一個(gè)版本并不是上面的實(shí)現(xiàn),我實(shí)現(xiàn)了3個(gè)版本:
第一個(gè)版本是保存了一個(gè)LRU的數(shù)組,為了減少時(shí)間復(fù)雜度,但是浪費(fèi)了空間復(fù)雜度,而且經(jīng)過(guò)測(cè)試,store的get方法耗時(shí)比較大,主要是parse的耗時(shí)。
第二個(gè)版本,為了能讓lz-string壓縮率最大化,我把所有的數(shù)據(jù)包括LRU數(shù)組保存到了一個(gè)key值上,導(dǎo)致數(shù)據(jù)多的時(shí)候lz-string和getItem,parse時(shí)間消耗非常大,雖然計(jì)算的時(shí)間復(fù)雜度是最低。
最后一個(gè)版本,就是上面的,我犧牲了一些時(shí)間復(fù)雜度和空間復(fù)雜度,但是因?yàn)槠款i在于set和get的讀寫(xiě)速度,單個(gè)的保存讀寫(xiě)速度極快,獲取keys的方法因?yàn)榈讓邮怯玫膄or in localstorage實(shí)現(xiàn)的,性能還是很不錯(cuò)的,20kb存滿(mǎn)100條,讀寫(xiě)也在1s左右,性能非常不錯(cuò)。
六,總結(jié)和對(duì)比
模塊寫(xiě)完了,我才知道原來(lái)還有這么一個(gè)庫(kù): zendesk/cross-storage
但是我查看了他的api和源代碼,對(duì)比了一下實(shí)現(xiàn)方法,我覺(jué)得還是我這個(gè)版本考慮的比較多。
1,我的版本對(duì)域名和數(shù)據(jù)的管理有控制。
2,我的版本promise api更簡(jiǎn)化,比它少一個(gè)onConnect,可以參考他的實(shí)現(xiàn),比我寫(xiě)的多多了,也沒(méi)解決這個(gè)iframe等待異步的問(wèn)題。
3,不支持lz壓縮數(shù)據(jù)。
4,不支持LRU的儲(chǔ)存池管理,所以可能存多了造成寫(xiě)不進(jìn)的問(wèn)題。
5,他貌似每次交互都搞一個(gè)iframe,太浪費(fèi)dom操作和廣播了,我覺(jué)得一直開(kāi)著并沒(méi)有什么問(wèn)題,當(dāng)然他可能有需求連接多個(gè)client才這么處理的。
關(guān)于怎么在使用localstorage代替cookie實(shí)現(xiàn)跨域共享數(shù)據(jù)就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。
新聞名稱(chēng):怎么在使用localstorage代替cookie實(shí)現(xiàn)跨域共享數(shù)據(jù)
鏈接URL:http://aaarwkj.com/article38/jejopp.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供標(biāo)簽優(yōu)化、關(guān)鍵詞優(yōu)化、虛擬主機(jī)、自適應(yīng)網(wǎng)站、網(wǎng)站設(shè)計(jì)公司、建站公司
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶(hù)投稿、用戶(hù)轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)