One, the background

Because the website system growing, the different domain name business, even different partner website cookie may need more or less Shared use, encounter this situation, we usually think of is the use of login center distribution state of cookies to synchronize to solve, the high cost and implementation is more complex and trouble.

Because cookies in the case of cross-domain, browsers do not allow mutual access restrictions, in order to break through this limitation, so the following implementation scheme, using PostMessage and localstorage for cross-domain data sharing.

The principle is relatively simple, but there are also many pits encountered, here combing, do a backup.

Second, API design

As mentioned in the background, we use localstorage instead of cookies. There are some differences between localstorage and cookies. For example, localstorage has a larger capacity, but there is no expiration time. Postmessage can break easily if it is not used properly, and even though it supports cross-domain, security issues and API asynchronization make it difficult to use. How can we make it easier to use?

Take a look at the API I designed:

import { crosData } from 'base-tools-crossDomainData'; Var store = new crosData({iframeUrl:"somefile.html", // share iframe address with expire:'d,h,s' // Can also be planted when covered}); Store. Set ('key','val',{expire:'d,h,s' //option with expire}). Then ((data)=>{ //data {val:'val',key:'key',domain:'domain'}; }).catch((err)=>{ console.log(err); }); Store. The get (' key '{domain:' (. *). Sina. Cn '/ / can specify the domain name, can also be used (. *) to match the regular string, Return of val will bring domain information, don't fill in the return of this domain}). Then ((vals) = > {the console. The log (val) / / asynchronous access to store data, may be more, is an array / {}, {}}). The catch ((err) = > {}); store.clear('key').then().catch(); // Only the keys in the current domain are known. Only the keys in other domains can be readCopy the code

The speed of a module depends mainly on THE API, so for a data sharing module, I think it is OK to support set, GET and clear methods, because postMessage itself is an asynchronous behavior once and for all, packaged as a promise must be more appropriate and easy to use. Because localstorage does not support expiration time, we need a global expiration time configuration, of course, can also be set separately in the set, and when get we can specify to obtain data in a certain domain or multiple domains, because the key name may be repeated, but there is only one domain. The clear and set apis can only store data in the local domain and cannot operate data in other domains. Get is allowed.

Let’s take a look at the client Settings and APIS:

<! DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>crosData</title> </head> <body> <script> window.CROS = { Domain :/(.*).sina.cn/, // or you can allow the domain name, support re and * wildcard lz:false // whether lz compression val character}; </script> <script src="http://cdn/sdk.js"></script> </body> </html>Copy the code

You can use the js SDK of client in an HTML document of any domain, and then configure a domain whitelist that you allow to be classified into the domain of the document by using global properties, support for regex, and then enable lZ-string compression. I’ll talk about lZ compression later.

At this point, a more general API design is complete, let’s take a look at the implementation principle and some specific problems.

Three, the implementation principle

It sounds pretty simple, but it’s not really written, so first of all we need to know how to use postMessage, which is a very common API, and it has one key point here, Postmessages can only communicate with each other in an iframe or by using window.open to open a new page. Of course, we need to create a hidden iframe to cross domains.


First, the parent page creates a hidden IFrame, and then when the set, GET, clear and other commands are executed, the message is broadcast through postMessage. After the child page receives the message, the command is parsed. Data and callback IDS (postMessage can’t pass functions and references, it’s better to only pass string because of compatibility issues, so stringify data as well). Then, when the child page finishes processing the localstorage operation, it returns the corresponding Cbid and data to the parent page through postMessage. The parent page listens for message events and processes the results.

Four, coding

Well, so after a few lines, let’s start coding:

First of all, let’s introduce what third-party packages we use and why we use them:

1, URl-parse parse the URL, mainly using its origin attribute, because postMessage itself has a strict verification of Origin, we need to support whitelist and domain name management.

2, MS to time shorthand to do ms conversion tool library.

3, LZ-string to do string compression tool kit, here to give you popular science lZ compression algorithm, first understand LZ need to understand RLZ, Run Length Encoding, is a lossless compression algorithm is very simple. It replaces repeated bytes with a simple description of repeated bytes and the number of times they are repeated. Behind the LZ compression algorithm is the use of the RLE algorithm to replace references to the same byte sequence that appeared earlier. Simply put, the LZ algorithm is considered a string matching algorithm. For example, a string occurs frequently in a piece of text and can be represented by a string pointer that appears in previous text.

Lz-string itself has the advantage of greatly reducing your storage capacity. The 5MB localstorage itself will be compressed and used up quickly if it is used to support data storage of multiple domain names. However, LZ-String itself is relatively slow and consumes a lot of data. You can use this compression algorithm to optimize string length if you need to transfer data in your work. It is disabled by default.

4, Store2 itself localstorage API is relatively simple, in order to reduce the code logic complexity, here selected a popular localstorage implementation library to store operations.

The parent page of the package is 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; If (origin!) {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]); }}Copy the code

There are only a few ways to do it, and there are a few key points that I want to make.

1. The get, set and clear methods are all unified send methods, but the options part is completed.

2. Send returns a Promise object. If iframe has been onloaded, postHandle is called for postMessage. Wrapped with function, waiting for iframe onLoad to end after the unified call, function wrapped with postHandle method.

The postHandle method wraps data before sending the request and generates CBID, Origin, Action, and ARgs. The CBS object holds resolve and Reject under each Cbid and waits for the postMessage of the sub-page to return. Since postMessage does not hold references and cannot pass functions, this method is chosen for correlation.

When the class is initialized, we define the properties of the options we need, create the iframe, and then listen for the message event, processing the message returned by the child page.

5. In the message event of the parent page, we need to verify that the message sent to me must be the window I open iframe, otherwise an error is reported, and then execute resolve and reject in CBS according to the ERR identifier in data.

In createIframe, the callback in iframe onload handles the call method cached before creation. Note that domReady is used here, because the body may not be parsed before SDK execution.

Here is the code for the Child section:

Class iframe {set(key, val, options, origin) {// Check the size of val. val = this.lz ? lzstring.compressToUTF16(val) : val; var valsize = sizeof(val, '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 {// Update 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; }); return message; } clear(key, origin) { store.remove(`${this.prefix}_${key},${origin}`); return {}; } clearOtherKey() {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; Parse (evt.data); // bytes 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); }}); }}Copy the code

Code is also not much, here is a brief description of the use and organization of each method:

The constructor class also checks for browser support and defines the store prefix value, maximum number, and maxsize for each key. We then create the Message channel and wait for the parent page to call.

2. In message, we check origin sending broadcast, and then check the method called, call the corresponding set, get, clear methods, and then get the execution result, bind cBID, and finally send postMessage back to the parent page.

3. ClearOtherKey Deletes invalid store data and retains only the data in the format.

4. In the set method, size check is performed for each piece of data, lZ compression is performed, and the saved data contains Val, key, expiration time and update time (used for LRU calculation).

5. In the set method, if the number of LS stored exceeds the upper limit, you need to delete LRU, which is the abbreviation of Least Recently Used. We do a sort of key by going through all the keys, by lastTime, and then pop the keys array, get the keys that need to be cleared at the end of the stack, and then delete them one by one.

6. In the get method, we match the key of the domain we need to get by traversing all the key values, and then disassemble the key of the returned value (we store the key in the format of the domain), because the API requires to return multiple matching values. We finally make a filter for the expired data, and then use Lz to extract the val value to ensure that the user gets the correct result.

The above is an overall implementation of our coding process and review. The following is a description of the pits encountered.

Five, some encountered pits

Because the above only gave the main code, not the complete code, because its logic is clear, take a little time can be written. Here’s what’s wrong with it.

1. Calculate the storage value of localStorage.

Because we all know that there are 5 MB limit, so every piece of data requirements should not exceed 20 largest * 1024 bytes, for the calculation of bytes, localstorage to use utf16 code conversion, reference this article: JS calculation string of bytes | AlloyTeam

2. Compatibility

In Ie8, postMessage should be sent as a string, events should be smoothed out, and JSON should be smoothed out.

3. Asynchronous processing of iframe creation

Here, a recursive wait of setTimeout was done before, and then changed to the above implementation method, which uniformly processed the promise’s reslove after onload to ensure the unity of the PROMISE API.

4. Spatial complexity vs. temporal complexity when saving data.

The first version is not the above implementation, I implemented 3 versions:

The first version saved an LRU array to reduce the time complexity, but wasted the space complexity, and after testing, store get method takes a lot of time, mainly parse time.

In the second version, in order to maximize the lZ-String compression rate, I saved all the data including the LRU array to a key value. As a result, lZ-String and getItem consumed a lot of data, even though the computation time complexity was the lowest.

In the last version, which is the above version, I sacrificed some time complexity and space complexity, but because the bottleneck is set and GET read and write speed, single save read and write speed is extremely fast, obtain keys method because the bottom layer is used for in localstorage implementation, performance is still very good, The 20KB memory is 100, and the read and write time is about 1s. The performance is very good.

Sixth, summarize and contrast

After the module was written, I learned that there was a library called Zendesk /cross-storage

But I looked at his API and source code, compared the implementation method, I think it is my version of the more consideration.

1. My version has control over domain name and data management.

2. My version of the Promise API is much simpler, with one less onConnect than it is. You can refer to its implementation, which is much more than I wrote, and does not solve the problem of iframe waiting for async.

3. Lz compression data is not supported.

4, does not support LRU storage pool management, so it may be too much storage to write into the problem.

5, he seems to have an IFrame for every interaction, which is a waste of DOM manipulation and broadcasting. I think it is ok to leave it on all the time. Of course, he may need to connect multiple clients to do so.

Ok, hope the above content is helpful to you.