Создание настраиваемого веб-приложения, которое можно использовать на другой веб-странице, сопряжено с рядом проблем в области междоменной связи. Вот как я решил эти проблемы для Zwibbler.com. (См. Демонстрацию API здесь )
Zwibbler состоит из большой программы на JavaScript и небольшого HTML. Часть javascript использует методы Ajax для отправки запросов POST обратно на сервер zwibbler.com, чтобы отобразить некоторые элементы без ограничений тега CANVAS. В частности, это позволяет поддерживать вывод PDF, а также SVG и PNG.
Если вы хотите включить Zwibbler.com на другой веб-сайт, приложению zwibbler все еще необходимо связаться с zwibbler.com для выполнения этих задач. Однако браузеры не позволят этого из-за ограничений безопасности. Код Javascript может общаться только с сервером, с которого пришла основная веб-страница.
HTML позволяет встраивать одну веб-страницу в другую, в элемент <iframe>. Они остаются по существу разделенными. Веб-сайту контейнера разрешается общаться только с его веб-сервером, а iframe — только с исходным сервером. Кроме того, поскольку они имеют различное происхождение, браузер запрещает любой контакт между двумя кадрами . Это включает вызовы функций и доступ к переменным.
Но что, если вы хотите получить некоторые данные между двумя отдельными окнами? Например, документ zwibbler может быть длиной в мегабайт при преобразовании в строку. Я хочу, чтобы содержащаяся веб-страница могла получить копию этой строки, когда она захочет, чтобы она могла сохранить ее. Кроме того, он должен иметь доступ к сохраненному PDF, PNG или SVG-изображению, которое создает пользователь. HTML5 предоставляет ограниченный способ связи между различными фреймами одного и того же окна, называемый window.postMessage (). Функция postMessage принимает два параметра:
- Строка для передачи
- Источник цели, или «*», чтобы разрешить любое происхождение.
Например, чтобы передать сообщение с веб-страницы контейнера в iframe, мы используем:
iframe.contentWindow.postMessage("hello there", "http://zwibbler.com");
Получатель сообщения должен быть предварительно зарегистрирован для события HTML с именем «message». Это событие происходит через тот же механизм, что и щелчки мышью.
window.addEventListener("message", function( event ) { if ( event.data === "hello there" ) { // event.origin contains the host of the sending window. alert("Why, hello to you too, " + event.origin); } }, false );
Проблема 1: двусторонняя связь
Этот метод связи является односторонним, но для вызова метода мы должны разрешить двустороннюю связь. Мы добавляем простую оболочку, называемую Messenger, чтобы обеспечить двустороннюю связь. Каждый раз, когда вы вызываете метод в iframe, вы передаете функцию ответа, которая вызывается с результатами этого вызова метода. Мы используем JSON для маршаллинга параметров.
Объект Messenger должен также отслеживать, как направлять полученные ответы. Он присваивает каждому запросу уникальный билет и сохраняет их в таблице вместе с функцией ответа. Когда получен ответ с соответствующим билетом, вызывается соответствующая функция:
Messenger.prototype = { init: function( targetFrame, targetDomain) { // The DOM node of the target iframe. this.targetFrame = targetFrame; // The domain, including http:// of the target iframe. this.targetDomain = targetDomain; // A map from ticket number strings to functions awaiting replies. this.replies = {}; this.nextTicket = 0; var self = this; window.addEventListener("message", function(e) { self.receive(e); }, false ); }, send: function( functionName, args, replyFn ) { var ticket = "ticket_" + (this.nextTicket++); var text = JSON.stringify( { "function": functionName, "args": args, "ticket": ticket }); if ( replyFn ) { this.replies[ticket] = replyFn; } this.targetFrame.postMessage( text, this.targetDomain ); },
Функция получения сначала проверяет происхождение сообщения. Если это не то, что мы ожидали, тогда мы игнорируем сообщение. Может быть, это из другого iframe, такого как реклама или игра, которая находится на той же странице. Затем он проверяет, есть ли у него номер билета. Если это так, он декодирует аргументы и вызывает соответствующую функцию ответа.
receive: function( e ) { if ( e.origin !== this.targetDomain ) { // not for us: ignore. return; } var json; try { json = JSON.parse( e.data ); } catch(except) { alert( "Syntax error in response from " + e.origin + ": " + e.data ); return; } if ( !(json["ticket"] in this.replies ) ) { // no reply ticket. return; } var replyFn = this.replies[json["ticket"]]; delete this.replies[json["ticket"]]; var args = []; if ( "args" in json ) { args = json["args"]; } replyFn.apply( undefined, args ); },
Проблема 2: отложенная загрузка
Есть еще одна сложность. Когда мы загружаем iframe, требуется некоторое время для инициализации, прежде чем он будет готов к приему событий. Если вы отправите сообщение до его регистрации, я не уверен, что произойдет, но оно не сработало, когда я его попробовал.
Поэтому мы должны добавить немного логики в приведенный выше код. Когда iframe завершает инициализацию, он отправляет сообщение, состоящее из текста «ready», в свое родительское окно. Если Messenger попросили отправить сообщение, и он еще не получил сообщение «ready», то вместо отправки он добавляет его в очередь на потом. Когда он, наконец, получает сообщение о готовности, он проходит по очереди и, наконец, отправляет все ожидающие сообщения в iframe.
Полный код содержится в component.js