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

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

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

JavaScriptでは、[window.postMessage](https://developer.mozilla.org/ja/docs/Web/API/Window/postMessage)を使うことで、ポップアップやiframeなどの別ウィンドウとWeb Messaging(HTML5)を介して通信することができる。

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

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

MDN web docsには、`message`について
> 他のウィンドウに送られるデータ。データは [the structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) に従ってシリアル化されます。つまり、手動でシリアル化することなく様々なデータオブジェクトを渡すことができます。
> [\(window.postMessage - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/Window/postMessage))

と書かれている。

この「[the structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)」（日本語「[構造化複製アルゴリズム](https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)」）という部分が重要で、この中で「[構造化複製で動作しないもの](https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#%E6%A7%8B%E9%80%A0%E5%8C%96%E8%A4%87%E8%A3%BD%E3%81%A7%E5%8B%95%E4%BD%9C%E3%81%97%E3%81%AA%E3%81%84%E3%82%82%E3%81%AE)」というのが示されいる。

> - 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](https://developer.mozilla.org/ja/docs/Introduction_to_using_XPath_in_JavaScript)

記法については[こちらの記事](https://qiita.com/rllllho/items/cb1187cec0fb17fc650a)が詳しいが、ざっくり言うと、例えばbody直下の`<h1>`にアクセスするためのXpathは

```
/html/body/h1
```

となる。

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

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

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

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

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

[ブラウザ上のクリックした要素のXpathを取得する - Qiita](https://qiita.com/narikei/items/fb62b543ca386fcee211)

```JavaScript: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を受け取って参照する

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

```JavaScript: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](https://developer.mozilla.org/ja/docs/Introduction_to_using_XPath_in_JavaScript)

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