開発環境のためにServiceWorkerを使う

mizchi氏(以下、mizchi):では「ServiceWorker Side XXX」ということで発表させていただきます。mizchiです。よろしくお願いします。

(会場拍手)

ちょっと自己紹介とかはする気ないんですけど、最近本を書いたので、その紹介だけさせてください。

WEB+DB PRESS Vol.106』。最近、仮想DOM芸人みたいになってるんですけど、書いたのでよろしくお願いします。

最近趣味でnedi.appというエディタを作っていて。これはブラウザで独立して動くエディタで、Gitが動くんですけど、全部IndexedDBで動いています。

いろいろブラウザで完結してGitクローンからプッシュまでいけるというもので、Patreonでちょっとお金を集めたら月2万円ぐらいになってます。

これを作っててちょっといろいろ発展があって。開発環境のためにServiceWorkerをいろいろ酷使して、いろいろできそうだけど、いろいろできなかったという話をします。

ServiceWorkerって、今はこういうステータスなんです。

Opera Miniのことはみなさんたぶんどうでもいいと思っているので、一番左のIE11ですよね。これさえ無視すればもう動きます。ほぼ動きます。

実際使ったことある人?

(会場挙手)

仕事で使ったことある人?

(会場挙手)

まぁ、意外といる感じですね。

ServiceWorkerとはなにかというと、ただのすごいローカルプロキシなんですよね。

クライアントがなぜかクライアント内でクライアントサーバモデルを取れるという。これは一番上が呼び出す例で、下がfetchを掴んでなにかログを吐くってだけの例です。

ServiceWorkerは開発者向けにはもう使ってOKだと思っています。

ただ、一般向けには2020年ぐらいにWindows 7が終わると、Windows 8……まぁWindows 10はEdgeを積んでいるので一応使えると言えるようになるので、ビジネス向けにはWindows 8も見ないといけないから2023年ぐらいなんですけど、今から作り始めるアプリはどうせリリースが2020に近い時期なので、もうやっていいと思います。

ServiceWorkerの2つの方針

ServiceWorkerにはいろいろな使い方はあるんですけど、2つ、僕が勝手に名前をつけたんですが、透過的なServiceWorkerと積極的なServiceWorkerの2つがあると思っています。

一番上のやつがよくPWAとかで使われるやつですよね。既存の振る舞いを拡張する。だから、キャッシュ構築したり、投機的先読みしたり、消極的にオフライン化するという感じです。

例えば、これは『dev.to』の記事引っ張ってきたんですが、マウスを乗せるだけでfrom ServiceWorkerとなって、裏側で引っ張ってきている。

投機的にキャッシュをためています。いろいろ先読みしたりして、裏側にキャッシュためておくみたいな。

あと、Google製のWorkboxというプラグインがあって、これがすごく便利です。

一応webpackのworkbox-webpack-plugin突っ込むだけでそれっぽくキャッシュレイヤーが挟まってくるので、すごく便利ですね。

これにWorkbox Strategyという概念があります。

Workboxではどういうキャッシュパターンがあるか、いろいろキャッシュパターンを選べるんですね。

1つがCacheFirstといって、とりあえずキャッシュ見にいってダメだったらネットワークから返すよとか。

これはおもしろいですね。StaleWhileRevalidate。

とりあえずいったんキャッシュを返しておくんだけど、裏側でネットワークのデータを返して裏でアップデートしておく。次見たら更新するというやつです。

表向き透過なだけでもいろんなキャッシュパターンがあって。全部トレードオフがあって、わからないと嵌まるので気をつけてください。

さっきも言ったように、今すぐ使えます。レガシー環境では振る舞いが透過なので単に無視される。使えるブラウザではすごい賢くなる。プログレッシブなんですね。Progressive Web AppsのProgressive。

透過的PWAの弱点

ただ、これにはちょっと弱点があると僕は思っています。ServiceWorkerって、その他リソースは並列で初期化されます。sw.jsというよくServiceWorkerで名前つけられるやつと、なにかほかのアセットは実は並列で読み込まれるので、どこからキャッシュされるか実は自明じゃないんです。

ちょっと簡単な図を作りました。

sw.jsがreadyになるタイミングとfoo.jsとbar.jsを読んだ段階で、foo.jsがServiceWorker Readyになる前にキャッシュが返ってしまうと、これServiceWorker通らずに返ってきてしまいます。

ServiceWorker、たぶん普通に使うと全部こうなります。だから、ServiceWorker使う人はだいたい3回リロードするんですけど。1回目でServiceWorker変えて、2回目でキャッシュ捨てて有効化して、3回目で保証された状態で見るみたいな、すごいだるい感じになります。

透過的PWAの弱点への対応

これは対策のしようはあります。例えば、コメントアウトされているのが本来のscript src="./main.js"だったら、開発環境だけではこのように書き換えてしまう。

navigator.serviceWorkerをregisterして、readyして、終わったらimportする。dynamic importですね。開発環境だけこうすればいいので、本番ではdeleteして上のやつを読むイメージです。

ほかにも、ServiceWorkerが更新するのも問題になるので、裏側でsetIntervalで、これはregistrationというオブジェクトが入るんですけど、registrationを毎秒更新する。

更新があったら、このcontrollerchangeが来るので、windowをreloadして無理やり入れ替える、みたいなことができなくはないです。

ただ、現実としてベストプラクティスとかはとくになくて。これらは僕が勝手にやっているだけで、ちょっと時代が早いという感じですね。よく壊れます。ポートが壊れたり、serviceWorker.readyが来なくなったりします。だから、chrome://serviceworker-internalsという開発者ツールで chromeの内部状態、内部のオブジェクトを無理やり見て破壊したりします。

積極的PWA

ここまでが透過的なんですけど、積極的PWAという概念があると思っていて。「それってServiceWorkerじゃないとそもそも動かない機能も、たくさん使ったらなんでもできるじゃん。だってただのクライアントサーバモデルのローカルプロキシでしょ?」ということですね。

この弱点はすごく明確で、モダンブラウザでしか動かないということと、ServiceWorkerが初期化されていないとなにもかも見れないので、SEOは完全に壊滅します。

ただ、使い道はあると思っていて。例えばサーバサイドで、今回webpack疲れというのが話あったじゃないですか。webpackをインストールせずにBabelとかTypeScriptをサーバサイドでコンパイルしたりとか、Express serverをそのままServiceWorkerに放り込んじゃってそこで動かすとか、できると思います。

例えばServiceWorkerインストール後に、これは全部壊れるのでやっちゃいけないコードなんですけど、fetchですべてのオブジェクトが「event.respondWith(new Response("console.log('hello world')"))」ってやると、index.htmlですらこれになるので、2回目のリロードから壊れるのですが、まぁHello Worldにできます。

あと、これはBabelでawait fetchでevent.requestを、とりあえずサーバサイドに取りにいってオリジナルソースを取ってきて、それをbabel.transformで変換して返すということができますね。

ServiceWorkerでWebpackをエミュレートする

「だったらServiceWorkerでWebpackのエミュレーションすればいいんじゃないかな?」ってちょっと思って。なんでかというと、WebpackってES Modulesのエミュレータ+αなんだけど、でも、プラスαのところがいま重点にされてしまっています。

例えば、JSXとか絶対に将来にわたってサポートされない……まぁほぼサポートされないはずなので、なんだかんだでそれに近い機能が必要と考えると、将来的にも捨てられない可能性が高いんだったら、ServiceWorkerでWebpackをエミュレートすればいいんじゃないか?  やってみました。こういうモジュールを作ったので……ええと、見ておいてください。

(会場笑)

先にデモしますが、これが動いているやつです。

ネットワークがどうなっているかというとjspm.ioというCDNがあって、そこからすべてReactのソースをその場で全部構築して。これReact Reduxが動いています。Babelの変換もこの場でやっています。

例えばこういうコードが動いています。

これはなにかというと、import React 、ReactDOM、App "./components/App"、ReactDOM.render。

これはWeb標準では動かないんですけど、これを無理やり動くようにするためには、まずはBabelでコンパイルしてnpmのモジュールをCDNの、このモジュールパスを書き換えたCDNに指し直す。拡張子がさらにApp.jsみたいに省略されてるのを復元します。これ「.js」「 .ts」「/index」とか全部探索して取れたやつを返してるんですけど。あとは、外部モジュールの相対パスの解決をしたり。

ES Modulesのimportを通る際はServiceWorker.onfetchという機構を通って変換されるので、それを変換できるというやつです。外部ライブラリのコードはバージョンごとに変わらないはずなので、それをServiceWorkerでキャッシュしてしまえばいいと。

やってみました。そして、動いた。React Reduxまで動いたんですけど、実際にはいろいろ問題はあって。

これはnpmモジュールのCommonJSからES Modulesに無理やり変換してる。通常の逆をしないといけないんですね。

webpackで、npmのモジュールはCommonJSなんですけど、CommonJSをES Modulesに再変換しないといけないという処理をやっているんですが、やっぱり手がかりが少なくて、しかも可逆じゃないので、やってみたけどできなかった。

じゃあES Modulesのソースを直接参照できればいいじゃんと思ったんだけど、そもそもES Modulesのソースをそのまま公開してくれているライブラリなんて少数で、あんまりダメ。できそうにない。あと、そもそも今回使ったCDN、jspm.ioというCDNが頻繁に落ちているという。Jspm.ioはnpmのモジュールをESMに変換してくれているというやつですね。

上のコードは動くんですけど、下のコードが動かなくて。

module.exportsをESMのexport defaultとして返してくれるんですけど、それ以外のエクスポートされたオブジェクトは取れないということになっちゃいました。

なので、可逆なコンパイラを自分で書くしかないなと思ってるんですけど。本当に使いたければ。

みんながrollupを使えばES Modulesも提供することになるので動くんじゃないですかね。

ServiceWorkerを使ったアイデア

あと、ほかにもいろいろできると思っていて。例えばExpressのServiceWorker。

これはモックサーバで使えると思っていて、一部のルーティング、まぁ、モックを作るのにほしいのはルーティング処理で。APIエンドポイントだけモックして、なにかJSON、適当なオブジェクトをモックサーバで返すとか。

あと、そもそもサーバでServiceWorker動かすみたいな。

これはなんとなぜかCloudflareが実装しているんですね。Cloudflare Workersといって、CDNでServiceWorkerを実装して、それを返す。

これは公式のコードで、ServiceWorkerのあのfetchでrespondWith(fetchAndApply)みたいな感じ。

これはなんでServiceWorkerなんだろうって、いまいちよくわからないです。

あとはServiceWorkerでServer Side Renderingする。

もうサーバなのかなんだかよくわからないですけど。

(会場笑)

まぁ、できます。ServiceWorkerでindex.htmlを構築して返す……これSEOにはなんの役にも立たないんですけど、First Meaningful Paintを最適化したければ、サーバサイドの中でReactDOM.renderToStringして、initialStateもembedして実行しちゃえば動きます。

ある程度いろいろ考えてみたんですけど、いろいろ変えたら開発ツールには使える。現状はまだ早いので、みなさんがんばってこれ開拓してください。僕はちょっと疲れました。

(会場笑)

あとはある種の開発者ツール。クライアントでBabelを動かす必要があるような開発者ツールとか、そういうときにServiceWorker Side Babelみたいものが便利かなという気がします。

まとめは、IEが死んだあとに動的ローダーとして使えるので。本当はIEのためにこれを利用したいのですが、IEじゃ動かないんですね。なので、デッドロックしてるんですけど、まぁIEは死ぬんで忘れましょう。

今のうちに経験値貯めておけば使えるように、2020年ぐらいにちょっと使えると思います。以上、お疲れ様でした。

(会場拍手)