Καλωσορίσατε, επισκέπτη!

Φαίνεται πως έρχεστε για πρώτη φορά εδώ. Αν θέλετε να συμμετέχετε, πάτησε ένα από αυτά τα πλήκτρα!

【済】ページロード中にZenzaWatchがエラーで停止することがある

About: ZenzaWatch DEV版
ytyt
επεξεργάστηκε September 25 σε Συζητήσεις κώδικα [?]

Chromiumにてページロード中にZenzaWatchが次のエラーで停止することがあります。

Uncaught (in promise) TypeError: Cannot read property 'dispatchEvent' of null
    at Object.dispatchCustomEvent (<anonymous>:2918:7)
    at initialize (<anonymous>:29561:8)
    at <anonymous>:30133:14

処理が途中で停止するため、Zenボタンの埋め込みなどが行われません。
リロードにより解決するためこれもタイミング問題のようです。(Ctrl-F5したときに高確率で発生)

Firefoxでは再現できませんでした。

デバッガで該当箇所を見るとここのutil.dispatchCustomEventに渡したdocument.bodyがnullになっています。

    const initialize = async function (){
        window.console.log('%cinitialize ZenzaWatch...', 'background: lightgreen; ');
        util.dispatchCustomEvent(
            document.body, 'BeforeZenzaWatchInitialize', window.ZenzaWatch, {bubbles: true, composed: true});
        util.addStyle(CONSTANT.COMMON_CSS, {className: 'common'});
        initializeBySite();
        replaceRedirectLinks();

どこでdocument.bodyが消えたのかをconsole.logを仕込んで確認した結果が次のとおりです。
boot.js:

    } else if (host === 'ext.nicovideo.jp' && name.startsWith(`videoInfo${PRODUCT}Loader`)) {
        GateAPI.exApi();
    } else if (window === window.top) {
        window.console.log(`boot 1 document.body: ${document.body}`);
        await AntiPrototypeJs();
        window.console.log(`boot 2 document.body: ${document.body}`);
        if (window.ZenzaLib) {
            window.ZenzaJQuery = window.ZenzaLib.$;

AntiPrototypeJs:

    Object.assign(f.style, { position: 'absolute', left: '-100vw', top: '-100vh' });
    console.log(`AntiPrototypeJs 1 document.body: ${window.document.body}`);
    return this.promise = new Promise(res => {
        console.log(`AntiPrototypeJs 2 document.body: ${window.document.body}`);
        f.onload = ()=> {
            console.log(`AntiPrototypeJs 3 document.body: ${window.document.body}`);
            res()
        };
        document.documentElement.append(f);
    }).then(() => {
I don't like prototype.js 1.5.x
AntiPrototypeJs 1 document.body: [object HTMLBodyElement]
AntiPrototypeJs 2 document.body: [object HTMLBodyElement]
@require {"lodash":"4.17.11"}
boot 1 document.body: [object HTMLBodyElement]
AntiPrototypeJs 3 document.body: null
boot 2 document.body: null

この結果からiframeのonloadのタイミングでdocument.bodyがnullになっていることがわかります。
なお、スクリプト先頭にある_template.jsAntiPrototypeJs();をコメントアウトしても以下のように結果は変わりませんでした
(が、await AntiPrototypeJs();より前にiframeのonloadが呼ばれてしまうとやはり処理が止まると思います)

@require {"lodash":"4.17.11"}
boot 1 document.body: [object HTMLBodyElement]
I don't like prototype.js 1.5.x
AntiPrototypeJs 1 document.body: [object HTMLBodyElement]
AntiPrototypeJs 2 document.body: [object HTMLBodyElement]
AntiPrototypeJs 3 document.body: null
boot 2 document.body: null

動作しないバージョン: 2.4.0, 2.4.23
動作するバージョン: 2.3.2
検証環境: Chromium 76 + Tampermonkey v4.8.41

Σχόλια

  • ytyt
    επεξεργάστηκε September 20 [?]

    (が、await AntiPrototypeJs();より前にiframeのonloadが呼ばれてしまうとやはり処理が止まると思います)。

    すみません。これは間違いでした。
    resolve済みのPromiseにthenを追加したりawaitしたりすると直ちに呼び出されますね。

    さて、document.bodyが消えるなら前の値を保存しておけば良いのではないか
    などと考えていくつか調査してみたのですが、
    どうもawaitで消える前のdocument.bodyも素性がおかしいように思います。

    例えば次のようにbodyにsetAttributeした場合、
    await後にdocument.bodyがnullになっていないケースで最終的なbodyのattributeは
    test1="1" が存在しない一方で test2="2" が存在する状態になります。

            console.log('boot 1 document.body.outerHTML:', document.body.outerHTML);
            console.log(`boot 1 document.body: ${document.body}`);
            document.body.setAttribute("test1", "1");
            await AntiPrototypeJs();
            console.log(`boot 2 document.body: ${document.body}`);
            if (document.body) document.body.setAttribute("test2", "2");
    

    document.body.outerHTMLはこうなっていました。

    boot 1 document.body.outerHTML: <body style="visibility: hidden; width: 0px; height: 0px; border: 0px; margin: 0px; background: none;"><div classname="t" style="padding-left: 1px; width: 1px;"></div></body>
    

    もしかするとこれは@run-at document-bodyの場合には、ページの読み込み中に
    ダミーのdocument.bodyを作ってスクリプトを先に実行しているのではないでしょうか。
    すると、iframeのonloadでdocument.bodyがnullになっている件は、
    「本体のページの読み込みが終わる前にiframeのonloadが実行されてしまっている」という仮説が立ちます。

    実際、@run-at document-endにして実行すると

    boot 1 document.body.outerHTML: <body id="PAGETOP" class="ja-jp">
    (略)
    

    のように本来のdocument.bodyが渡されますし、await後にdocument.bodyがnullになる症状も発生しません。

    検証環境: Chromium 76 + Tampermonkey v4.8.41

  • v2.4.24ではCtrl-F5時にmonkeyの先頭のログ(ZenzaWatch@DEV v2.4.24 (゚∀゚) ゼンザ! Nicorü?)すら出なくなりました。

    I don't like prototype.js 1.5.x
    @require {"lodash":"4.17.11"}
    (終わり)
    

    v2.4.24で行われた次の修正を元に戻すとv2.4.23の状態に戻ります。

        Object.assign(f.style, { position: 'absolute', left: '-100vw', top: '-100vh' });
        return this.promise = new Promise(res => {
            f.onload = res;
    -       document.documentElement.append(f);
    +       (document.body || document.documentElement).append(f);
        }).then(() => {
            window.PureArray = f.contentWindow.Array;
            delete window.Array.prototype.toJSON;
            delete window.String.prototype.toJSON;
    

    iframeの処理が完了する前にダミーのdocument.bodyごとiframeが削除されているのだと思います。

  • document.bodyにしろdocument.documentElementにしろ中身がちゃんと入るのはDOMContentLoaded以降ですから、
    解決策の提案としては
    1. @run-at document-end (DOMContentLoadedのタイミングで呼ばれる)
    2. iframeのonloadでdocument.bodyが存在しなければDOMContentLoadedを待つ

        Object.assign(f.style, { position: 'absolute', left: '-100vw', top: '-100vh' });
        return this.promise = new Promise(res => {
            f.onload = () => {
                if (document.body) res();
                else document.addEventListener("DOMContentLoaded", res, {once: true});
            }
            document.documentElement.append(f);
        }).then(() => {
    

    といったところです。

    2の対策を取った場合もprototype.jsがロードされていないページで
    document.bodyがダミーのまま処理に入ると良からぬことが起きそうですし
    1の @run-at document-end にしてしまう方が良いのではないかとは思います。

  • v2.4.25で再現しないことを確認しました。ありがとうございます。

Συνδεθείτε ή Εγγραφείτε για να σχολιάσετε.