postMessage также может играть так

внешний интерфейс
postMessage также может играть так

В повседневной работе обмен сообщениями является очень распространенным сценарием. Например, всем знакома структура B/S, в которой обмен сообщениями между браузером и сервером основан на протоколе HTTP:

Однако в дополнение к протоколу HTTP в некоторых сценариях, требующих больших данных в реальном времени, мы будем использовать протокол WebSocket для завершения обмена сообщениями:

Я полагаю, что все знакомы с этими двумя сценариями. Далее Brother A Bao представит другой сценарий обмена сообщениями, а именно, как передавать сообщения между родительской страницей и дочерней страницей, загружаемой iframe.

Прочитайте последние популярные статьи брата А Бао (спасибо за вашу поддержку и поддержку 🌹🌹🌹):

Почему ты вдруг пишешь на эту тему? На самом деле это потому, что в недавнем проекте Brother Abao необходимо реализовать обмен сообщениями между родительской страницей и дочерней страницей, загруженной iframe. Кроме того, совсем недавно брат А Бао писалАнализ исходного кодаСпециальная тема, поэтому я искал 🔍 на Github и нашел хороший проект——Postmate.

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

  • Роль рукопожатия в системе сообщений и как реализовать рукопожатие;
  • Дизайн модели сообщений и способы реализации проверки сообщений для обеспечения безопасности связи;
  • Использование postMessage и способы его использования для обеспечения обмена сообщениями между родительской и дочерней страницами;
  • Разработка и реализация API обмена сообщениями.

Хорошо, без лишних слов, давайте кратко представимPostmate.

В дополнение к Postmate, FrominXu'spostmessagejsЭто также хороший выбор.

1. Введение в Postmate

Postmateмощный, простой, основанныйPromiseБиблиотека postMessage. Он позволяет родительским страницам взаимодействовать с междоменными дочерними с минимальными затратами.iframeобщаться. Библиотека имеет следующие возможности:

  • API на основе обещаний для элегантного и простого общения;
  • использоватьпроверка сообщенияДля защиты безопасности двусторонней родительской дочерней связи;
  • Дочерние объекты предоставляют извлекаемые объекты модели, к которым могут обращаться родительские объекты;
  • Дочерние объекты могут отправлять события, которые прослушивал родительский объект;
  • Родительский объект может вызывать функции в дочернем объекте;
  • Нулевые зависимости. Предоставьте пользовательские полифилы или абстракции для Promise API, если это необходимо;
  • Легкий, размером около 1,6 КБ (минимизированный и сжатый gzip).

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

Следуйте «Дорога к бессмертному совершенствованию с полным стеком», чтобы прочитать 3 бесплатные электронные книги и 50 учебных пособий «Повторное изучение TS» от брата Абао.

2. Как пожимать руки

Когда TCP устанавливает соединение, требуется трехстороннее рукопожатие. Точно так же, когда родительская страница взаимодействует с дочерней страницей, Postmate также использует «рукопожатие», чтобы гарантировать, что обе стороны могут нормально общаться. так какPostmateВ основе общения лежатpostMessage, поэтому, прежде чем рассказывать, как пожимать руки, давайте кратко рассмотримpostMessageAPI.

2.1 Введение в postMessage

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

2.1.1 Синтаксис postMessage()
otherWindow.postMessage(message, targetOrigin, [transfer]);
  • otherWindow: ссылка на другие окна, такие как свойство contentWindow iframe, объект окна, возвращаемый при выполнении window.open и т. д.
  • message: данные для отправки в другие окна, они будутАлгоритм структурированного клонированияСериализация.
  • targetOrigin: укажите, какие окна могут получать события сообщения через свойство происхождения окна, и его значение может быть строкой «*» (представляющей неограниченное количество) или URI.
  • передача (необязательно): это строка, передаваемая одновременно с сообщением.Transferableобъект. Право собственности на эти объекты будет передано получателю сообщения, а отправитель больше не сохранит право собственности.

Отправитель отправляет сообщения через API postMessage, а получатель может прослушиватьmessageсобытие для добавления функции обратного вызова обработки сообщений, конкретное использование выглядит следующим образом:

window.addEventListener("message", receiveMessage, false);

function receiveMessage(event) {
  let origin = event.origin || event.originalEvent.origin; 
  if (origin !== "http://semlinker.com") return;
}

2.2 Реализация рукопожатия Postmate

В телекоммуникационных и микропроцессорных системах термин Рукопожатие (также известный как рукопожатие) имеет следующие значения:

  • При передаче данных последовательность событий, управляемая аппаратным или программным обеспечением, требует взаимного согласования состояния рабочего режима, прежде чем можно будет обмениваться информацией.
  • Процесс установления параметров связи между принимающей и передающей станцией.

Для систем связи рукопожатие происходит после установления канала связи и до начала передачи информации.Квитирование используется для достижения таких параметров, как скорость передачи информации, алфавит, четность, процедуры прерывания и другие функции протокола..

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

В Postmate сообщение рукопожатия инициируется родительской страницей.Чтобы инициировать сообщение рукопожатия на родительской странице, вам сначала нужно создатьPostmateОбъект:

const postmate = new Postmate({
  container: document.getElementById('some-div'), // iframe的容器
  url: 'http://child.com/page.html', // 包含postmate.js的iframe子页面地址
  name: 'my-iframe-name' // 用于设置iframe元素的name属性
});

В приведенном выше коде мы создаем объект postmate, вызывая конструктор Postmate Внутри конструктора Postmate есть два основных шага: установка внутренних свойств объекта Postmate и отправка сообщения рукопожатия:

Код, соответствующий приведенной выше блок-схеме, относительно прост. Брат Абао не будет публиковать здесь подробный код. Заинтересованные друзья могут прочитатьsrc/postmate.jsсоответствующее содержимое в файле. Чтобы иметь возможность отвечать на информацию о рукопожатии родительской страницы, нам нужно создать объект модели на дочерней странице:

const model = new Postmate.Model({
  // Expose your model to the Parent. Property values may be functions, promises, or regular values
  height: () => document.height || document.body.offsetHeight
});

где конструктор Postmate.Model определяется следующим образом:

// src/postmate.js
Postmate.Model = class Model {
  constructor(model) {
    this.child = window;
    this.model = model;
    this.parent = this.child.parent;
    return this.sendHandshakeReply();
  }
}

В конструкторе модели мы можем ясно увидеть вызовsendHandshakeReplyЭтот метод, здесь мы смотрим только на основной код:

Теперь давайте суммируем процесс рукопожатия между родительской и дочерней страницами: когда дочерняя страница загружается, родительская страница проходитpostMessageAPI отправляет на подстраницуhandshakeсообщение о рукопожатии. получено на подстраницеhandshakeПосле сообщения о рукопожатии будет использоваться то же самое.postMessageОтвет API на родительскую страницуhandshake-replyИнформация.

Кроме того, следует отметить, что для того, чтобы подстраница могла получатьhandshakeсообщение о рукопожатии, вsendHandshakeВнутри метода запускается таймер для выполнения операции отправки:

// src/postmate.js
class Postmate {
  sendHandshake(url) {
    return new Postmate.Promise((resolve, reject) => {
      const loaded = () => {
        doSend();
        responseInterval = setInterval(doSend, 500);
      };

      if (this.frame.attachEvent) {
        this.frame.attachEvent("onload", loaded);
      } else {
        this.frame.addEventListener("load", loaded);
      }
      
      this.frame.src = url;
    });
  }
}

Конечно, чтобы избежать отправки слишком большого количества недействительной информации о рукопожатии, вdoSendВнутри метода ограничено максимальное количество рукопожатий:

const doSend = () => {
  attempt++;
  this.child.postMessage(
    {
      postmate: "handshake",
      type: messageType,
      model: this.model,
    },
    childOrigin
  );
  // const maxHandshakeRequests = 5;
  if (attempt === maxHandshakeRequests) {
     clearInterval(responseInterval);
  }
};

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

3. Как реализовать двустороннюю передачу сообщений

вызовPostmateиPostmate.ModelПосле конструктора возвращается объект Promise. И когда состояние объекта Promise изменяется сpendingстатьresolvedПосле этого вернетсяParentAPIиChildAPIОбъект:

Postmate

// src/postmate.js
class Postmate {
  constructor({
    container = typeof container !== "undefined" ? container : document.body,
    model, url, name, classListArray = [],
  }) {
    // 省略设置 Postmate 对象的内部属性
    return this.sendHandshake(url);
  }
  
  sendHandshake(url) {
    // 省略部分代码
    return new Postmate.Promise((resolve, reject) => {
      const reply = (e) => {
        if (!sanitize(e, childOrigin)) return false;
        if (e.data.postmate === "handshake-reply") {
          return resolve(new ParentAPI(this));
        }
        return reject("Failed handshake");
      };
    });
  }
}

ParentAPI

class ParentAPI{
  +get(property: any) // 获取子页面中Model对象上的property属性上的值
  +call(property: any, data: any) // 调用子页面中Model对象上的方法
  +on(eventName: any, callback: any) // 监听子页面派发的事件
  +destroy() // 移除事件监听并删除iframe
}

Postmate.Model

// src/postmate.js
Postmate.Model = class Model {
  constructor(model) {
    this.child = window;
    this.model = model;
    this.parent = this.child.parent;
    return this.sendHandshakeReply();
  }

  sendHandshakeReply() {
    // 省略部分代码
    return new Postmate.Promise((resolve, reject) => {
      const shake = (e) => {
        if (e.data.postmate === "handshake") {
          this.child.removeEventListener("message", shake, false);
          return resolve(new ChildAPI(this));
        }
        return reject("Handshake Reply Failed");
      };
      this.child.addEventListener("message", shake, false);
    });
  }
};

ChildAPI

class ChildAPI{
  +emit(name: any, data: any)
}

3.1 Дочерняя страница -> Родительская страница

3.1.1 Подстраницы отправляют сообщения
const model = new Postmate.Model({
  // Expose your model to the Parent. Property values may be functions, promises, or regular values
  height: () => document.height || document.body.offsetHeight
});

model.then(childAPI => {
  childAPI.emit('some-event', 'Hello, World!');
});

В приведенном выше коде доступ к подстраницам можно получить черезChildAPIпредоставленный объектemitметод для отправки сообщения, метод определяется следующим образом:

export class ChildAPI {
  emit(name, data) {
    this.parent.postMessage(
      {
        postmate: "emit",
        type: messageType,
        value: {
          name,
          data,
        },
      },
      this.parentOrigin
    );
  }
}
3.1.2 Родительская страница прослушивает сообщения
const postmate = new Postmate({
  container: document.getElementById('some-div'), // iframe的容器
  url: 'http://child.com/page.html', // 包含postmate.js的iframe子页面地址
  name: 'my-iframe-name' // 用于设置iframe元素的name属性
});

postmate.then(parentAPI => {
  parentAPI.on('some-event', data => console.log(data)); // Logs "Hello, World!"
});

В приведенном выше коде родительская страница может передаватьParentAPIпредоставленный объектonметод для регистрации обработчика событий, метод определяется следующим образом:

export class ParentAPI {
  constructor(info) {
    this.parent = info.parent;
    this.frame = info.frame;
    this.child = info.child;

    this.events = {};

    this.listener = (e) => {
      if (!sanitize(e, this.childOrigin)) return false;
			// 省略部分代码
      if (e.data.postmate === "emit") {
        if (name in this.events) {
          this.events[name].forEach((callback) => {
            callback.call(this, data);
          });
        }
      }
    };

    this.parent.addEventListener("message", this.listener, false);
  }

  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }
}

3.2 Проверка сообщения

Чтобы обеспечить безопасность связи, Postmate проверит сообщение во время обработки сообщения, и соответствующая логика проверки инкапсулирована вsanitizeВ методе:

const sanitize = (message, allowedOrigin) => {
  if (typeof allowedOrigin === "string" && message.origin !== allowedOrigin)
    return false;
  if (!message.data) return false;
  if (typeof message.data === "object" && !("postmate" in message.data))
    return false;
  if (message.data.type !== messageType) return false;
  if (!messageTypes[message.data.postmate]) return false;
  return true;
};

Соответствующие правила проверки следующие:

  • Убедитесь, что источник сообщения является законным;
  • Убедитесь, что тело сообщения включено;
  • Убедитесь, что тело сообщения содержитpostmateАтрибуты;
  • Убедитесь, что тип сообщения"application/x-postmate-v1+json";
  • Проверка сообщения телаpostmateЯвляется ли соответствующий тип сообщения допустимым;

Postmate поддерживает следующие типы сообщений:

const messageTypes = {
  handshake: 1, 
  "handshake-reply": 1, 
  call: 1,
  emit: 1, 
  reply: 1, 
  request: 1,
};

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

{
   postmate: "emit", // 必填:"request" | "call" 等等
   type: messageType, // 必填:"application/x-postmate-v1+json"
   // 自定义属性
}

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

3.3 Родительская страница -> Дочерняя страница

3.3.1 Вызов методов объекта модели дочерней страницы

На странице черезParentAPIпредоставленный объектcallметод, мы можем вызвать метод для объекта модели дочерней страницы:

export class ParentAPI {
	call(property, data) {
    this.child.postMessage(
      {
        postmate: "call",
        type: messageType,
        property,
        data,
      },
      this.childOrigin
    );
  }
}

существуетChildAPIобъект, воляcallТип сообщения обрабатывается соответствующим образом, и соответствующая логика обработки выглядит следующим образом:

export class ChildAPI {
  constructor(info) {
		// 省略部分代码
    this.child.addEventListener("message", (e) => {
      if (!sanitize(e, this.parentOrigin)) return;
      const { property, uid, data } = e.data;
      
      // 响应父页面发送的call消息类型,用于调用Model对象上的对应方法
      if (e.data.postmate === "call") {
        if (
          property in this.model &&
          typeof this.model[property] === "function"
        ) {
          this.model[property](data);
        }
        return;
      }
    });
  }
}

Из приведенного выше кода видно, что сообщение о вызове может использоваться только для вызова метода объекта Model дочерней страницы и не может получить возвращаемое значение вызова метода. Однако в некоторых сценариях нам нужно получить возвращаемое значение вызова метода, давайте посмотримParentAPIкак достичь этой функции.

3.3.2 Вызов метода объекта модели дочерней страницы и получение возвращаемого значения

Чтобы получить возвращаемое значение после вызова, нам нужно вызватьParentAPIпредоставляется на объектеgetметод:

export class ParentAPI {
	get(property) {
    return new Postmate.Promise((resolve) => {
      // 从响应中获取数据并移除监听
      const uid = generateNewMessageId();
      const transact = (e) => {
        if (e.data.uid === uid && e.data.postmate === "reply") {
          this.parent.removeEventListener("message", transact, false);
          resolve(e.data.value);
        }
      };
      
      // 监听来自子页面的响应消息
      this.parent.addEventListener("message", transact, false);

      // 向子页面发送请求
      this.child.postMessage(
        {
          postmate: "request",
          type: messageType,
          property,
          uid,
        },
        this.childOrigin
      );
    });
  }
}

Отправлено на родительскую страницуrequestсообщение, в подстраницах будет проходить черезresolveValueметод, чтобы получить возвращаемый результат, а затем передатьpostMessageчтобы вернуть результат:

// src/postmate.js
export class ChildAPI {
  constructor(info) {
    this.child.addEventListener("message", (e) => {
      if (!sanitize(e, this.parentOrigin)) return;
      const { property, uid, data } = e.data;
      
      // 响应父页面发送的request消息
      resolveValue(this.model, property).then((value) =>
        e.source.postMessage(
          {
            property,
            postmate: "reply",
            type: messageType,
            uid,
            value,
          },
          e.origin
        )
      );
    });
  }
}

в приведенном выше кодеresolveValueРеализация метода также очень проста:

const resolveValue = (model, property) => {
  const unwrappedContext =
    typeof model[property] === "function" ? model[property]() : model[property];
  return Postmate.Promise.resolve(unwrappedContext);
};

3.4 Механизм расширения модели

Postmate предоставляет очень гибкий механизм расширения модели, позволяющий разработчикам расширять объекты модели подстраниц в соответствии со своими потребностями:

Соответствующий механизм расширения не сложен в реализации, конкретная реализация выглядит следующим образом:

Postmate.Model = class Model {
  constructor(model) {
    // 省略部分代码
    return this.sendHandshakeReply();
  }

  sendHandshakeReply() {
    return new Postmate.Promise((resolve, reject) => {
      const shake = (e) => {
        // 省略部分代码
        if (e.data.postmate === "handshake") {
          // 使用父页面提供的模型对象来扩展子页面已有的模型对象
          const defaults = e.data.model;
          if (defaults) {
            Object.keys(defaults).forEach((key) => {
              this.model[key] = defaults[key];
            });
          }
          return resolve(new ChildAPI(this));
        }
      };
    });
  }
};

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

4. Как отключить

Когда родительская страница и дочерняя страница завершат обмен сообщениями, нам нужно отключиться. В этот момент мы можем позвонитьParentAPIна объектеdestroyметод отключения.

// src/postmate.js
export class ParentAPI {
	destroy() {
    window.removeEventListener("message", this.listener, false);
    this.frame.parentNode.removeChild(this.frame);
  }
}

В этой статье брат Абао начинает сPostmateВозьмите эту библиотеку в качестве примера, чтобы представить, как реализовать элегантную передачу сообщений между родительской страницей и дочерней страницей iframe на основе postMessage. Если вам все еще нечего делать, вы можете прочитать статьи, посвященные общению, написанные братом Абао ранее:Как элегантно реализовать обмен сообщениями?иЧего вы не знаете о WebSocket.

Следуйте «Дорога к бессмертному совершенствованию с полным стеком», чтобы прочитать 3 бесплатные электронные книги и 50 учебных пособий «Повторное изучение TS» от брата Абао.

5. Справочные ресурсы