create-react-app
には registerServiceWorker.js
というservice workerを登録する仕組みが乗っかっている。
これはoffline cacheを実現するものなのだが、cache-first strategyという手法を取っており、コードを更新してデプロイした際の取り回しがなかなか手こずるので、割といつもdisableにしがち。
だが、いつまでもこのままというのもちょっと気持ち悪いので、一度service-workerの更新に合わせて「更新してください」みたいなモーダルを出すexampleをやってみた。
仕組みおさらい
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>
);
}
}
まとめ
とりあえずこれで最初に出したデモみたいなものは出来た。
今回のは若干怪しいと自分でも思っているので、もっと正しいやり方があったら知りたい。
また、なんとなく次期バージョンだともうちょっと扱いやすくなってそうなので、それまで待ってもいいのかもしれない。