ServiceWorker Side XXX

@mizchi

「仮想DOM革命」書いた

最近趣味でやってること

開発環境のために SW を使おうとした話

みなさん ServiceWorker 使ってますか?

Service Worker とは

合法ローカルプロキシ

// client
navigator.serviceWorker.register("/sw.js");
// sw.js
self.addEventListener('fetch', event => {
  console.log('[fetch on sw]', event.request.url)
})

service-worker いつ使える?

  • 開発者向けツール: たぶん使って OK
  • 一般向けサービス: 2020~ (Windows7 サポート終了)
  • ビジネス向けサービス: 2023~ (Windows8 サポート終了)

Service Worker: 2 つの方針

  • 透過的 PWA: 既存の振る舞いを守ったまま高速化
  • 積極的 PWA: SW でしか再現できない機能を使う

透過的 PWA

  • cache 構築
  • 投機的先読み
  • 消極的なオフライン化

投機的先読み

https://dev.to

Workbox

Workbox Strategy

  • staleWhileRevalidate
  • cacheFirst
  • networkFirst
  • networkOnly
  • cacheOnly

workbox-strategies

cacheFirst

「キャッシュが駄目だったらネットワークへ」

staleWhileRevalidate

「キャッシュを返しつつ裏でアップデートしておく」

透過的 PWA: 大事なこと

  • 表向き透過なだけでも様々なキャッシュパターンがある
  • パフォーマンス特性でトレードオフがある

透過的 PWA は今すぐ使える

  • レガシー環境では単に無視される
  • 使えるブラウザではパフォーマンス向上させる (Prgressive)

透過的 PWA の弱点

  • SW とその他リソースは 並列で初期化される
  • 初回リクエストは SW でプロキシされる保証がない

どう対策するか

初期化を SW ready まで遅延する

<!-- <script src="./main.js"></script> -->
<script type=module>
(async () => {
  await navigator.serviceWorker.register("/sw.js");
  await navigator.serviceWorker.ready;
  import("./main.js");
})()
</script>

新しい SW に更新されたらリロード強制

const r = await navigator.serviceWorker.register("/sw.js");
navigator.serviceWorker.addEventListener("controllerchange", e =>
  window.reload()
);
setInterval(() => r.update(), 1000);

現実

  • あんまりベストプラクティス定まってない
  • 枯れてないのでハマりがち
  • 開発環境でなにかと壊れる
  • chrome://serviceworker-internals が友達

積極的 PWA

積極的 PWA

  • SW でないと実現できない機能をふんだんに使う
  • 単に クライアント/サーバー モデルと捉える

積極的 PWA の弱点

  • モダンブラウザのみ
  • SEO 壊滅

どう使うか?

SW Side Tranform

  • Babel / TypeScript
  • Server Side GraphQL
  • Express Emulator

すべてが Hello World になる例

self.addEventListener("fetch", event => {
  event.respondWith(new Response("console.log('hello world')"));
});

Babel で返す

import babel from '@babel/standalone'
event.respondWith((async () => {
  const res = await fetch(event.request)
  return new Response(babel.transform(await res.text()), {
    mode: "no-cors",
    headers: { "Content-Type": "text/javascript" }
  });
})());
});

思いつき

ServiceWorker で Webpack のエミュレーションすればいいのでは

気持ち

  • Webpack は ESM エミュレータである
  • でも JSX とか将来に渡ってネイティブでも動かない
  • SW で済ませば Webpack 不要になるのでは?

=> やってみた

trans-loader

  • mizchi/trans-loader
  • onfetch 時の babel / typescript コンパイル
  • バンドルせず native esm でモジュール解決
  • jspm.io による npm モジュールの解決

デモ

動いたコード

import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
ReactDOM.render(<App />, document.querySelector(".root"));

JSX / npm / 拡張子省略 / 相対パス解決

jspm.io

  • https://jspm.io
  • npm のモジュールをESMに変換して返却するCDN
  • バージョンごとに冪等なはずなので trans-loader でキャッシュ

trans-loader: 問題

  • commonjs <=> esm を可逆にするのは難しい
  • ESM ビルドを公開してるライブラリは少数
  • (https://dev.jpsm.io が頻繁に落ちてる)

諦めた部分

// trans-loader で動くコード
import Redux from 'redux'
Redux.createStore(...)
// 動かないコード
import { createStore } from 'redux'

実用可能か

  • 可逆な ESM <=> commonjs コンパイラを書く
  • みんなが rollup 使えばいける
  • 現時点ではSW運用含めて厳しい

他のアイデア

Express in ServiceWorker(Mock)

  • サーバー(Express) を SW で動かす
  • モックサーバーに使える

express-service

Server Side ServiceWorker

  • 逆に SW 用のコードをサーバーで実行する
  • なんと Cloudflare が実装している (Cloudflare Workers)

cloudflare workers

addEventListener('fetch', event => {
  event.respondWith(fetchAndApply(event.request))
})

async function fetchAndApply(request) {
  if (request.headers.get('cf-connecting-ip') === '225.0.0.1') {
    return new Response('Sorry, this page is not available.',
        { status: 403, statusText: 'Forbidden' })
  }

  return fetch(request)
}

Service Worker Side React SSR

  • html を返却する時に SSR する
  • SEO はともかく、 FMP 最適化にはなる
const html = `
<script>window.initialState = ${JSON.stringify(initialState)};<script>
<body>${ReactDOM.renderToString(<App/ >)}</body>`
event.respondWith(new Request(html, ...))

積極的 PWA: 用途

  • (将来的に枯れたら) 開発ツール
  • ある種の最適化が必要な ツール or ゲーム

まとめ

  • SW は IEが死んだ後の動的ローダーとしても使える
  • 開発者向けツールではハチャメチャな事ができる
  • 今のうちに経験値貯めときましょう

おわり