2020-08-24に更新

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を参照し、当該要素にアクセスすることが可能になった。もちろんその逆も可能である。

Originally published at qiita.com
ツイッターでシェア
みんなに共有、忘れないようにメモ

ウラル

Splatoonの二次創作サイト「スプランプ」の管理人です。サーモンラン研究所やオクトチャット、フェス速報などを作りました。

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント