2
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Organization

JavaScriptのpostMessageでDOMツリーのノード参照を渡す方法[Xpath]

ポップアップしたウィンドウに要素の参照(DOMノード)を送りたかったので、この記事を書いた。

Web MessagingはDOMノードを送れない

JavaScriptでは、window.postMessageを使うことで、ポップアップやiframeなどの別ウィンドウとWeb Messaging(HTML5)を介して通信することができる。

子ウィンドウ、親ウィンドウへの参照はそれぞれwindow.open()window.openerで持つことができるから、window.postMessageと併せてあらゆるデータのやり取りが自由にできそうなものである。

しかし、window.postMessageでは送ることができないデータがある。

MDN web docsには、messageについて

他のウィンドウに送られるデータ。データは the structured clone algorithm に従ってシリアル化されます。つまり、手動でシリアル化することなく様々なデータオブジェクトを渡すことができます。
(window.postMessage - Web API | MDN)

と書かれている。

この「the structured clone algorithm」(日本語「構造化複製アルゴリズム」)という部分が重要で、この中で「構造化複製で動作しないもの」というのが示されいる。

  • Function オブジェクトは構造化複製アルゴリズムでは複製されません。複製しようとすると DATA_CLONE_ERR 例外が送出されます。
  • DOM ノードを複製しようとしても同様に DATA_CLONE_ERR 例外が送出されます。

つまり、例えばdocument.querySelector()などを使えば要素の参照を取得できるが、このような参照はWeb Messagingで送ることができない。
実際にwindow.postMessageでDOMノードを送ろうとすると、

Uncaught DOMException: Failed to execute 'postMessage' on 'Window': HTMLButtonElement object could not be cloned.

のようなエラーが発生する。

ウィンドウ間で互いのDOMの参照はできるのだから、DOMノードも送れるべきである。
そこで、住所のように、テキストでDOMツリーにおけるノードの位置を表現できる方法を探していると、「Xpath」という言語構文を見つけた。

Xpathとは

Xpathとは、XMLやHTMLのようなツリー状の階層構造を持つ文書で、様々なノードの位置や情報を表すことができる記法のことである。URLのようなパス表記ができることが特徴。

Introduction to using XPath in JavaScript | MDN

記法についてはこちらの記事が詳しいが、ざっくり言うと、例えばbody直下の<h1>にアクセスするためのXpathは

/html/body/h1

となる。

また、2番目の<div>の3番目の<span>にアクセスするためのXpathは

/html/body/div[2]/span[3]

といったように表すことができる。

要素のXpathを取得して送信する

まず、送るためにはDOMノードのXpathを取得する必要がある。
以下の記事のコードを使用して送信側のスクリプトを書いた。

ブラウザ上のクリックした要素のXpathを取得する - Qiita

parent.htmlのjs

let childWindow = window.open('child.html', 'child', 'width=300,height=400,scrollbars');

// クリックされたらその要素のXpathを子ウィンドウにpostMessageする
window.addEventListener('click', (e) => {
    childWindow.postMessage(getXpath(e.target), 'http://localhost');
});

/* https://qiita.com/narikei/items/fb62b543ca386fcee211 */
function getXpath(element) {
    if(element && element.parentNode) {
        var xpath = getXpath(element.parentNode) + '/' + element.tagName;
        var s = [];

        for(var i = 0; i < element.parentNode.childNodes.length; i++) {
            var e = element.parentNode.childNodes[i];
            if(e.tagName == element.tagName) {
                s.push(e);
            }
        }

        if(1 < s.length) {
            for(var i = 0; i < s.length; i++) {
                if(s[i] === element) {
                    xpath += '[' + (i+1) + ']';
                    break;
                }
            }
        }

        return xpath.toLowerCase();
    } else {
        return '';
    }
}

要素のXpathを受け取って参照する

受信側は以下のようになる。

child.htmlのjs
let parent = window.opener.document;

function receiveMessage(e) {
    if (e.origin !== "http://localhost") {return}
    parent.evaluate(e.data, parent, null, XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue.innerHTML = 'ok!';
}

window.addEventListener("message", receiveMessage);

Introduction to using XPath in JavaScript | MDN

成功すると、親ウィンドウでクリックした要素に「ok!」と表示されるはず。
これで、親ウィンドウから子ウィンドウに送られたXpathを使って、子ウィンドウが親ウィンドウのDOMを参照し、当該要素にアクセスすることが可能になった。もちろんその逆も可能である。

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
2
Help us understand the problem. What are the problem?