Edited at

Hello, WorkerDOM! WorkerDOMを使ってWeb Worker内でDOM操作

会社で「AMPページ内でこういう操作したときにこういうDOMを差し込むことってできるんですか?」って質問されて、「それworker-domがAMP使えるようになったらできそうですね」ってなったのでworker-domを触ってみました。

※この記事でやってることはamp-bindとか既存のAMPコンポーネントでも実装可能ですが、worker-domの素振りログなのでご了承ください。

今回出てくるコードはgistに置いています。


worker-domとは

GitHub - ampproject/worker-dom: The same DOM API and Frameworks you know, but in a Web Worker.

AMPプロジェクトが開発しているWorker内でDOM操作をするライブラリです。現時点(2018年12月)ではalpha版です。

WorkerっていうのはJavaScriptでマルチスレッドを行うためのAPIです。


JavaScriptはシングルスレッドで実行されます。しかし、重い処理があるとUIを司るメインスレッドを止めてしまいカクカクしたりします。そこでWorkerを用いて重い処理を別スレッドに逃してUIの処理などメインスレッドに影響を与えないようにできます。

Can I Useで見る限りほぼすべてのブラウザでWorkerを使うことができます。

https://caniuse.com/#search=worker

従来のWorkerではDOMを操作できませんでしたが、

worker-domを使えばWorker内で実行されるJavaScriptからDOMを操作することができます。

このworker-domですが、将来的にAMPでも使えるようにしたいと考えているようです。

参考: Google Developers Japan: WorkerDOM: DOM に対応した同時実行 JavaScript プログラミング


WorkerからHello, World!

例えば3秒後にHello, World!を表示するDOMを追加する場合、worker側ではこんな感じのファイルを用意しておきます。

setTimeout(() => {

const h1 = document.createElement('h1');
h1.textContent = 'Hello World!';
document.getElementById('waiting').remove();
document.body.appendChild(h1);
}, 3000);

id=waitingdiv要素を削除して<h1>Hello, World!</h1>を追加しているだけです。

メインスレッドの処理を書いたHTMLは以下の通りです。

<div src="hello.js" id="hello">

<div id="waiting">Waiting...</div>
</div>
<script type="module">
import {upgradeElement} from 'https://unpkg.com/@ampproject/worker-dom@0.2.8/dist/unminified.index.mjs';
upgradeElement(document.getElementById('hello'), 'https://unpkg.com/@ampproject/worker-dom@0.2.8/dist/unminified.worker.mjs');
</script>

次の節から一つずつ説明していきます。


worker-domの使い方

<div src="hello.js" id="hello">

srcにworker側で実行するJavaScriptのファイルを指定しています。今回はHTMLと同じ階層に配置されているhello.jsをしています。もちろん<div src="/dist/hello.js" id="hello">のようにディレクトリを辿ってファイルを指定することも可能です。

idはworker-domのAPIで指定します。

<div id="waiting">Waiting...</div>

これはworkerで実行されるhello.jsが実行されるまで表示する初回表示用のdivです。

ここで注意しなければいけないのが、先程出てきたworker実行するファイルを指定していたid="hello"divの内側に書かなければworker側のファイルからDOM操作できない点です。

<div src="hello.js" id="hello">

</div>

<!-- worker側から以下のDOMは操作できない -->
<div id="waiting">Waiting...</div>

workerを指定するDOMの外側に書かれたDOMにはworker側のファイルからはアクセスできません。

上記の例ではworker側からid=waitingにはアクセスできません。

<script type="module">

import {upgradeElement} from 'https://unpkg.com/@ampproject/worker-dom@0.2.8/dist/index.mjs';
upgradeElement(document.getElementById('hello'), 'https://unpkg.com/@ampproject/worker-dom@0.2.8/dist/unminified.worker.mjs');
</script>

最後にメインスレッドで動くJavaScriptを書きます。

今回はCDN(unpkg.com)からロードするようにしてみました。

もちろんnpmからインストールすることも可能です。

upgradeElementという関数を使います。第一引数にworkerで実行するファイルを指定したDOMを、第二引数にはWorkerに登録するためのworker.mjsのURLを指定します。

注意:worker.mjsではなくminifyされていないunminified.worker.mjsじゃないと現時点ではHTMLに書かれたDOMにアクセスするとこができませんでした。


nomoduleでも使えるようにする場合

今回はES Modulesが使える前提で書いていますが、色々な事情で使えない場合もあります。しかしnomoduleでもworker-domを使うことはできます。

<head>

<script src="https://unpkg.com/@ampproject/worker-dom@0.2.8/dist/index.js" nomodule defer></script>
</head>
<body>
<div src="hello.js" id="hello">
<div id="waiting">Waiting...</div>
</div>
<script type="module">
import {upgradeElement} from 'https://unpkg.com/@ampproject/worker-dom@0.2.8/dist/index.mjs';
upgradeElement(document.getElementById('hello'), 'https://unpkg.com/@ampproject/worker-dom@0.2.8/dist/unminified.worker.mjs');
</script>
<script nomodule async=false defer>
document.addEventListener('DOMContentLoaded', function() {
MainThread.upgradeElement(document.getElementById('upgrade-me'), '/dist/worker.js');
}, false);
</script>
</body>

head内でindex.jsを読み込んでおいて、nomoduleの場合に実行しておくのがいいでしょう。

その場合、upgradeElementの呼び出しが変わってMainThread.upgradeElementと書かなければいけません。


動作確認

ここまで説明した最初に紹介したコードを動かしたGIFが以下です

Worker内で動いているかはdevToolsのSourcesから確認できます。

歯車のアイコン内がWorker内で動いているファイルです。


その他Demo

@ampproject/worker-domにデモが用意されているのでcloneして実行してみてください。素数を表示するデモやpreactを使ったデモがあります。


まとめ

worker-domを使ってWorker側でDOMにアクセスしてみました。Worker側のコードは変わったことをしなくても使えるのが良い感じです。

worker-domを使ってworker内でReactも実行できたので、また別記事で書きたいと思います。

最後までお読みいただきありがとうございました。不備などありましたら、@shisama_にメンションするかコメントいただけると嬉しいです。


参考