Предыстория


Небольшое вступление для понимания “зачем мне это надо”. Так получилось, что организация, в которой я работаю, выпускает несколько продуктов, результатом работы одного из них является HTML документ. Продукты десктопные, и HTML документ приходится открывать в WEB-браузере с локального диска. Всё бы ничего, если бы не ограничения браузеров, которые работают на “движке” Chromium. В моём случае в “хроме” нельзя из одного iframe изменить src другого iframe. Вернее это ограничение можно обойти, если “хром” запустить с ключом: chrome.exe – allow-file-access-from-files. Но, к сожалению, это срабатывает только в том случае, если ни одной копии “хрома” не загружено. Всё это накладывает ограничения или, вернее, неудобства при работе с документами.

Решаем проблему


Позаимствуем понятие instance (экземпляр) у ООП, для простоты описания. Здесь инстанс будет означать окно с документом, либо это окно основного документа, либо окно документа внедренного при помощи iframe.

Далее опишу как это работает.

Имеем 3 инстанса: index.html — основной, стартовый, center.html и bottom.html – внедрённые при помощи iframe.

На самом деле наш документ значительно сложнее, для понимания, привожу пример в конце статьи.


Задача – динамически управлять загрузкой контента в bottomFrame из centerFrame, и в centerFrame из indexLeftPanel. Поскольку два внедренных инстанса напрямую друг с другом ничего сделать не могут, то напишем «диспетчер сообщений» в основном инстансе index.html. Т.е. главный инстанс будет при загрузке «регистрироваться» (на рисунке стрелка № 1) во внедрённом и у них появится возможность обмена сообщениями. Таким образом у внедрённых документов, появляется возможность, управлять другими документами (стрелки №2, 3).


Для начала подгрузим center.html в centerFrame, для этого нажимаем “Change HTML in center frame”.

Тут всё штатно, смена centerFrame.src происходит обычным образом из mainlayout.js, загруженного в index.html, поскольку это происходит в одном инстансе — index.html:

listeners: {
    click: function () {
        var mainFrame = document.getElementById("centerFrame");
        mainFrame.src = 'layout/center.html';
    }
}

Для обмена сообщениями между окнами из разных инстансов необходимо проделать ряд телодвижений. Готовим index.html:

<script type="text/javascript">
    if (window.addEventListener) {
        window.addEventListener("message", listener, false);
    } else {
        window.attachEvent("onmessage", listener);
    }

    function listener(event) {
        var sO = event.data;
        if (sO) {
            if (sO.action == acNavigate) {
                var iframe = document.getElementById(sO.frame);
                if (iframe)
                    iframe.src = sO.source;
            }
        }
    }
</script>

В функцию function listener(event) будут приходить сообщения из center.html.

Готовим center.html:

<iframe id="centerFrame" src="" width="100%" height="100%" frameborder="0" onload="loadPage_centerFrame()"></iframe>
function loadPage_centerFrame() {
    var centerFrame = document.getElementById("centerFrame");
    if (centerFrame) {
        var sO = sendObject;
        sO.action = acInit;
        centerFrame.contentWindow.postMessage(sO, '*');
    }
}

Этот код должен выполниться в инстансе index.html.
Функция loadPage_centerFrame() выполнится позже назначения обработчика события в инстнсе center.html:

if (window.addEventListener) {
    window.addEventListener("message", listener, false);
} else {
    window.attachEvent("onmessage", listener);
}

Благодаря этому, инстанс center.html получит ссылку на окно index.html и запомнит её в переменную mainWindow:

var mainWindow = null;
function listener(event) {
    mainWindow = event.source;
}

Всё готово.

Да, в функцию loadPage_centerFrame() передаётся запись sendObject, на самом деле в прототипе эта запись не используется, в отличии от реальной доки, в которой эта запись служит для передачи служебной информации.

Теперь загрузим bottom.html в bottomFrame кликнув на ссылку “Change HTML in bottom frame” из center.html. При нажатии на ссылку вызывается функция из crossdocmess.js:

function linkclick(frame, link) {
    if (mainWindow) {
        var sO = sendObject;
        sO.action = acNavigate;
        sO.frame = frame;
        sO.source = link;
        mainWindow.postMessage(sO, "*");
    }
}

При помощи полей записи sendObject можно реализовать различный функционал. В данном примере реализована навигация, т.е. передаём какому внедрённому iframe какой документ назначить.

Рабочий прототип


Пример реального документа:

Комментарии (12)


  1. serf
    17.10.2017 08:45

    Это не только Chromium ограничение, но всех современных бразуеров тк это ограничение безопасности которое очень логично. В cross-window/cross-origin взаимодействии через window.postMessage ничего нового нет. Касательно использование postMessage(sO, "*") возможно стоит подумать об ограничении получателя, то есть вместо "*" указывать что-то конкретное из доверенного списка.


    1. Ujuf66 Автор
      17.10.2017 11:01

      Вы почти правы, но вот этот код

      function linkclick(frame, link) {
            top.bottomFrame.src = 'layout/bottom.html';
      }
      работает в современном FireFox-е.


  1. nckma
    17.10.2017 09:11

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


  1. justboris
    17.10.2017 11:16
    +2

    Звонили из 2000го, передавали привет и просили больше не использовать айфреймы для обновления части страницы без перезагрузки.


    1. kspksp
      17.10.2017 11:27
      -1

      Вроде бы речь идет о документе не на сервере, а на локальном диске. XMLHTTP запросы тут не работают…


      1. justboris
        17.10.2017 11:29

        Можно загружать данные через скрипты, как require.js или jsonp делают, например. Такой подход с локальными файлами работает.


    1. Ujuf66 Автор
      17.10.2017 11:36
      -1

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


      1. justboris
        17.10.2017 11:38

        Require.js, Webpack — решений много.


        У вас, как я вижу, используется ExtJS. Там тоже есть решения для ленивой загрузки.


  1. Akuma
    17.10.2017 11:38
    +1

    Может стоит попробовать реализовать все это в виде десктопного приложения на каком-нибудь Electron (https://electron.atom.io/) например? Надо уходить от iframe, он разве что для встраиваемых виджетов подходит, которые как раз не должны взаимодействовать с остальной страницей.

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

    UPD: Если у вас это все генерится уже каким-то приложением, может проще будет поднять внутри него мини веб-сервер и с него все гонять? Ну как будто это сайт. Там уже можно полноценное веб-приложение сделать, а не извращаться.


    1. Ujuf66 Автор
      17.10.2017 11:53

      На счёт electron.atom.io вы это хорошо подметили, но у нас весь «двигатель» работает на паре виндовых DLL, одна из них привязана к дельфовым классам, так просто от этого не уйти.

      поднять внутри него мини веб-сервер и с него все гонять
      отличная мысль.
      а не извращаться
      я не считаю, использование postMessage — извращением


      1. Akuma
        17.10.2017 12:14
        +2

        postMessage — не извращение :) iframe — извращение :)

        Да, dll к электрону наверное не получится привязать.


        1. dnbard
          17.10.2017 14:56

          Да, dll к электрону наверное не получится привязать.

          можно, например через node-ffi