【備忘録】Vue.js(SPA) + PWA + ServiceWorker 環境構築メモ

すごい分かり難いのでメモしとく。

# 用語 etc.
* Vue.js(https://ja.vuejs.org/)
* SPA([シングルページアプリケーション | Single Page Application | MDN](https://developer.mozilla.org/ja/docs/Glossary/SPA))
* PWA([プログレッシブウェブアプリケーション | Progressive Web Application | MDN](https://developer.mozilla.org/ja/docs/Web/Progressive_web_apps))
* [サービスワーカー | Service Worker | MDN](https://developer.mozilla.org/ja/docs/Web/API/Service_Worker_API)
    * `new webpack.Worker()`とかの [ウェブワーカー | Web Worker | MDN](https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Using_web_workers) とは別のもの
* [Workbox | Chrome Developers](https://developer.chrome.com/docs/workbox/)

# 環境

* Vue.js - v3.x
* @vue/cli-plugin-pwa - v5.x
* register-service-worker - v1.7.2

# 1. `@vue/cli-plugin-pwa` を導入

公式：[@vue/cli-plugin-pwa](https://cli.vuejs.org/core-plugins/pwa.html#vue-cli-plugin-pwa)

`vue create`する際に同時に導入するか、既存のプロジェクトに後で追加[（公式）](https://cli.vuejs.org/core-plugins/pwa.html#installing-in-an-already-created-project)するか

## 1.1 （参考）関連ファイル

* `./src/main.js`
    * `./src/registerServiceWorker.js`を`import`する
* `./src/registerServiceWorker.js`
    * ServiceWorker の登録等。`./dist/service-worker.js`を`register`する
*  `./src/sw.js`
    * `workbox` 経由でビルドされるServiceWorkerの元。カスタマイズコードなどもこのファイルに追加する。`vue.config`の`pwa.workboxOptions.swSrc`で指定。
* `./vue.config.js`
    * `workbox`の設定等。
* `./dist/service-worker.js`
    * 実際にクライアント側でハンドルされる`workbox`でビルドされた ServiceWorkerの実態。`vue.config`の`pwa.workboxOptions.swDest`で指定。

## 1.2 (参考) 注意点

* Service Worker は、`production`モードでしかビルドされない。`npm run serve`等では実行不可。
* `localhost`でも`production`モードでビルド・実行ならService Workerは実行可能。
    * local web server があればテスト可能。
        * Simple Web Server | https://simplewebserver.org/download.html
        * Firebase Local Emulator Suite | https://firebase.google.com/docs/emulator-suite?hl=ja
* `Workbox`[(公式)](https://developer.chrome.com/docs/workbox/)をある程度理解する必要がある。
    * 簡単な話、Service Workerの実態をビルドしてくれるプラグイン
* 特に何も指定しない場合、`./dist`配下は、全て`precache`される模様
    * `http(s)`プロトコルで`precache`される。特別なプロトコルで云々...ではない。
    * 全ての`precache`時にエラーなしで`install`は正常と判断。一つでもエラーがあれば失敗。`ServiceWorker.state === "redundant"`になる。（[ServiceWorker.state | MDN](https://developer.mozilla.org/ja/docs/Web/API/ServiceWorker/state)）
    * ので、`./dist/404.html`等があると404エラーとなり`install`は必ず失敗する。何かしらの対処が必須。

# 2. `vue.config`の設定

重要なのは以下。
※ `webpack`ではなく`pwa`配下の`workbox`のが重要

```js:vue.config.js
module.exports = defineConfig({
  //...
  pwa: {
    // configure the workbox plugin
    workboxPluginMode: "InjectManifest", // カスタマイズされたServiceWorkerを運用したいならこっち。
    workboxOptions: {
      // swSrc is required in InjectManifest mode.
      swSrc: "./src/sw.js", // workbox でビルドされるServiceWorkerの元
      swDest: "./service-worker.js", // workbox でビルドされたServiceWorkerの実態。./distにビルドされる
      // ...other Workbox options...
      exclude: [/404\.html/], // precache から除外するファイル
    },
  },
  configureWebpack: {
    // webpack の設定
    // pwa + ServiceWorker では、特別変更する必要はない
  },
});
```

# 3. `./src/sw.js` に `Workbox`による`precache`を実装する

Workbox の injectManifest モードで`precache`を利用する際は必須。
指定しない場合は build でエラーになる。

[Precaching with Workbox#Side-by-side comparison | Chrome Developers](https://developer.chrome.com/docs/workbox/precaching-with-workbox/#side-by-side-comparison)

```js:./src/sw.js
import { precacheAndRoute } from "workbox-precaching";

precacheAndRoute(self.__WB_MANIFEST);

//以降は実際に動作させたい Service Worker の処理

self.addEventListener("fetch", (event)=>{
  console.log("[sw.js] fetch", event);
});
```

# 4. Service Worker の動作確認

ビルド→LocalWebServer起動→index.htmlにアクセスで可能

## 4.1 ビルド

```bash:bash
$ npm run build

( 省略 )

 DONE  Build complete. The dist directory is ready to be deployed.
 INFO  Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html
```

## 4.2 `./dist/service-worker.js`があるか確認

まぁ、ある

## 4.3 Local Web Server の起動

firebase emulators の場合

```bash:bash
$ firebase emulators:start

( 省略 )

┌─────────────────────────────────────────────────────────────┐   
│ ✔  All emulators ready! It is now safe to connect your app. │   
│ i  View Emulator UI at http://127.0.0.1:4000/               │   
└─────────────────────────────────────────────────────────────┘   

```

## 4.4 localhost にブラウザでアクセス

firebase emulators Hosting | http://localhost:5000

以下の様なログがブラウザのコンソールに出力されていれば大体成功

```log:console.log
Service worker has been registered.
New content is downloading.
Content has been cached for offline use.
App is being served from cache by a service worker.
For more details, visit https://goo.gl/AFskqB
```

### 4.4.1 初回アクセス時のイベント発行順序
初回は以下の順で`./src/registerServiceWorker.js`内のイベント(関数)が発行される。
（※正常ケース）

1. registered()
1. updatefound()
1. cached()
1. ready()

### 4.4.2 ２回目以降
（※正常ケース）

1. ready()
1. registered()

### 4.4.3 異常ケース

初回アクセス時。ready()の発行がないパターン。
（めちゃくちゃ分かりづらい

```log:console.log
Service worker has been registered.
New content is downloading.
```

1. registered()
1. updatefound()

この場合は、F5でブラウザをリロードしてみる

```log:console.log
Service worker has been registered.
registerServiceWorker.js:21 New content is downloading.
Uncaught (in promise) bad-precaching-response: bad-precaching-response :: [{"url":"http://127.0.0.1:5000/404.html","status":404}]
    at A._handleInstall (http://127.0.0.1:5000/service-worker.js:2917:217483)
    at async A._handle (http://127.0.0.1:5000/service-worker.js:2917:216888)
    at async A._getResponse (http://127.0.0.1:5000/service-worker.js:2917:215954)
```

分かり辛いが`workbox`の`precache`時に404エラーが発生して`register`が失敗しているらしい。
ので、`vue.config`の`pwa.workboxOptions.exclude`に、`precache`しないファイルを指定する必要がある。（本投稿内で説明済み）



# ext.1. Reload からの 404 に対応する

PWA というか、SPA で必要

## ext.1.1 Web Hash History Mode 利用の場合

Web Server 側の設定変更ができない場合等
（※Github Pages で Vue.js アプリをホストする場合等

### ext.1.1.1 vue-router を Web Hash History Mode にする

vue-router https://router.vuejs.org/api/#Functions-createWebHashHistory

```diff_javascript:./src/router/index.js
- import { createRouter, createWebHistory } from "vue-router";
+ import { createRouter, createWebHashHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";

// (省略)

const router = createRouter({
-  history: createWebHistory(),
+  history: createWebHashHistory(),
  routes,
});

export default router;
```

### ext.1.1.2 BASE_URL を変更する

```diff_javascript:./src/router/index.js
// (省略)

+ const BASE_URL = (() => {
+   if (process.env.NODE_ENV === "production") {
+     return process.env.BASE_URL + "<your-github-project-name>";
+   } else {
+     return process.env.BASE_URL;
+   }
+ })();

const router = createRouter({
  history: createWebHashHistory(),
+   history: createWebHashHistory(BASE_URL),
  routes,
});

export default router;
```

## ext.1.2 Web History Mode 利用の場合

Web Server 側の設定を変更できる場合等に使える

### ext.1.2.1 vue-router を Web History Mode にする

vue-router https://router.vuejs.org/api/#Functions-createWebHistory

```diff_javascript:./router/index.js
- import { createRouter, createWebHashHistory } from "vue-router";
+ import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";

// (省略)

const router = createRouter({
-  history: createWebHashHistory(),
+  history: createWebHistory(),
  routes,
});

export default router;
```

### ext.1.2.2 Web Server に rewrite を設定する

※SPAでは、index.htmlのみしか実態を持たないが、内部で利用されるURL自体は`./hoge`や`./fuga`等は十分ありえる。ブラウザが`./hoge`の時点でreload（もしくは別APIからのredirect）すると http request がWebサーバーに発行されてしまい、404 Not Foundに必ずなる。その動作への対処。
※本投稿では、Firebase Hosting に限定。

```diff_json:firebase.json
{

  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
+    "rewrites": [
+      {
+        "source": "**",
+        "destination": "/index.html"
+      }
    ]
  }
```

## ext.1.3 Webpack Dev Server の場合

ローカルでの開発時に有用。

### ext.1.3.1 webpack config に rewrites を設定する

webpack failback https://webpack.js.org/configuration/dev-server/#devserverhistoryapifallback

```diff_javascript:vue.config.js
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({

// (省略)

  configureWebpack: {
    devServer: {
      historyApiFallback: {
        rewrites: [{ from: /\*/, to: "/index.html" }],
      },
    },
  },

// (省略)

});
```

了