会社で「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=waiting
なdiv
要素を削除して<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_にメンションするかコメントいただけると嬉しいです。