Debido a que el sistema del sitio web es cada vez más grande, es posible que sea necesario compartir más o menos cookies de diferentes nombres de dominio e incluso de diferentes sitios web asociados. Cuando se encuentra con esta situación, lo que todos suelen pensar es utilizar el centro de inicio de sesión para distribuir el estado de las cookies. y luego sincronizarlo Solución, el costo es mayor y la implementación es más complicada y problemática.
Debido a que las cookies son entre dominios, los navegadores no permiten ningún acceso mutuo. Para superar esta restricción, se utiliza el siguiente plan de implementación para compartir datos entre dominios utilizando postmessage y almacenamiento local.
El principio es relativamente simple, pero se han encontrado muchos obstáculos. Vamos a solucionarlo aquí y hacer una copia de seguridad.
2. Diseño de APIComo se mencionó anteriormente, utilizamos almacenamiento local en lugar de cookies. Existen algunas diferencias en el uso entre el almacenamiento local y las cookies. Por ejemplo, el almacenamiento local tiene una capacidad mayor, pero no hay tiempo de vencimiento, aunque la capacidad es grande, está disponible. diferentes navegadores El límite superior de espacio hace que sea fácil fallar si el funcionamiento no es bueno. Además, aunque postmessage admite dominios cruzados, los problemas de seguridad y la API asincrónica también generan algunos problemas de uso. más fácil de usar?
Primero echemos un vistazo a la API que diseñé:
import { crosData } from 'base-tools-crossDomainData';var store = new crosData({ iframeUrl:somefile.html, //Dirección de iframe compartida, el iframe tiene requisitos especiales, consulte el archivo de plantilla expire:'d,h,s' / /El tiempo de vencimiento predeterminado en días, horas, segundos, también se puede sobrescribir al plantar});store.set('key','val',{ expire:'d,h,s' //opción Puede traer tiempo de vencimiento, anular el vencimiento}).then((data)=>{ //Método asincrónico, si falla, ingresará el evento de captura //data {val:'val',key:'key',domain :' dominio'};}).catch((err)=>{ console.log(err);}); store.get('key',{ dominio:'(.*).sina.cn' //Puede especificar un nombre de dominio o puede usar (.*) para que coincida con cadenas normales. La información de valor devuelta incluirá información de dominio. Si no se completa, devolverá el dominio local }).then((vals) =>{ console.log (val) //Obtiene los datos almacenados de forma asincrónica, puede haber varios, es una matriz [{},{}]}).catch((err)=>{});store.clear ('clave').luego().catch(); // Borrar solo las claves del dominio actual. No se permite borrar las claves de otros dominios. Solo se pueden leer.El uso rápido de un módulo depende de la API, por lo que, para un módulo de intercambio de datos, creo que está bien admitir los tres métodos de establecer, obtener y borrar, porque el mensaje posterior en sí es un comportamiento asincrónico de una sola vez. y debe empaquetarse en una promesa. Más adecuado y más fácil de usar. Debido a que el almacenamiento local no admite el tiempo de vencimiento, se requiere una configuración de tiempo de vencimiento global. Por supuesto, también se puede configurar individualmente durante el conjunto. Al obtenerlo, podemos especificar si desea obtener datos de un determinado dominio o datos de varios dominios, porque Nombres clave. Puede repetirse, pero solo hay un dominio. Esto implica la gestión de datos. Hablaremos de ello por separado más adelante. Finalmente, las API claras y configuradas solo pueden colocar datos en este dominio y no pueden operar datos en otros dominios.
Echemos un vistazo a la configuración del cliente y la API:
<!DOCTYPE html><html> <head> <meta charset=utf-8> <title>crosData</title> </head> <body> <script> window.CROS = { dominio:/(.*). sina.cn/, //O el nombre de dominio que permita, admite comodines regulares y * lz:false //Si habilitar la compresión lz de caracteres val} </script> <script src=http://cdn/sdk.js></script> </body></html>Puede introducir de manera flexible el js SDK del cliente en un documento html en cualquier dominio y luego configurar una lista blanca de dominio que le permita ubicarse en el dominio donde se encuentra este documento a través de atributos globales. Admite expresiones regulares, y luego lz es si. Inicie la compresión de cadenas lz. Más adelante presentaré qué es la compresión lz.
En este punto, se completa un diseño de API relativamente general. Echemos un vistazo a los principios de implementación y algunos problemas específicos.
3. Principio de implementaciónSuena muy simple, pero en realidad no está escrito. Primero debemos saber cómo usar postMessage. Esta es una API muy común. Hay un punto importante que decirle aquí, es decir, postMessage solo se puede usar en un iframe. o usar una ventana .open es una forma de abrir una nueva página para comunicarse entre sí. Por supuesto, aquí primero debemos crear un iframe oculto para dominios cruzados.
Soy demasiado vago para usar herramientas para hacer dibujos, porque el proceso es relativamente claro. Aquí volveré a contar todo el proceso de comunicación en palabras. Primero, la página principal crea un iframe oculto y luego, cuando se ejecutan comandos como configurar, obtener y borrar. , etc., el mensaje se transmite a través de postMessage. Después de que la página recibe el mensaje, analiza el comando, los datos y el ID de devolución de llamada (postMessage no puede pasar funciones y referencias debido a problemas de compatibilidad. Es mejor pasar solo el tipo de cadena). , por lo que los datos deben ser stringificados). Luego, cuando la página secundaria completa la operación de almacenamiento local, devuelve el cbid y los datos correspondientes a la página principal a través de postMessage. La página principal escucha el evento del mensaje y procesa los resultados.
4. CodificaciónBueno, solo hay unas pocas líneas, comencemos a codificar:
Primero, introduzcamos qué paquetes de terceros utilizamos y por qué los utilizamos:
1. url-parse analiza la URL, principalmente utilizando el atributo de origen que contiene, porque postMessage en sí tiene una verificación estricta del origen y también necesitamos admitir la administración de nombres de dominio y listas blancas.
2. ms es una biblioteca de herramientas para convertir la abreviatura de tiempo en milisegundos.
3. lz-string es un conjunto de herramientas para comprimir cadenas. Aquí hay una introducción científica al algoritmo de compresión LZ. Primero, para comprender LZ, debe comprender RLZ, Run length Encoding, que es un algoritmo muy simple para compresión sin pérdidas. Reemplaza los bytes repetidos con una descripción simple de los bytes repetidos y el número de repeticiones. La idea detrás del algoritmo de compresión LZ es utilizar el algoritmo RLE para reemplazar una aparición anterior de una referencia a la misma secuencia de bytes. En pocas palabras, el algoritmo LZ se considera un algoritmo de coincidencia de cadenas. Por ejemplo: una determinada cadena aparece con frecuencia en un fragmento de texto y puede representarse mediante un puntero de cadena que aparece en el texto anterior.
La ventaja de lz-string en sí es que puede reducir en gran medida su capacidad de almacenamiento. Si se utiliza el almacenamiento local de 5 MB para admitir el almacenamiento de datos de varios nombres de dominio, se comprimirá y consumirá rápidamente. más lento y consume más dinero. Si tiene requisitos de tamaño para la cantidad de datos que se transmitirán en el trabajo, puede intentar utilizar este algoritmo de compresión para optimizar la longitud de la cadena.
4. La API de almacenamiento local de store2 en sí es relativamente simple. Para reducir la complejidad de la lógica del código, se selecciona una biblioteca de implementación de almacenamiento local popular para realizar operaciones de almacenamiento.
Después de hablar sobre el paquete de terceros, echemos un vistazo a cómo escribir el js de la página principal:
clase crosData { constructor(opciones) { supportCheck(); this.options = Object.assign({ iframeUrl: '', expire: '30d' }, opciones this.cid = 0; .iframeBeforeFuns = []; this.parent = ventana; this.origin = nueva URL (this.options.iframeUrl).origin; this.createIframe(this.options.iframeUrl); addEvent(this.parent, 'mensaje', (evt) => { var datos = JSON.parse(evt.data); var origen = evt.origin || evt.originalEvent .origin; // Solo recibo el mensaje del iframe que abrí, los demás son ilegales y se informará un error directamente si (origin! == this.origin) { rechazar('origen ilegal!'); retorno; } if (data.err) { this.cbs[data.cbid].reject(data.err } else { this.cbs[data.cbid].resolve(data) .ret); } eliminar this.cbs[data.cbid]; } } createIframe(url) { addEvent(documento, 'domready', () => { var marco = document.createElement('iframe'); frame.style.cssText = 'ancho:1px;alto:1px;border:0;posición:absoluta;izquierda:-9999px;arriba:-9999px;'; 'src', url); frame.onload = () => { this.child = frame.contentWindow; this.iframeBeforeFuns.forEach(item => item()); } document.body.appendChild(frame }); } postHandle(type, args) { return new Promise((resolver, rechazar) => { var cbid = this.cid; var mensaje = { cbid: cbid, origen: nueva url(ubicación.href).origin, acción: tipo, args: args } this.child.postMessage(JSON.stringify(mensaje), this.origin); this.cbs[cbid] = {resolver, rechazar} this.cid++}); => { if (this.child) { return this.postHandle(tipo, args).luego(resolver } else { var self = this); this.iframeBeforeFuns.push(function() { self.postHandle(type, args).then(resolve); } } }) } set(key, val, options) { options = Object.assign({ expirar: ms (this.options.expire) }, opciones); return this.send('set', [clave, val, opciones]); Object.assign({ dominio: nueva url(ubicación.href).origin }, opciones); devolver this.send('get', [clave, opciones]); ', [llave]); }}Probablemente solo existan unos pocos métodos. Aquí hay algunos puntos clave, permítanme hablar sobre ellos.
1. Los métodos get, set y clear se denominan uniformemente métodos de envío, pero la parte de opciones se complementa.
2. El método de envío devuelve un objeto de promesa. Si el iframe se ha cargado correctamente, se llama directamente al método postHandle para realizar la operación postMessage. Si el iframe aún se está cargando, la operación actual se envía a la matriz iframeBeforeFuns, envuelta con un. función y espera a que finalice la carga del iframe. Después de la llamada unificada, la función también se incluye en el método postHandle.
3. El método postHandle envuelve los datos antes de enviar la solicitud y genera cbid, origen, acción y argumentos. El objeto cbs guarda la resolución y el rechazo en cada cbid y espera a que regrese el postMessage de la subpágina antes de procesar. Debido a que postMessage no puede retener referencias y no puede pasar funciones, aquí se elige este método para la asociación.
4. El constructor es fácil de entender. Cuando se inicializa esta clase, definimos algunos atributos de opciones que necesitamos, creamos un iframe, luego escuchamos el evento del mensaje y procesamos el mensaje devuelto por la subpágina.
5. En el evento de mensaje de la página principal, debemos verificar que el mensaje que me enviaron debe ser el iframe de la ventana que abrí; de lo contrario, se informará un error y luego la resolución y el rechazo en cbs se ejecutarán de acuerdo con el identificador de error en los datos.
6. En el método createIframe, la devolución de llamada en iframe onload maneja el método de llamada de almacenamiento en caché antes de la creación. Preste atención al uso de domready aquí, porque el SDK puede ejecutarse antes de que se analice el cuerpo.
Aquí está el código para la parte secundaria:
class iframe { set(key, val, options, origin) { //Verifique el tamaño del valor, que no puede exceder los 20k val = val.toString(); val = this.lz ? valsize = sizeof(val, 'utf16'); //localStorage almacena bytes usando codificación utf16 if (valsize > this.maxsize) { return { err: 'el valor de su tienda: ' + valstr + ' el tamaño es ' + valsize + 'b, maxsize :' + this.maxsize + 'b , use utf16' } } key = `${this.prefix}_${key}, ${nueva URL(origen).origen}`; var datos = { val: val, última vez: Fecha.ahora(), caducar: Fecha.ahora() + opciones.expire }; store.set(key, data); // Si es mayor que el número máximo de almacenamiento, elimine la última actualización if (store.size() > this.storemax) { var claves = store.keys(); llaves.sort( (a, b) => { var elemento1 = store.get(a), item2 = store.get(b); devolver elemento2.lasttime - item1.lasttime; var removesize = Math.abs( este.storemax - store.size()); while (removesize) { store.remove(keys.pop()); removesize--; claves = store.keys(); var regexp = new RegExp('^' + this.prefix + '_' + clave + ',' + opciones.dominio + '$'); claves.filter((clave) => { return regexp.test(clave); }).map((storeKey) => { var datos = store.get(storeKey); data.key = clave; data.domain = storeKey .split(',')[1]; if (data.expire < Date.now()) { store.remove(storeKey); return indefinido; //Actualizar la última vez; val: data.val, última vez: Date.now(), expirar: data.expire }); } data.val = this.lz? lzstring.decompressFromUTF16(data.val): datos de retorno }). filter(item => { return !!item; //Filtro indefinido }); mensaje de retorno } clear(key, origin) { store.remove(`${this.prefix}_${key},${origin}`); return {}; clearOtherKey() { //Eliminar claves ilegales var claves = store.keys(); new RegExp('^' + this.prefix); claves.forEach(clave => { if (!keyReg.test(clave)) { store.remove(clave); } }); } constructor(safeDomain, lz) { supportCheck(); this.safeDomain = safeDomain || this.prefix = '_cros'; safeDomain) !== '[object RegExp]') { throw new Error('safeDomain debe ser regexp' } this.lz = lz; this.storemax = 100; this.maxsize = 20 * 1024; //byte addEvent(ventana, 'mensaje', (evt) => { var data = JSON.parse(evt.data); var originHostName = nueva URL( evt .origin).nombre de host; var origen = evt.origin, acción = datos.acción, cbid = datos.cbid, args = datos.args; // Transmisión legal if (evt.origin === data.origin && this.safeDomain.test(originHostName)) { args.push(origin); var whiteAction = ['set', 'get', 'clear']; if (whiteAction.indexOf(action) > -1) { var mensaje = this[action].apply(this, args message.cbid = cbid); window.top.postMessage(JSON.stringify(mensaje), origen); } } else { window.top.postMessage(JSON.stringify({ cbid: cbid, err: 'Dominio ilegal' }), origen); ; }}No hay mucho código. Aquí hay una breve introducción al uso y la relación organizativa de cada método:
1. En la parte del constructor, la clase anterior también verifica la compatibilidad con las funciones del navegador y luego define atributos como el valor del prefijo de la tienda, el número máximo y el tamaño máximo de cada clave. Luego creamos un canal de mensajes y esperamos a que la página principal lo llame.
2. En el mensaje, verificamos el origen de la transmisión, luego verificamos el método llamado, llamamos a los métodos set, get y clear correspondientes, luego obtenemos el resultado de la ejecución, vinculamos cbid y finalmente devolvemos la página principal postMessage.
3. clearOtherKey elimina algunos datos almacenados ilegalmente y solo retiene los datos que se ajustan al formato.
4. En el método set, la verificación del tamaño y la compresión lz se realizan en cada dato. Los datos guardados incluyen val, clave, tiempo de vencimiento y tiempo de actualización (utilizados para el cálculo de LRU).
5. En el método establecido, si el número de ls almacenados excede el límite máximo, se requiere una operación de eliminación en este momento. LRU es la abreviatura de Menos utilizado recientemente, es decir, el menos utilizado recientemente. Recorremos todos los valores clave, ordenamos los valores clave, pasamos la última vez y luego realizamos la operación emergente de la matriz de claves para obtener las claves que deben borrarse al final de la pila y luego las eliminamos una por una. .
6. En el método get, recorremos todos los valores clave, hacemos coincidir la clave del dominio que necesitamos obtener y luego desensamblamos la clave en el valor de retorno (la almacenamos en el formato de clave y dominio), porque La API requiere que se devuelvan varios valores coincidentes. Hacemos un filtro final de los datos caducados y luego usamos lz para descomprimir el valor val para garantizar que el usuario obtenga el resultado correcto.
Lo anterior es nuestro proceso general de codificación y revisión de implementación. Hablemos de los obstáculos encontrados.
5. Algunos obstáculos encontradosDebido a que arriba solo se proporciona el código principal, no es el código completo. Debido a que la lógica en sí es relativamente clara, se puede escribir en poco tiempo. Hablemos de los peligros a continuación.
1. Calcule el valor de almacenamiento del almacenamiento local.
Como todos sabemos que existe un límite de 5 MB, el requisito máximo para cada dato no puede exceder los 20 * 1024 bytes. Para el cálculo de bytes, el almacenamiento local debe utilizar la codificación utf16 para la conversión. Consulte este artículo: Cálculo de caracteres JS. Ocupado por cadenas número de secciones
2. Compatibilidad
Es mejor pasar cadenas en postMessage en IE8. Los eventos deben suavizarse y JSON debe suavizarse.
3. Procesamiento asincrónico al crear iframe
Aquí, anteriormente hicimos una espera recursiva para setTimeout, pero luego lo cambiamos al método de implementación anterior. Después de la carga, la reslove de la promesa se procesa de manera uniforme para garantizar la unificación de la API de la promesa.
4. Al guardar datos, complejidad del espacio versus complejidad del tiempo.
La primera versión no es la implementación anterior, implementé 3 versiones:
La primera versión guarda una matriz LRU para reducir la complejidad del tiempo, pero desperdicia la complejidad del espacio. Además, después de la prueba, el método de obtención de la tienda consume relativamente tiempo, principalmente debido al tiempo de análisis.
En la segunda versión, para maximizar la tasa de compresión de lz-string, guardé todos los datos, incluida la matriz LRU, en un valor clave. Como resultado, lz-string y getItem consumen mucho tiempo de análisis cuando hay. muchos datos. Aunque el cálculo La complejidad del tiempo es la más baja.
La última versión es la anterior. Sacrifiqué algo de complejidad de tiempo y espacio, pero debido a que el cuello de botella radica en la velocidad de lectura y escritura de set y get, la velocidad de lectura y escritura de un solo guardado es extremadamente rápida. Las claves se deben a que se usa la capa inferior. En el almacenamiento local, el rendimiento sigue siendo muy bueno. Se pueden almacenar 100 entradas en 20 kb y el tiempo de lectura y escritura es de aproximadamente 1 segundo.
6. Resumen y comparaciónDespués de escribir el módulo, me di cuenta de que existe una biblioteca de este tipo: zendesk/cross-storage
Pero verifiqué su API y su código fuente y comparé los métodos de implementación. Creo que mi versión es más importante.
1. Mi versión tiene control sobre el nombre de dominio y la gestión de datos.
2. Mi versión de la API de promesa es más simplificada y tiene un onConnect menos. Puede consultar su implementación. Es mucho más que lo que escribí y no resuelve el problema del iframe esperando asincrónico.
3. No se admiten datos comprimidos LZ.
4. No se admite la administración del grupo de almacenamiento LRU, por lo que es posible que haya demasiado almacenamiento que pueda causar fallas de escritura.
5. Parece crear un iframe para cada interacción, lo cual es un desperdicio de operaciones y transmisiones DOM. Creo que no hay problema en dejarlo activado. Por supuesto, es posible que necesite conectarse a varios clientes, por lo que lo maneja de esta manera. .
ResumirLo anterior es la introducción del editor al problema del uso de almacenamiento local en lugar de cookies para realizar el intercambio de datos entre dominios. Espero que le resulte útil. Si tiene alguna pregunta, déjeme un mensaje y el editor le responderá. a tiempo. ¡También me gustaría agradecer a todos por su apoyo al sitio web de artes marciales VeVb!