Интервьюер: Межстраничная коммуникация, какие методы вы знаете?

браузер
Интервьюер: Межстраничная коммуникация, какие методы вы знаете?

введение

В браузере мы можем одновременно открывать несколько вкладок, и каждую вкладку можно грубо понимать как «независимую» рабочую среду, и даже глобальные объекты не будут использоваться несколькими вкладками. Однако иногда мы хотим иметь возможность синхронизировать данные страницы, информацию или состояние между этими «независимыми» страницами вкладок.

Так же, как в следующем примере: после того, как я нажму «Избранное» на странице списка, соответствующая кнопка страницы сведений будет автоматически обновлена ​​до состояния «Избранное»; аналогичным образом, после нажатия «Избранное» на странице сведений кнопка в списке страница также будет обновляться.

跨页面通信实例

Это то, что мы называем фронтальной межстраничной коммуникацией.

Какие методы межстраничной коммуникации вы знаете? Если непонятно, я покажу вам семь способов общения между страницами.


1. Межстраничная связь между страницами одного происхождения

следующими способамиОнлайн-демонстрация доступна здесь >>

браузерТа же политика происхожденияНекоторые методы межстраничной коммуникации, описанные ниже, по-прежнему имеют ограничения. Поэтому давайте сначала посмотрим, какие технологии можно использовать для достижения межстраничной связи при условии соблюдения политики одного и того же происхождения.

1. BroadCast Channel

BroadCast ChannelМожет помочь нам создать канал связи для трансляции. Когда все страницы прослушивают сообщения из одного и того же канала, сообщения, отправленные одной из них, будут получены всеми остальными страницами. Его API и использование очень просты.

Следующий способ может создать идентификатор какAlienZHOUканал:

const bc = new BroadcastChannel('AlienZHOU');

Каждая страница доступна черезonmessageДля прослушивания трансляции эфира:

bc.onmessage = function (e) {
    const data = e.data;
    const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
    console.log('[BroadcastChannel] receive message:', text);
};

Чтобы отправить сообщение, просто вызовите экземплярpostMessageМетод может быть:

bc.postMessage(mydata);

Конкретное использование широковещательного канала можно найти в этой статье.«[3-минутный обзор] Передняя широковещательная связь: широковещательный канал».

2. Service Worker

Service WorkerЭто Worker, который может работать в фоновом режиме в течение длительного времени и может осуществлять двустороннюю связь со страницей. Service Worker можно совместно использовать с несколькими страницами, а эффект широковещания можно получить, используя Service Worker в качестве центра обработки сообщений (центральной станции).

Service Worker также является одной из основных технологий в PWA.Поскольку эта статья не посвящена PWA, если вы хотите узнать больше о Service Worker, вы можете прочитать мою предыдущую статью.[Обучение и практика PWA] (3) Сделайте свое веб-приложение доступным в автономном режиме.

Сначала нужно зарегистрировать Service Worker на странице:

/* 页面逻辑 */
navigator.serviceWorker.register('../util.sw.js').then(function () {
    console.log('Service Worker 注册成功');
});

в../util.sw.js— это соответствующий сценарий Service Worker. Сам Service Worker автоматически не имеет функции «широковещательной связи», нам нужно добавить некоторый код, чтобы преобразовать его в станцию ​​ретрансляции сообщений:

/* ../util.sw.js Service Worker 逻辑 */
self.addEventListener('message', function (e) {
    console.log('service worker receive message', e.data);
    e.waitUntil(
        self.clients.matchAll().then(function (clients) {
            if (!clients || clients.length === 0) {
                return;
            }
            clients.forEach(function (client) {
                client.postMessage(e.data);
            });
        })
    );
});

Слушаем в Service WorkermessageСобытия, получить информацию, отправленную страницей (называемой клиентом с точки зрения работника службы). затем пройтиself.clients.matchAll()Получите все страницы, зарегистрированные в настоящее время в Service Worker, позвонив каждому клиенту (т.е. странице)postMessageметод отправки сообщения на страницу. Это уведомляет другие страницы о сообщениях, полученных из одного места (вкладки).

После обработки Service Worker нам необходимо отслеживать сообщения, отправляемые Service Worker на странице:

/* 页面逻辑 */
navigator.serviceWorker.addEventListener('message', function (e) {
    const data = e.data;
    const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
    console.log('[Service Worker] receive message:', text);
});

Наконец, когда вам нужно синхронизировать сообщения, вы можете вызвать Service Worker’spostMessageметод:

/* 页面逻辑 */
navigator.serviceWorker.controller.postMessage(mydata);

3. LocalStorage

LocalStorage — это наиболее часто используемое локальное хранилище во внешнем интерфейсе, и все должны быть хорошо знакомы с ним; ноStorageEventЭто событие, связанное с ним, может быть незнакомо некоторым студентам.

Запускается при изменении LocalStoragestorageмероприятие. Используя эту функцию, мы можем записать сообщение в LocalStorage при отправке сообщения, а затем на каждой странице, прослушиваяstorageсобытия, о которых нужно уведомлять.

window.addEventListener('storage', function (e) {
    if (e.key === 'ctc-msg') {
        const data = JSON.parse(e.newValue);
        const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
        console.log('[Storage I] receive message:', text);
    }
});

Добавьте приведенный выше код на каждую страницу, чтобы отслеживать изменения в LocalStorage. Когда странице нужно отправить сообщение, нам нужно только использовать знакомыйsetItemМетод может быть:

mydata.st = +(new Date);
window.localStorage.setItem('ctc-msg', JSON.stringify(mydata));

Обратите внимание, здесь есть деталь: мы добавляем временную метку в mydata, которая принимает текущую миллисекунду.stАтрибуты. Это потому что,storageСобытие срабатывает только тогда, когда значение действительно изменяется. Например:

window.localStorage.setItem('test', '123');
window.localStorage.setItem('test', '123');

благодаря второму значению'123'То же значение, что и в первый раз, поэтому приведенный выше код будет работать только в первый раз.setItemвызыватьstorageмероприятие. Поэтому мы устанавливаемstчтобы убедиться, что он будет запускаться каждый раз, когда он вызываетсяstorageмероприятие.

Вздремнуть

Выше мы рассмотрели три способа реализации межстраничной связи, будь то широковещательный канал, устанавливающий широковещательный канал, или станция ретрансляции сообщений с использованием Service Worker, или какие-то хитрые способы.storageСобытия, которые все находятся в «режиме вещания»: страница уведомляет о сообщении на «центральную станцию», а «центральная станция» уведомляет каждую страницу.

В приведенном выше примере «центральная станция» может быть экземпляром BroadCast Channel, Service Worker или LocalStorage.

Ниже мы увидим два других способа обмена данными между страницами, которые я называю «Общее хранилище + режим опроса».


4. Shared Worker

Shared Workerявляется еще одним членом семьи рабочих. Обычные рабочие процессы работают независимо и не взаимодействуют друг с другом; общие рабочие процессы, зарегистрированные в нескольких вкладках, могут обмениваться данными.

Проблема с Shared Worker при реализации межстраничной связи заключается в том, что он не может активно уведомлять все страницы, поэтому мы будем использовать опрос для получения последних данных. Идея заключается в следующем:

Пусть совместный работник поддерживает два вида сообщений. Один пост, общий рабочий сохранит данные после его получения; другой получен, общий работник пройдет сохраненные данные после получения сообщенияpostMessageна страницу, на которой он зарегистрирован. То есть пусть страница активно получает (синхронизирует) последние новости через get. Конкретная реализация выглядит следующим образом:

Во-первых, мы запустим Shared Worker на странице, способ запуска очень прост:

// 构造函数的第二个参数是 Shared Worker 名称,也可以留空
const sharedWorker = new SharedWorker('../util.shared.js', 'ctc');

Затем поддержите получение и отправку сообщений в Shared Worker:

/* ../util.shared.js: Shared Worker 代码 */
let data = null;
self.addEventListener('connect', function (e) {
    const port = e.ports[0];
    port.addEventListener('message', function (event) {
        // get 指令则返回存储的消息数据
        if (event.data.get) {
            data && port.postMessage(data);
        }
        // 非 get 指令则存储该消息数据
        else {
            data = event.data;
        }
    });
    port.start();
});

После этого страница периодически отправляет сообщение команды get в Shared Worker, опрашивает последние данные сообщения и отслеживает возвращаемую информацию на странице:

// 定时轮询,发送 get 指令的消息
setInterval(function () {
    sharedWorker.port.postMessage({get: true});
}, 1000);

// 监听 get 消息的返回数据
sharedWorker.port.addEventListener('message', (e) => {
    const data = e.data;
    const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
    console.log('[Shared Worker] receive message:', text);
}, false);
sharedWorker.port.start();

Наконец, если вы хотите общаться между страницами, просто дайте Shared WorkerpostMessageТолько что:

sharedWorker.port.postMessage(mydata);

Обратите внимание, что если вы используетеaddEventListenerЧтобы добавить прослушиватель сообщений Shared Worker, вам нужно явно вызватьMessagePort.startметод, указанный вышеsharedWorker.port.start(); если вы используетеonmessageСвязывание слушателей не требуется.

5. IndexedDB

Помимо использования Shared Worker для совместного использования данных хранилища, вы также можете использовать некоторые другие «глобальные» (поддерживающие межстраничные) решения для хранения. НапримерIndexedDBили куки.

Ввиду того, что вы уже знакомы с печеньем, и как «одно из самых ранних решений для хранения в Интернете», файлы cookie предприняли больше обязанностей в практических приложениях, чем они были первоначально разработаны. Мы будем использовать IndexedDB для реализации его ниже Отказ

Идея проста: подобно схеме Shared Worker, отправитель сообщения сохраняет сообщение в IndexedDB, а получатель (например, все страницы) получает самую свежую информацию путем опроса. Перед этим давайте просто инкапсулируем несколько методов инструмента IndexedDB.

  • Открытое соединение с базой данных:
function openStore() {
    const storeName = 'ctc_aleinzhou';
    return new Promise(function (resolve, reject) {
        if (!('indexedDB' in window)) {
            return reject('don\'t support indexedDB');
        }
        const request = indexedDB.open('CTC_DB', 1);
        request.onerror = reject;
        request.onsuccess =  e => resolve(e.target.result);
        request.onupgradeneeded = function (e) {
            const db = e.srcElement.result;
            if (e.oldVersion === 0 && !db.objectStoreNames.contains(storeName)) {
                const store = db.createObjectStore(storeName, {keyPath: 'tag'});
                store.createIndex(storeName + 'Index', 'tag', {unique: false});
            }
        }
    });
}
  • Хранение данных
function saveData(db, data) {
    return new Promise(function (resolve, reject) {
        const STORE_NAME = 'ctc_aleinzhou';
        const tx = db.transaction(STORE_NAME, 'readwrite');
        const store = tx.objectStore(STORE_NAME);
        const request = store.put({tag: 'ctc_data', data});
        request.onsuccess = () => resolve(db);
        request.onerror = reject;
    });
}
  • запрос/чтение данных
function query(db) {
    const STORE_NAME = 'ctc_aleinzhou';
    return new Promise(function (resolve, reject) {
        try {
            const tx = db.transaction(STORE_NAME, 'readonly');
            const store = tx.objectStore(STORE_NAME);
            const dbRequest = store.get('ctc_data');
            dbRequest.onsuccess = e => resolve(e.target.result);
            dbRequest.onerror = reject;
        }
        catch (err) {
            reject(err);
        }
    });
}

В остальном работа очень проста. Сначала откройте подключение к данным и инициализируйте данные:

openStore().then(db => saveData(db, null))

Для чтения сообщения вы можете опросить после подключения и инициализации:

openStore().then(db => saveData(db, null)).then(function (db) {
    setInterval(function () {
        query(db).then(function (res) {
            if (!res || !res.data) {
                return;
            }
            const data = res.data;
            const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
            console.log('[Storage I] receive message:', text);
        });
    }, 1000);
});

Наконец, когда вы хотите отправить сообщение, просто сохраните данные в IndexedDB:

openStore().then(db => saveData(db, null)).then(function (db) {
    // …… 省略上面的轮询代码
    // 触发 saveData 的方法可以放在用户操作的事件监听内
    saveData(db, mydata);
});

Вздремнуть

Помимо «режима вещания» мы также узнали о режиме «общее хранилище + долгий опрос». Вам может показаться, что длительный опрос не так элегантен, как режим прослушивания, но на самом деле иногда при использовании формы «общего хранилища» использовать длительный опрос не обязательно.

Например, в сценарии с несколькими вкладками мы можем оставить вкладку A для работы на другой вкладке B; когда мы переключаемся с вкладки B обратно на вкладку A через некоторое время, мы надеемся синхронизировать информацию о предыдущей операции на вкладке B обратно. . В настоящее время он используется только для мониторинга на вкладке A.visibilitychangeДля такого события достаточно сделать синхронизацию информации.

Далее я представлю еще один метод общения, который я называю моделью «сарафанного радио».


6. window.open + window.opener

когда мы используемwindow.openКогда страница открыта, метод возвращает открытую страницуwindowцитаты. в то время как указанныйnoopener, на открытую страницу можно попасть черезwindow.openerПолучить ссылку на страницу, на которой она была открыта — таким образом мы соединяем страницы (древовидная структура).

Во-первых, мы положилиwindow.openоткрытой страницыwindowОбъекты собираются:

let childWins = [];
document.getElementById('btn').addEventListener('click', function () {
    const win = window.open('./some/sample');
    childWins.push(win);
});

Затем, когда нам нужно отправить сообщение, как инициатору сообщения, страница должна уведомить как страницу, которую она открывает, так и страницу, которая ее открывает:

// 过滤掉已经关闭的窗口
childWins = childWins.filter(w => !w.closed);
if (childWins.length > 0) {
    mydata.fromOpenner = false;
    childWins.forEach(w => w.postMessage(mydata));
}
if (window.opener && !window.opener.closed) {
    mydata.fromOpenner = true;
    window.opener.postMessage(mydata);
}

Обратите внимание, что я использую.closedСвойство отфильтровывает окна вкладок, которые были закрыты. Таким образом, задача быть отправителем сообщения выполнена. Давайте посмотрим, что он должен делать в качестве получателя сообщений.

На данный момент страница, которая получает сообщение, не может быть такой эгоистичной.Помимо отображения полученного сообщения, она также должна передать сообщение «людям, которых она знает» (открытие и открытие ею):

Следует отметить, что, оценивая источник сообщения, я избегаю отправки сообщения обратно отправителю и предотвращаю передачу сообщения в бесконечном цикле между ними. (В этой схеме будут и другие мелкие проблемы, которые можно дополнительно оптимизировать на практике)

window.addEventListener('message', function (e) {
    const data = e.data;
    const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
    console.log('[Cross-document Messaging] receive message:', text);
    // 避免消息回传
    if (window.opener && !window.opener.closed && data.fromOpenner) {
        window.opener.postMessage(data);
    }
    // 过滤掉已经关闭的窗口
    childWins = childWins.filter(w => !w.closed);
    // 避免消息回传
    if (childWins && !data.fromOpenner) {
        childWins.forEach(w => w.postMessage(data));
    }
});

Таким образом, каждый узел (страница) несет ответственность за доставку сообщений, что я называю «сарафанным радио», и сообщения циркулируют в этой древовидной структуре.

Вздремнуть

Очевидно, есть проблема с моделью «сарафанное радио»: если страница неwindow.openОткрытая (например, введенная непосредственно в адресную строку или ссылка с другого веб-сайта), ссылка не работает.

В дополнение к вышеупомянутым шести распространенным методам на самом деле существует (седьмой) метод синхронизации с помощью технологии «проталкивания сервера», такой как WebSocket. Это похоже на перемещение нашей «центральной станции» из переднего конца в задний.

О WebSocket и других технологиях "проталкивания сервера" студенты, которые не понимают, могут прочитать эту статью.«Принципы и примеры различных технологий «Server Push» (опрос/COMET/SSE/WebSocket)»

Кроме того, я также написалДемонстрация онлайн-демонстрации >>

Demo页面

2. Связь между страницами разного происхождения

Выше мы ввели метод семи напротив раскол, но большинство из них ограничены гомологичными стратегиями. Однако иногда у нас есть два разных домены линий продуктов, и мы надеемся, что все страницы ниже могут передавать доступность. Что я должен делать?

Для этого в качестве «моста» можно использовать невидимый для пользователя iframe. Поскольку iframe и родительскую страницу можно указать, указавoriginЧтобы игнорировать ограничение одинакового происхождения, чтобы вы могли встроить iFrame на страницу (например,:http://sample.com/bridge.html), и эти фреймы принадлежат одной и той же исходной странице, потому что они используют URL-адрес, а их методы связи могут повторно использовать различные методы, упомянутые в первой части выше.

Связь между страницей и iframe очень проста.Во-первых, вам нужно отслеживать сообщение, отправленное iframe на странице, и выполнять соответствующую бизнес-обработку:

/* 业务页面代码 */
window.addEventListener('message', function (e) {
    // …… do something
});

Затем, когда страница хочет связаться с другими страницами того же или другого происхождения, она сначала отправит сообщение в iframe:

/* 业务页面代码 */
window.frames[0].window.postMessage(mydata, '*');

что для простоты будетpostMessageВторой параметр установлен в'*', вы также можете установить URL-адрес iframe. После того, как iframe получает сообщение, он использует какой-то метод обмена сообщениями между страницами для синхронизации сообщения во всех iframe, например широковещательный канал, используемый ниже:

/* iframe 内代码 */
const bc = new BroadcastChannel('AlienZHOU');
// 收到来自页面的消息后,在 iframe 间进行广播
window.addEventListener('message', function (e) {
    bc.postMessage(e.data);
});    

Когда другие фреймы получают уведомление, они синхронизируют сообщение со страницей, которой они принадлежат:

/* iframe 内代码 */
// 对于收到的(iframe)广播消息,通知给所属的业务页面
bc.onmessage = function (e) {
    window.parent.postMessage(e.data, '*');
};

На следующем рисунке показан шаблон связи между негомологичными страницами с использованием iframe в качестве «моста».

Среди них «гомологическая схема междоменной связи» может использовать одну из технологий, упомянутых в первой части статьи.


Суммировать

Сегодня я поделюсь с вами различными способами межстраничной коммуникации.

Для страниц того же происхождения распространены следующие способы:

  • Широковещательный режим: широковещательный канал / сервисный работник / LocalStorage + StorageEvent
  • Режим общего хранилища: Shared Worker/IndexedDB/cookie
  • Режим сарафанного радио: window.open + window.opener
  • Серверные: Websocket/Comet/SSE и т. д.

Для страниц разного происхождения вы можете преобразовать сообщения страницы другого происхождения в сообщения страницы того же происхождения, внедрив iframe того же происхождения в качестве «моста».

Услуги для студентов приветствуют вниманиеМой блог >> https://github.com/alienzhou/blog

В то же время, когда вы делитесь этой статьей, она также предназначена для метания и цитирования нефрита. Если у вас есть другие идеи, приглашаем обсудить их вместе и выдвинуть свои мнения и идеи~