Web サイトのシステムはますます大きくなっているため、さまざまなドメイン名やさまざまなパートナー Web サイトの Cookie を多かれ少なかれ共有する必要がある場合があります。このような状況に遭遇したとき、誰もが通常、ログイン センターを使用して Cookie ステータスを配布することを考えます。解決策としては、コストが高くなり、実装がより複雑で面倒になります。
Cookie はクロスドメインであるため、ブラウザーは相互アクセスをまったく許可しません。この制限を突破するために、次の実装計画を使用して、postmessage と localstorage を使用してドメイン間でデータを共有します。
原理は比較的単純ですが、落とし穴もたくさんありますので、ここで整理してバックアップしておきます。
2. API設計背景で述べたように、Cookie の代わりに localstorage を使用します。たとえば、localstorage は容量が大きくなりますが、有効期限はありません。また、ポストメッセージはクロスドメインをサポートしていますが、スペースの上限があるため、使用する際に問題が発生します。使いやすいですか?
最初に私が設計した API を見てみましょう。
import { crosData } from 'base-tools-crossDomainData';var store = new crosData({ iframeUrl:somefile.html, //共有 iframe アドレス、iframe には特別な要件があります。テンプレート ファイルを参照してください。expire:'d,h,s' / /日、時間、秒単位のデフォルトの有効期限は、植え付け時に上書きすることもできます});store.set('key','val',{expired:'d,h,s' //オプション有効期限を設定でき、expire をオーバーライドできます。}).then((data)=>{ //非同期メソッド。失敗すると、catch イベントに入ります。 //data {val:'val',key:'key',domain :' ドメイン'};}).catch((err)=>{ console.log(err);}); store.get('key',{ ドメイン:'(.*).sina.cn' //ドメイン名を指定することも、(.*) を使用して通常の文字列と一致させることもできます。入力しない場合は、ローカル ドメインが返されます。 =>{ console.log (val) //保存されたデータを非同期で取得します。複数ある可能性があります。配列です。 [{},{}]}).catch((err)=>{});store.clear ('キー').then(); //現在のドメインのキーのみをクリアします。他のドメインのキーはクリアできません。読み取りのみが可能です。モジュールが使いやすいかどうかは API に依存するので、データ共有モジュールの場合は、postmessage 自体が 1 回限りの非同期動作であるため、set、get、clear の 3 つのメソッドをサポートすることで問題ないと思いますが、そして、より適切で使いやすい Promise にパッケージ化する必要があります。 localstorage は有効期限をサポートしていないため、グローバルな有効期限の設定が必要です。もちろん、取得時に、キー名が指定されているため、特定のドメインのデータを取得するように指定することもできます。繰り返しになりますが、ドメインは 1 つだけです。これにはデータ管理が関係します。最後に、clear および set API はこのドメインにのみデータを植えることができ、他のドメインのデータを操作することはできません。
クライアント設定と API を見てみましょう。
<!DOCTYPE html><html> <head> <meta charset=utf-8> <title>crosData</title> </head> <body> <script> window.CROS = { ドメイン:/(.*)。 sina.cn/, //または許可するドメイン名は、通常のワイルドカードと * ワイルドカードをサポートします lz:false // val 文字の lz 圧縮を有効にするかどうか </script> <script}; src=http://cdn/sdk.js></script> </body></html>クライアントの js SDK を任意のドメインの HTML ドキュメントに柔軟に導入し、グローバル属性を通じてこのドキュメントが配置されているドメインに配置できるようにドメイン ホワイトリストを構成できます。正規表現がサポートされており、lz は次のいずれかです。 lz-string圧縮を開始します。 lz圧縮とは何かについては後ほど紹介します。
この時点で、比較的一般的な API 設計が完了しました。実装の原則といくつかの具体的な問題を見てみましょう。
3. 実施原則とても簡単そうに見えますが、実際には書かれていません。これは非常に一般的な API です。ここで重要な点が 1 つあります。それは、postMessage は iframe 内でのみ使用できるということです。もちろん、ここでは最初にクロスドメイン用の非表示の iframe を作成する必要があります。
プロセスは比較的明確なので、ツールを使用して絵を描くのは面倒です。ここでは、最初に親ページが非表示の iframe を作成し、次に set、get、clear などのコマンドを実行するときに説明します。などが実行されると、メッセージは postMessage を通じてブロードキャストされます。ページはメッセージを受信した後、コマンド、データ、コールバック ID を解析します (互換性の問題により、postMessage は関数や参照を渡すことができません。文字列型のみを渡すことをお勧めします)。したがって、データを文字列化する必要があります)。次に、子ページが localstorage 操作を完了すると、対応する cbid とデータを postMessage を通じて親ページに返します。親ページはメッセージ イベントをリッスンし、結果を処理します。
4. エンコーディングさて、数行しかないのでコーディングを始めましょう。
まず、私たちが使用しているサードパーティ パッケージと、それらを使用する理由を紹介しましょう。
1. url-parse は、主にその元の属性を使用して URL を解析します。これは、postMessage 自体が送信元の厳密な検証を備えており、ホワイトリストとドメイン名の管理もサポートする必要があるためです。
2. ms は、時間の略称をミリ秒に変換するためのツール ライブラリです。
3. lz-string は文字列を圧縮するためのツールキットです。LZ 圧縮アルゴリズムについての一般的な科学入門です。まず、LZ を理解するには、可逆圧縮のための非常に単純なアルゴリズムである RLZ (ランレングス エンコーディング) を理解する必要があります。繰り返されるバイトを、繰り返されるバイトと繰り返しの数の簡単な説明に置き換えます。 LZ 圧縮アルゴリズムの背後にある考え方は、RLE アルゴリズムを使用して、以前に発生した同じバイト シーケンスへの参照を置き換えることです。簡単に言うと、LZ アルゴリズムは文字列照合アルゴリズムとみなされます。たとえば、特定の文字列はテキストの一部に頻繁に出現し、前のテキストに出現する文字列ポインタによって表すことができます。
lz-string 自体の利点は、複数のドメイン名のデータ ストレージをサポートするために 5MB のローカル ストレージを使用すると、ストレージ容量が大幅に削減されることです。ただし、lz-string 自体は圧縮され、すぐに使い果たされてしまいます。動作が遅くなり、より多くの費用がかかります。仕事で送信するデータ量にサイズ要件がある場合は、この圧縮アルゴリズムを使用して文字列の長さを最適化することができます。デフォルトでは有効になっていません。
4. store2 の localstorage API 自体は比較的単純です。コード ロジックの複雑さを軽減するために、ストア操作を実行するために一般的な localstorage 実装ライブラリが選択されます。
サードパーティのパッケージについて話した後、親ページの js を記述する方法を見てみましょう。
クラス crosData {constructor(options) { supportCheck(); this.options = Object.assign({ iframeUrl: '', 有効期限: '30d' }, this.cbs = {}; .iframeBeforeFuns = []; this.parent = ウィンドウ; this.origin = 新しい URL(this.options.iframeUrl).origin; this.createIframe(this.options.iframeUrl); addEvent(this.parent, 'message', (evt) => { var data = JSON.parse(evt.data); varorigin = evt.origin || evt.originalEvent .origin; //開いた iframe のメッセージのみを受信し、その他は不正で、 (origin !== this.origin) { の場合はエラーが直接報告されます。拒否('不正なオリジン!'); } if (data.err) { this.cbs[data.cbid].reject(data.err) } else { this.cbs[data.cbid].resolve(data) .ret); } this.cbs[data.cbid] を削除します }); } createIframe(url) { addEvent(document, 'domready', () => { varフレーム = document.createElement('iframe'); フレーム.スタイル.cssText = '幅:1px;高さ:1px;境界線:0;位置:絶対;左:-9999px;上:-9999px;'; 'src', url); フレーム.onload = () => { this.child = フレーム.コンテンツウィンドウ; this.iframeBeforeFuns.forEach(item => item()); } document.body.appendChild(frame) }) } postHandle(type, args) { return new Promise((resolve, accept) => { var cbid = this.cid; var message = { cbid: cbid、オリジン: 新しい url(location.href).origin、アクション: タイプ、引数: args } this.child.postMessage(JSON.stringify(message), this.origin); this.cbs[cbid] = { 解決、拒否 } 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({ 有効期限: ms (this.options.expire) }, options); return this.send('set', [key, val, options]) } get(key, options); Object.assign({ ドメイン: 新しい URL(location.href).origin }, options); return this.send('get', [key, options]) } clear(key) { return this.send('clear) '、 [鍵]); }}方法はおそらくいくつかしかありません。ここで重要なポイントをいくつか説明します。
1. getメソッド、setメソッド、clearメソッドを一律にsendメソッドと呼びますが、オプション部分を補足します。
2. send メソッドは、iframe が正常にロードされた場合、postHandle メソッドを直接呼び出して postMessage 操作を実行します。iframe がまだロード中の場合は、現在の操作が iframeBeforeFuns 配列にプッシュされます。関数を呼び出し、統合呼び出しの後、iframe onload が終了するのを待ちます。関数は postHandle メソッドにもラップされます。
3. postHandle メソッドは、リクエストを送信する前にデータをラップし、cbid、origin、action、args を生成します。cbs オブジェクトは、各 cbid での解決と拒否を保存し、処理する前にサブページの postMessage が返されるのを待ちます。 postMessage は参照を保持できず、関数を渡すこともできないため、ここでは関連付けのためにこのメソッドが選択されています。
4. コンストラクターは理解しやすいです。このクラスを初期化するときに、必要なオプション属性を定義し、iframe を作成して、メッセージ イベントをリッスンして、サブページから返されたメッセージを処理します。
5. 親ページのメッセージ イベントで、私に送信されたメッセージが私が開いたウィンドウ iframe であることを確認する必要があります。そうでない場合はエラーが報告され、cbs での解決と拒否が以下に従って実行されます。データ内のエラー識別子。
6. createIframe メソッドでは、iframe onload のコールバックが作成前にキャッシュの呼び出しメソッドを処理します。これは、本体が解析される前に SDK が実行される可能性があるため、ここでの domready の使用に注意してください。
子部分のコードは次のとおりです。
class iframe { set(key, val, options,origin) { //val サイズを確認します。val = val.toString() : val; valsize = sizeof(val, 'utf16'); //localStorage は utf16 エンコーディングを使用してバイトを保存します if (valsize > this.maxsize) { return { err: 'ストア値: ' + valstr + ' サイズは ' + valsize + 'b、maxsize :' + this.maxsize + 'b、utf16 を使用' } } key = `${this.prefix}_${key}, ${新しい url(origin).origin}`; var data = { val: val、lasttime: Date.now()、expired: Date.now() + options.expire }; store.set(key, data); //最大ストレージ数を超えている場合は、最後に更新されたものを削除します if (store.size() > this.storemax) { var key = store.keys(); key.sort( (a, b) => { var item1 = store.get(a), item2 = store.get(b); return item2.lasttime - item1.lasttime; }); this.storemax - store.size()); while (removesize) {store.remove(keys.pop()); } } return { ret: data } } get(key, options) { var message = {};キー = store.keys(); var regexp = new RegExp('^' + this.prefix + '_' + key + ',' + options.domain + '$'); key.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) } else { //前回の更新; val: data.val、lasttime: Date.now()、expired: data.expire }); } data.val = this.lz ? lzstring.decompressFromUTF16(data.val) : data.val; filter(item => { return !!item; // 未定義のメッセージをフィルターします }); store.remove(`${this.prefix}_${key},${origin}`); return {}; } clearOtherKey() { //不正なキーを削除します var key = store.keys(); new RegExp('^' + this.prefix); key.forEach(key => { if (!keyReg.test(key)) { store.remove(key); } }); }constructor(safeDomain, lz) { supportCheck(); this.safeDomain = safetyDomain || /.*/; this.prefix = '_cros'; if (Object.prototype.toString.call(this. safeDomain) !== '[object RegExp]') { throw new Error('safeDomain は regexp である必要があります') } this.lz = lz; this.storemax = 100; this.maxsize = 20 * 1024; //byte addEvent(window, 'message', (evt) => { var data = JSON.parse(evt.data); varoriginHostName = new url( evt) .origin).hostname; var 原点 = evt.origin、アクション = data.action、cbid = data.cbid、args = data.args; // 正当なブロードキャスト if (evt.origin === data.origin && this.safeDomain.test(originHostName)) { args.push(origin) = ['set', 'get', 'clear'] ; if (whiteAction.indexOf(action) > -1) { var message = this[action].apply(this, args); window.top.postMessage(JSON.stringify(メッセージ), 起点); } } else { window.top.postMessage(JSON.stringify({ cbid: cbid, err: '不正なドメイン' }), } }); ; }}コードはそれほど多くありません。各メソッドの使用法と構成の関係を簡単に説明します。
1. コンストラクター部分では、上記のクラスはブラウザー機能のサポートもチェックし、ストアのプレフィックス値、最大数、各キーの最大サイズなどの属性を定義します。次に、メッセージ チャネルを作成し、親ページがそれを呼び出すのを待ちます。
2. メッセージ内で、ブロードキャストの発信元を確認し、呼び出されたメソッドを確認し、対応する set、get、clear メソッドを呼び出して、実行結果を取得し、cbid をバインドして、最後に postMessage 親ページを送り返します。
3. clearOtherKey は、一部の不正ストアデータを削除し、フォーマットに準拠したデータのみを保持します。
4. set メソッドでは、各データに対してサイズ検証と lz 圧縮が実行され、保存されるデータには val、key、有効期限、更新時刻 (LRU 計算に使用) が含まれます。
5. set 方式では、保存されている IS 数が上限を超えた場合、この時点で削除操作が必要になります。LRU は、Least Recently Used の略で、最も最近使用されていないものです。すべてのキー値を走査し、キー値を並べ替え、最後の時刻を渡し、次にキー配列のポップ操作を実行してスタックの最後にあるクリアする必要のあるキーを取得し、それらを 1 つずつ削除します。 。
6. get メソッドでは、すべてのキー値を調べ、取得する必要があるドメインのキーと一致させ、戻り値でキーを逆アセンブルします (キーとドメインの形式で格納します)。複数の一致する値を返す必要があります。期限切れのデータに対して最終フィルターを作成し、lz を使用して val 値を解凍し、ユーザーが正しい結果を取得できるようにします。
以上が全体的な実装コーディング プロセスと、遭遇する落とし穴について説明します。
5. いくつかの落とし穴に遭遇上記はメインコードのみなので完全なコードではありませんが、ロジック自体は比較的明確なので、少しの時間で書くことができます。以下で落とし穴について話しましょう。
1. localstorage のストレージ値を計算します。
5MB の制限があることは誰もが知っているので、各データの最大要件は 20*1024 バイトを超えることはできません。バイトの計算については、localstorage で変換に utf16 エンコーディングを使用する必要があります。この記事を参照してください: JS 文字の計算。 Strings が占めるセクションの数
2. 互換性
IE8 では、postMessage で文字列を渡すのが最善です。イベントは平滑化する必要があり、JSON も平滑化する必要があります。
3. iframe作成時の非同期処理
ここでは、以前は setTimeout を再帰的に wait していましたが、onload 後に Promise の reslove を均一に処理するように変更して、Promise API の統一を図りました。
4. データを保存する場合、スペースの複雑さと時間の複雑さ。
最初のバージョンは上記の実装ではなく、3 つのバージョンを実装しました。
最初のバージョンでは、時間の複雑さを軽減するために LRU 配列が保存されますが、さらに、テスト後のストアの get メソッドは、主に解析に時間がかかるため、比較的時間がかかります。
2 番目のバージョンでは、lz-string の圧縮率を最大化するために、LRU 配列を含むすべてのデータをキー値に保存しました。その結果、lz-string と getItem がある場合、解析時間の消費が非常に大きくなります。大量のデータが含まれますが、計算時間は最も低くなります。
最後のバージョンは、時間の複雑さとスペースの複雑さをある程度犠牲にしましたが、ボトルネックは set と get の読み取りと書き込みの速度にあるため、1 回の保存の読み取りと書き込みの速度は非常に高速です。キーはローカルストレージで使用されるため、パフォーマンスは依然として非常に優れており、100 エントリを 20kb に保存でき、読み取りおよび書き込み時間は約 1 秒です。
6. 概要と比較モジュールを書いた後、zendesk/cross-storage というライブラリがあることに気付きました。
しかし、私は彼の API とソースコードを確認し、実装方法を比較しました。私のバージョンの方が重要だと思います。
1. 私のバージョンでは、ドメイン名とデータ管理を制御できます。
2. 私のバージョンの Promise API はより簡素化されており、onConnect が 1 つ少なくなっています。これは私が書いたものよりもはるかに多く、非同期を待機する iframe の問題は解決されません。
3. LZ 圧縮データはサポートされていません。
4. LRU ストレージ プール管理はサポートされていないため、ストレージが多すぎると書き込みエラーが発生する可能性があります。
5. 彼はインタラクションごとに iframe を作成しているようですが、これは DOM 操作とブロードキャストの無駄です。もちろん、複数のクライアントに接続する必要があるため、このように処理することは問題ないと思います。 。
要約する上記は、クロスドメインのデータ共有を実現するために Cookie の代わりにローカルストレージを使用する問題について編集者が紹介したものです。ご質問があれば、メッセージを残してください。編集者が返信します。間に合うように。また、VeVb武道サイトを応援してくださった皆様、誠にありがとうございました!