Как определить, когда iframe уже загружен

Кажется, что $('#someIframe').load(function(){...}) не будет запускаться, если он будет прикреплен после завершения загрузки iframe. Это верно?

Мне бы очень хотелось иметь функцию, которая всегда вызывается один раз, когда или после загрузки iframe. Чтобы сделать это более ясным, вот два случая:

  • Iframe еще не загружен: запустите функцию обратного вызова после ее загрузки.
  • Iframe уже загружен: немедленно запустите обратный вызов.

Как я могу это сделать?

Ответ 1

Я ударился головой о стену, пока не узнал, что происходит здесь.

Фоновая информация

  • Использование .load() невозможно, если iframe уже загружен (событие никогда не срабатывает)
  • Использование .ready() в элементе iframe не поддерживается (ссылка) и вызовет обратный вызов сразу, даже если iframe не является загружен еще
  • Использование postMessage или вызов функции контейнера на load внутри iframe возможен только при наличии контроля над ним
  • Использование $(window).load() в контейнере также будет ждать загрузки других ресурсов, таких как изображения и другие фреймы. Это не решение, если вы хотите подождать только для определенного iframe
  • Проверка readyState в Chrome для события onload, запущенного alredy, не имеет смысла, поскольку Chrome инициализирует каждый iframe пустой страницей "about: blank". readyState этой страницы может быть complete, но это не readyState ожидаемой страницы (атрибут src).

Решение

Необходимо следующее:

  • Если iframe еще не загружен, мы можем наблюдать событие .load()
  • Если iframe уже загружен, нам нужно проверить readyState
  • Если readyState - complete, мы можем предположить, что iframe уже загружен. Однако из-за вышеупомянутого поведения Chrome мы также должны проверить, есть ли readyState пустой страницы
  • Если это так, нам нужно наблюдать readyState в интервале, чтобы проверить, действительно ли фактический документ (относящийся к атрибуту src) complete

Я решил это со следующей функцией. Он был (преобразован в ES5) успешно протестирован в

  • Chrome 49
  • Safari 5
  • Firefox 45
  • IE 8, 9, 10, 11
  • Edge 24
  • iOS 8.0 ( "Safari Mobile" )
  • Android 4.0 ( "Браузер" )

Функция взята из jquery.mark

/**
 * Will wait for an iframe to be ready
 * for DOM manipulation. Just listening for
 * the load event will only work if the iframe
 * is not already loaded. If so, it is necessary
 * to observe the readyState. The issue here is
 * that Chrome will initialize iframes with
 * "about:blank" and set its readyState to complete.
 * So it is furthermore necessary to check if it's
 * the readyState of the target document property.
 * Errors that may occur when trying to access the iframe
 * (Same-Origin-Policy) will be catched and the error
 * function will be called.
 * @param {jquery} $i - The jQuery iframe element
 * @param {function} successFn - The callback on success. Will 
 * receive the jQuery contents of the iframe as a parameter
 * @param {function} errorFn - The callback on error
 */
var onIframeReady = function($i, successFn, errorFn) {
    try {
        const iCon = $i.first()[0].contentWindow,
            bl = "about:blank",
            compl = "complete";
        const callCallback = () => {
            try {
                const $con = $i.contents();
                if($con.length === 0) { // https://git.io/vV8yU
                    throw new Error("iframe inaccessible");
                }
                successFn($con);
            } catch(e) { // accessing contents failed
                errorFn();
            }
        };
        const observeOnload = () => {
            $i.on("load.jqueryMark", () => {
                try {
                    const src = $i.attr("src").trim(),
                        href = iCon.location.href;
                    if(href !== bl || src === bl || src === "") {
                        $i.off("load.jqueryMark");
                        callCallback();
                    }
                } catch(e) {
                    errorFn();
                }
            });
        };
        if(iCon.document.readyState === compl) {
            const src = $i.attr("src").trim(),
                href = iCon.location.href;
            if(href === bl && src !== bl && src !== "") {
                observeOnload();
            } else {
                callCallback();
            }
        } else {
            observeOnload();
        }
    } catch(e) { // accessing contentWindow failed
        errorFn();
    }
};

Рабочий пример

Состоит из двух файлов (index.html и iframe.html): index.html

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Parent</title>
</head>
<body>
    <script src="https://code.jquery.com/jquery-1.12.2.min.js"></script>
    <script>
        $(function() {

            /**
             * Will wait for an iframe to be ready
             * for DOM manipulation. Just listening for
             * the load event will only work if the iframe
             * is not already loaded. If so, it is necessary
             * to observe the readyState. The issue here is
             * that Chrome will initialize iframes with
             * "about:blank" and set its readyState to complete.
             * So it is furthermore necessary to check if it's
             * the readyState of the target document property.
             * Errors that may occur when trying to access the iframe
             * (Same-Origin-Policy) will be catched and the error
             * function will be called.
             * @param {jquery} $i - The jQuery iframe element
             * @param {function} successFn - The callback on success. Will 
             * receive the jQuery contents of the iframe as a parameter
             * @param {function} errorFn - The callback on error
             */
            var onIframeReady = function($i, successFn, errorFn) {
                try {
                    const iCon = $i.first()[0].contentWindow,
                        bl = "about:blank",
                        compl = "complete";
                    const callCallback = () => {
                        try {
                            const $con = $i.contents();
                            if($con.length === 0) { // https://git.io/vV8yU
                                throw new Error("iframe inaccessible");
                            }
                            successFn($con);
                        } catch(e) { // accessing contents failed
                            errorFn();
                        }
                    };
                    const observeOnload = () => {
                        $i.on("load.jqueryMark", () => {
                            try {
                                const src = $i.attr("src").trim(),
                                    href = iCon.location.href;
                                if(href !== bl || src === bl || src === "") {
                                    $i.off("load.jqueryMark");
                                    callCallback();
                                }
                            } catch(e) {
                                errorFn();
                            }
                        });
                    };
                    if(iCon.document.readyState === compl) {
                        const src = $i.attr("src").trim(),
                            href = iCon.location.href;
                        if(href === bl && src !== bl && src !== "") {
                            observeOnload();
                        } else {
                            callCallback();
                        }
                    } else {
                        observeOnload();
                    }
                } catch(e) { // accessing contentWindow failed
                    errorFn();
                }
            };

            var $iframe = $("iframe");
            onIframeReady($iframe, function($contents) {
                console.log("Ready to got");
                console.log($contents.find("*"));
            }, function() {
                console.log("Can not access iframe");
            });
        });
    </script>
    <iframe src="iframe.html"></iframe>
</body>
</html>

iframe.html

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Child</title>
</head>
<body>
    <p>Lorem ipsum</p>
</body>
</html>

Вы также можете изменить атрибут src внутри index.html, например. " http://example.com/". Просто поиграйте с ним.

Ответ 2

Я бы использовал postMessage. Iframe может назначить свое собственное событие onload и отправить родительскому элементу. Если возникают проблемы с синхронизацией, просто убедитесь, что вы назначили родительский обработчик postMessage перед созданием iframe.

Для этого iframe должен знать URL-адрес родителя, например, передав параметр GET в iframe.

Ответ 3

Я очень старался прийти к решению, которое постоянно работало над браузером. ВАЖНО: я не смог прийти к такому решению. Но вот насколько я получил:

// runs a function after an iframe node content has loaded
// note, this almost certainly won't work for frames loaded from a different domain
// secondary note - this doesn't seem to work for chrome : (
// another note - doesn't seem to work for nodes created dynamically for some reason
function onReady(iframeNode, f) {
    var windowDocument = iframeNode[0].contentWindow.document;
    var iframeDocument = windowDocument?windowDocument : iframeNode[0].contentWindow.document;

    if(iframeDocument.readyState === 'complete') {
        f();
    } else {
        iframeNode.load(function() {
            var i = setInterval(function() {
                if(iframeDocument.readyState === 'complete') {
                    f();
                    clearInterval(i);
                }
            }, 10);
        });
    }
}

и я использовал его вот так:

onReady($("#theIframe"), function() {
    try {
        var context = modal[0].contentWindow;
        var i = setInterval(function() {
            if(context.Utils !== undefined && context.$) { // this mess is to attempt to get it to work in firefox
                context.$(function() {
                    var modalHeight = context.someInnerJavascript();

                    clearInterval(i);
                });
            }
        }, 10);
    } catch(e) { // ignore
        console.log(e);
    }
});

Обратите внимание, что даже это не решает проблему для меня. Вот некоторые проблемы с этим решением:

  • В onReady для iframes, которые были добавлены динамически, iframeDocument.readyState, похоже, застрял в "неинициализированном" и, таким образом, обратный вызов никогда не срабатывает
  • По какой-то причине вся настройка по-прежнему не работает в firefox. Похоже, что функция setInterval очищается извне.
  • Обратите внимание, что некоторые из этих проблем возникают только тогда, когда на странице загружается много других вещей, что делает выбор времени для этих вещей менее детерминированным.

Итак, если кто-то может улучшить это, он будет очень признателен.

Ответ 4

У меня была такая же проблема, в моем случае я просто проверил, запущена функция загрузки или нет.

var loadingStatus = true;
iframe.onload = function () {
    loadingStatus = false;
    //do whatever you want [in my case I wants to trigger postMessage]
};
if (loadingStatus)
    //do whatever you want [in my case I wants to trigger postMessage]

Ответ 5

Только при загрузке содержимого внутри iframe innerDoc является истинным и запускает код внутри if.

    window.onload = function(){
 function manipulateIframe(iframeId, callback) {
     var iframe = document.getElementById(iframeId).contentWindow.document;
         callback(iframe);
 };
 manipulateIframe('IFwinEdit_forms_dr4r3_forms_1371601293572', function (iframe) {
     console.log(iframe.body);
 });};

пример

Ответ 6

Я думаю, вам следует попробовать использовать событие onreadystatechange.

http://jsfiddle.net/fk8fc/3/

$(function () {
    var innerDoc = ($("#if")[0].contentDocument) ? $("#if")[0].contentDocument :   $("#if")[0].contentWindow.document;
    console.debug(innerDoc);
    $("#if").load( function () { 
        alert("load");
        alert(innerDoc.readyState) 
    });
    innerDoc.onreadystatechange = function () {
        alert(innerDoc.readyState) 
    };

    setTimeout(innerDoc.onreadystatechange, 5000);
});

РЕДАКТИРОВАТЬ: контекст - это не то, что я думаю. вы можете просто проверить ReadyState документа iframe и все должно быть хорошо.

OP: Это упакованная функция, которую я сделал из понятий, описанных выше:

// runs a function after an iframe node content has loaded
// note, this almost certainly won't work for frames loaded from a different domain
onReady: function(iframeNode, f) {
    var windowDocument = iframeNode[0].contentWindow.document;
    var iframeDocument = windowDocument?windowDocument : iframeNode[0].contentWindow.document
    if(iframeDocument.readyState === 'complete') {
        f();
    } else {
        iframeNode.load(f);
    }
}