JavaScript
React
ServiceWorker
create-react-app

create-react-appのregisterServiceWorkerで更新があったらnoticeを出す奴にトライする

create-react-app には registerServiceWorker.js というservice workerを登録する仕組みが乗っかっている。

これはoffline cacheを実現するものなのだが、cache-first strategyという手法を取っており、コードを更新してデプロイした際の取り回しがなかなか手こずるので、割といつもdisableにしがち。

だが、いつまでもこのままというのもちょっと気持ち悪いので、一度service-workerの更新に合わせて「更新してください」みたいなモーダルを出すexampleをやってみた。


demo

ソース


仕組みおさらい

create-react-appは sw-precache-webpack-pluginというpluginを組み込んでserviceWorkerを組み込んでいる。

このpluginはwebpack上でマニフェスト的な値を生成し、それとsw-precacheを組わせて裏側でfetchする仕組みをもたせている。

// service-worker.js
 :
// ここらへんの値をwebpackから吐いている。
var precacheConfig = [["/index.html","08af9741a3d97d13dec114a974062844"],["/static/css/main.c17080f1.css","302476b8b379a677f648aa1e48918ebd"],["/static/js/main.0ccb3c41.js","426e3841d0fed136b6bb86108f30e5e2"],["/static/media/logo.5d5d9eef.svg","5d5d9eefa31e5e13a6610d9fa7a283bb"]];

// :
// あとはsw-preacacheの生成コード

そして、これを registerServiceWorker.jsが登録することでキャッシュが動いている。

// registerServiceWorker.js
  :
  :
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
      if (isLocalhost) {
        // This is running on localhost. Lets check if a service worker still exists or not.
        checkValidServiceWorker(swUrl);

1. 準備

create-react-appの標準状態だと、hot reloadがあったりしてlocalでのservice workerの検証がしづらい。

なのでejectして、加えてserveでローカルサーバーで実行する。 (serveはcache-controlを0にする必要もある)

{
  "scripts": {
    "start":
      "NODE_ENV=production webpack --config config/webpack.config.prod.js --watch",
    "static": "serve -c 0 -s build -o --local"
  }
}

これでこんな感じでstart。

$ yarn start & yarn static

2. onupdatefoundからイベントを発火

ServiceWorkerが更新を検知した際、registerServiceWorkerのonupdatefound が発火するので、ここにevent発火を仕掛ける。

registration.onupdatefound = () => {
  const installingWorker = registration.installing;
    installingWorker.onstatechange = () => {
        if (navigator.serviceWorker.controller) {
          // At this point, the old content will have been purged and
          // the fresh content will have been added to the cache.
          // It's the perfect time to display a "New content is
          // available; please refresh." message in your web app.
          console.log("New content is available; please refresh.");

          // イベント発火
          const event = new Event("newContentAvailable");
          window.dispatchEvent(event);
        } else {
           :

ちなみに、nextバージョン では、config.onUpdate というコールバックが追加されており、registerServiceWorker.jsに直接手を加えなくても大丈夫になりそう

2. <ReloadModal>を作る

次に <ReloadModal>を作る。これは発火されたグローバルな newContentAvailable イベントを拾ってくるためのもの。
この例では、わかりやすさ重視でstate使ったり、ライフサイクルをそのまま使っているので、必要に応じてreduxにしたりなにかしたりすると良さそう。

// Main App
class App extends Component {
  render() {
    return (
      <div className="App">
        <ReloadModal />
            :
      </div>
    );
  }
}

class ReloadModal extends Component {
  state = {
    show: false
  };
  componentDidMount() {
    // グローバルイベントを引っ掛ける。
    window.addEventListener("newContentAvailable", () => {
      this.setState({
        show: true
      });
    });
  }
  onClick = () => {
    // リロードする
    window.location.reload(window.location.href);
  };
  render() {
    if (!this.state.show) {
      return null;
    }
    // <Modal> は単なる固定モーダルのコンポーネントなので省略。
    return (
      <Modal onClick={this.onClick}>
        <span> New Content Available!please reload </span>
      </Modal>
    );
  }
}

まとめ

とりあえずこれで最初に出したデモみたいなものは出来た。
今回のは若干怪しいと自分でも思っているので、もっと正しいやり方があったら知りたい。

また、なんとなく次期バージョンだともうちょっと扱いやすくなってそうなので、それまで待ってもいいのかもしれない。