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

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

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

今回出てくるコードは[gist](https://gist.github.com/shisama/33570f8f4f638892c8b7a960ce263a82)に置いています。

# worker-domとは
[GitHub - ampproject/worker-dom: The same DOM API and Frameworks you know, but in a Web Worker.](https://github.com/ampproject/worker-dom)

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

WorkerっていうのはJavaScriptでマルチスレッドを行うためのAPIです。<br>
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 プログラミング](https://developers-jp.googleblog.com/2018/12/workerdom.html)



# WorkerからHello, World!

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

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

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

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

```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の使い方


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

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

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

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

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

```html
<div src="hello.js" id="hello">
</div>

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

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

```html
<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からインストール](https://github.com/ampproject/worker-dom#installation)することも可能です。

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

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

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

```html
<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-dom-hello.gif](https://qiita-image-store.s3.amazonaws.com/0/93166/71b5b22a-7869-28f1-4840-5a815f4a384b.gif)

Worker内で動いているかはdevToolsのSourcesから確認できます。
歯車のアイコン内がWorker内で動いているファイルです。

<img width="370" alt="スクリーンショット 2018-12-25 02.25.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/93166/939b6ae1-ac83-d2f7-3b6f-d6e4395a72dd.png">


# その他Demo

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

# まとめ
worker-domを使ってWorker側でDOMにアクセスしてみました。Worker側のコードは変わったことをしなくても使えるのが良い感じです。
worker-domを使ってworker内でReactも実行できたので、また別記事で書きたいと思います。

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

# 参考
- [WorkerDOM: Concurrency for JavaScript programming with the DOM – Accelerated Mobile Pages Project](https://amphtml.wordpress.com/2018/08/21/workerdom/)
- [Google Developers Japan: WorkerDOM: DOM に対応した同時実行 JavaScript プログラミング](https://developers-jp.googleblog.com/2018/12/workerdom.html)
- [好きなJavaScriptをAMPで実行できるようになるかも。Web Workerで実現か？ | 海外SEO情報ブログ](https://www.suzukikenichi.com/blog/js-in-amp-with-web-worker/)
- [この DOM がすごい2018: worker-dom - mizchi's blog](https://mizchi.hatenablog.com/entry/2018/10/18/155448)