※当サイトの記事には、広告・プロモーションが含まれます。

WorkboxのService Workerの挙動が分かり辛い...

nazology.net

nazology.net

⇧ amazing...

WorkboxのService Workerの挙動が分かり辛い...

Vue.jsでというか、Vue CLIで作成されたプロジェクトで、「@vue/cli-plugin-pwa」ってライブラリをインストールしていると、「WorkBox」というもので「Service Worker」を便利に使えるらしいのですが、挙動が分かり辛いですと。

ちなみに、Vue CLIを利用した場合、

re-engines.com

Progressive Web App (PWA) Support オプションを選択した場合、以下の npm package が追加されます。

  • “register-service-worker”: “^1.6.2”,
  • “@vue/cli-plugin-pwa”: “^4.2.0”,

また、 Service Worker 関連で registerServiceWorker.ts が追加されます。

Vue CLIでPWAが簡単に実装できる 〜 Service Worker と A2HS 〜 | RE:ENGINES

⇧ ということになる模様。

Vue CLIを使わずに作成してるVue.jsのプロジェクトの場合でも、npmなどで上記のモジュールを追加すれば、「WorkBox」で「Service Worker」を使えるようになるってことかと。

ちなみに、

stackoverflow.com

After taking a few days off and looking back at it now, I have realized that the currents docs by google are for workbox version 6, and documentation for workbox version 5 is marked legacy.

Whereas the @vue/cli-plugin-pwa plugin has a dependency for workbox version 4.3, which is severely outdated; there are a lot of breaking changes and compatibility issues between the two. Given this fact and that I am short on time I started to look in a different direction.

https://stackoverflow.com/questions/71515017/how-to-setup-google-workbox-with-vue-js-spa

⇧「@vue/cli-plugin-pwa」の依存関係の「Workbox」のバージョンが旧いという問題があるらしい...

「@vue/cli-plugin-pwa」の依存関係が、package.jsonに記載のもので合ってるのかが分かりませんが、

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-typescript-admin-template\node_modules\@vue\cli-plugin-pwa\package.json

{
  "_from": "@vue/cli-plugin-pwa@^4.5.12",
  "_id": "@vue/cli-plugin-pwa@4.5.17",
  "_inBundle": false,
  "_integrity": "sha512-IaODWmj5eQjv97ne0CTOgPZA8QmVS7zYX64C+SivWPw0uevJAhNUdDHgyrUODP7fEfyufKliStLMQJTowohGNQ==",
  "_location": "/@vue/cli-plugin-pwa",
  "_phantomChildren": {
    "@webassemblyjs/ast": "1.9.0",
    "@webassemblyjs/helper-module-context": "1.9.0",
    "@webassemblyjs/wasm-edit": "1.9.0",
    "@webassemblyjs/wasm-parser": "1.9.0",
    "acorn": "6.4.2",
    "ajv": "6.12.6",
    "ajv-errors": "1.0.1",
    "ajv-keywords": "3.5.2",
    "arr-diff": "4.0.0",
    "arr-flatten": "1.1.0",
    "array-unique": "0.3.2",
    "big.js": "5.2.2",
    "chrome-trace-event": "1.0.3",
    "define-property": "2.0.2",
    "emojis-list": "3.0.0",
    "enhanced-resolve": "4.5.0",
    "esrecurse": "4.3.0",
    "estraverse": "4.3.0",
    "extend-shallow": "3.0.2",
    "extglob": "2.0.4",
    "fragment-cache": "0.2.1",
    "is-buffer": "1.1.6",
    "is-extendable": "0.1.1",
    "isobject": "3.0.1",
    "json-parse-better-errors": "1.0.2",
    "kind-of": "6.0.3",
    "loader-runner": "2.4.0",
    "memory-fs": "0.4.1",
    "minimist": "1.2.6",
    "mkdirp": "0.5.6",
    "nanomatch": "1.2.13",
    "neo-async": "2.6.2",
    "node-libs-browser": "2.2.1",
    "object.pick": "1.3.0",
    "regex-not": "1.0.2",
    "repeat-element": "1.1.4",
    "repeat-string": "1.6.1",
    "snapdragon": "0.8.2",
    "snapdragon-node": "2.1.1",
    "split-string": "3.1.0",
    "tapable": "1.1.3",
    "terser-webpack-plugin": "1.4.5",
    "to-regex": "3.0.2",
    "watchpack": "1.7.5",
    "webpack-sources": "1.4.3"
  },
  "_requested": {
    "type": "range",
    "registry": true,
    "raw": "@vue/cli-plugin-pwa@^4.5.12",
    "name": "@vue/cli-plugin-pwa",
    "escapedName": "@vue%2fcli-plugin-pwa",
    "scope": "@vue",
    "rawSpec": "^4.5.12",
    "saveSpec": null,
    "fetchSpec": "^4.5.12"
  },
  "_requiredBy": [
    "#DEV:/"
  ],
  "_resolved": "https://registry.npmjs.org/@vue/cli-plugin-pwa/-/cli-plugin-pwa-4.5.17.tgz",
  "_shasum": "73b2f9dd1203de46761a9843e972966e2717fe87",
  "_spec": "@vue/cli-plugin-pwa@^4.5.12",
  "_where": "C:\\Users\\Toshinobu\\Desktop\\soft_work\\vue_work\\vue-typescript-admin-template",
  "author": {
    "name": "Evan You"
  },
  "bugs": {
    "url": "https://github.com/vuejs/vue-cli/issues"
  },
  "bundleDependencies": false,
  "dependencies": {
    "@vue/cli-shared-utils": "^4.5.17",
    "webpack": "^4.0.0",
    "workbox-webpack-plugin": "^4.3.1"
  },
  "deprecated": false,
  "description": "pwa plugin for vue-cli",
  "devDependencies": {
    "register-service-worker": "^1.7.1"
  },
  "gitHead": "58ff39cef3ec3574018089b3ea5bba5bb0abc10f",
  "homepage": "https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-pwa#readme",
  "keywords": [
    "vue",
    "cli",
    "pwa"
  ],
  "license": "MIT",
  "main": "index.js",
  "name": "@vue/cli-plugin-pwa",
  "peerDependencies": {
    "@vue/cli-service": "^3.0.0 || ^4.0.0-0"
  },
  "publishConfig": {
    "access": "public"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/vuejs/vue-cli.git",
    "directory": "packages/@vue/cli-plugin-pwa"
  },
  "version": "4.5.17"
}

⇧ 依存関係の中に「"workbox-webpack-plugin": "^4.3.1"」ってあるので、「Workbox」のバージョンは旧くなってるということになるんですかね?

もう1つ、「Service Worker」は、Googleが発祥という事で、「Android」なんかは問題なく対応してるとは思うのですが、

minory.org

やっと、iOS 11.4Safari 11.1)からPWAProgressive Web Apps)に対応しました!
簡単に何ができるかと言うと、WEBアプリオフラインでも動作するネイティブアプリのように振る舞うことができるようになりました。

iOSにも対応したService Workerを使ってPWAの基礎を学ぶ | Minory

⇧ 上記サイト様によりますと、「iOS」も対応が進んでいるようです。

脱線しましたが、Service Workerが登録される流れとしては、

qiita.com

nhiroki.jp

⇧ ブラウザとは関係なく、バックグラウンドで非同期に処理されてるということのようですが、stateがactivatedになっていると、Service Worker自体の読み込みは完了したと言えることになると考えて良いのか?

いまいち、ハッキリしない...

hazm.at

⇧ 上記サイト様によりますと、エラーが無ければ「Activated」が最終的な状態になるってことですかね?

console.dirで確認してみる

とりあえず、console.dirで確認してみる。

確認に利用するプロジェクトは、

ts0818.hatenablog.com

⇧ 上記記事で、インストールしたVue.jsの管理画面の雛型を使ってます。

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-typescript-admin-template\src\pwa\register-service-worker.ts

/* eslint-disable no-console */

import { register } from 'register-service-worker'

// if (process.env.NODE_ENV === 'production') {
if (process.env.NODE_ENV !== 'production') {
  register(`${process.env.BASE_URL}service-worker.js`, {
    ready() {
      console.log(
        'App is being served from cache by a service worker.\n' +
        'For more details, visit https://goo.gl/AFskqB'
      )
    },
    registered(registration) {
      console.log('Service worker has been registered.')
      // Routinely check for app updates by testing for a new service worker.
      // setInterval(() => {
      //   registration.update()
      // }, 1000 * 60 * 60) // hourly checks
      console.dir(registration)
    },
    cached() {
      console.log('Content has been cached for offline use.')
    },
    updatefound() {
      console.log('New content is downloading.')
    },
    updated(registration) {
      console.log('New content is available; please refresh.')
      // Add a custom event and dispatch it.
      // Used to display of a 'refresh' banner following a service worker update.
      // Set the event payload to the service worker registration object.
      document.dispatchEvent(
        new CustomEvent('swUpdated', { detail: registration })
      )
    },
    offline() {
      console.log('No internet connection found. App is running in offline mode.')
    },
    error(error) {
      console.error('Error during service worker registration:', error)
    }
  })
}

⇧ 本番環境ではないので、本番環境以外でService Workerが動くように条件を修正。

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-typescript-admin-template\src\views\login\components\LoadingScreen.vue

<template>
  <!--<div :class="{loader: true, fadeout: !isLoading}">-->
  <div>
    <div class="page-container loader">
      <img class="splash" src="@/assets/splash-loading.jpg" />
    </div>
  </div>
</template>

<script lang="ts">
/* eslint-disable no-console */
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'

Component({
  name: 'LoadingScreen',
  components: {
  }
})
export default class Loading extends Vue {
  @Prop()
  private isLoading = true;

  // created() {
  //   console.log('■')
  // }

  // mounted() {
  //   this.checkloading()
  // }

  // private checkloading() {
  //   console.log('■created()')
  // }

  // @Watch('isLoading')
  // private watchIsLoading() {
  //   console.log('■')
  // }
}
</script>

<style scoped>
.page-container {
    max-width: 500px;
    width: 100%;
    margin: auto;
}

.loader {
  background-color: #ffffff;
  /**bottom: 0; */
  color: white;
  /**display: flex; */
  align-items: center;
  text-align: center;
  /**left: 0; */
  overflow: hidden;
  position: fixed;
  inset: 0;
  /**right: 0; */
  /**top: 0; */
}

.splash {
  width: 120px;
}

.fadeout {
  animation: fadeout 2s forwards;
}

@keyframes fadeout {
  to {
    opacity: 0;
    visibility: hidden;
  }
}
</style>

⇧ ローディング画面(スマホとかだと、スプラッシュと言うらしい)用のVueファイルを準備。

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-typescript-admin-template\src\views\login\index.vue

<template>
  <div class="login-container">
    <el-form
      ref="loginForm"
      :model="loginForm"
      :rules="loginRules"
      class="login-form"
      autocomplete="on"
      label-position="left"
    >
      <div class="title-container">
        <h3 class="title">
          {{ $t('login.title') }} | テス | テス
        </h3>
        <lang-select class="set-language" />
      </div>

      <el-form-item prop="username">
        <span class="svg-container">
          <svg-icon name="user" />
        </span>
        <el-input
          ref="username"
          v-model="loginForm.username"
          :placeholder="$t('login.username')"
          name="username"
          type="text"
          tabindex="1"
          autocomplete="on"
        />
      </el-form-item>

      <el-tooltip
        v-model="capsTooltip"
        content="Caps lock is On"
        placement="right"
        manual
      >
        <el-form-item prop="password">
          <span class="svg-container">
            <svg-icon name="password" />
          </span>
          <el-input
            :key="passwordType"
            ref="password"
            v-model="loginForm.password"
            :type="passwordType"
            :placeholder="$t('login.password')"
            name="password"
            tabindex="2"
            autocomplete="on"
            @keyup.native="checkCapslock"
            @blur="capsTooltip = false"
            @keyup.enter.native="handleLogin"
          />
          <span
            class="show-pwd"
            @click="showPwd"
          >
            <svg-icon :name="passwordType === 'password' ? 'eye-off' : 'eye-on'" />
          </span>
        </el-form-item>
      </el-tooltip>

      <el-button
        :loading="loading"
        type="primary"
        style="width:100%; margin-bottom:30px;"
        @click.native.prevent="handleLogin"
      >
        {{ $t('login.logIn') }}
      </el-button>

      <div style="position:relative">
        <div class="tips">
          <span>{{ $t('login.username') }} : admin </span>
          <span>{{ $t('login.password') }} : {{ $t('login.any') }} </span>
        </div>
        <div class="tips">
          <span>{{ $t('login.username') }} : editor </span>
          <span>{{ $t('login.password') }} : {{ $t('login.any') }} </span>
        </div>

        <el-button
          class="thirdparty-button"
          type="primary"
          @click="showDialog=true"
        >
          {{ $t('login.thirdparty') }}
        </el-button>
      </div>
    </el-form>

    <el-dialog
      :title="$t('login.thirdparty')"
      :visible.sync="showDialog"
    >
      {{ $t('login.thirdpartyTips') }}
      <br>
      <br>
      <br>
      <social-sign />
    </el-dialog>
    <LoadingScreen :isLoading="isLoading" />
  </div>
</template>

<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator'
import { Route } from 'vue-router'
import { Dictionary } from 'vue-router/types/router'
import { Form as ElForm, Input } from 'element-ui'
import { UserModule } from '@/store/modules/user'
import { isValidUsername } from '@/utils/validate'
import LangSelect from '@/components/LangSelect/index.vue'
import SocialSign from './components/SocialSignin.vue'
import LoadingScreen from './components/LoadingScreen.vue'

@Component({
  name: 'Login',
  components: {
    LangSelect,
    SocialSign,
    LoadingScreen
  }
})
export default class extends Vue {
  private validateUsername = (rule: any, value: string, callback: Function) => {
    if (!isValidUsername(value)) {
      callback(new Error('Please enter the correct user name'))
    } else {
      callback()
    }
  }

  private validatePassword = (rule: any, value: string, callback: Function) => {
    if (value.length < 6) {
      callback(new Error('The password can not be less than 6 digits'))
    } else {
      callback()
    }
  }

  private loginForm = {
    username: 'admin',
    password: '111111'
  }

  private loginRules = {
    username: [{ validator: this.validateUsername, trigger: 'blur' }],
    password: [{ validator: this.validatePassword, trigger: 'blur' }]
  }

  private passwordType = 'password'
  private loading = false
  private showDialog = false
  private capsTooltip = false
  private redirect?: string
  private otherQuery: Dictionary<string> = {}
  private isLoading = true

  @Watch('$route', { immediate: true })
  private onRouteChange(route: Route) {
    // TODO: remove the "as Dictionary<string>" hack after v4 release for vue-router
    // See https://github.com/vuejs/vue-router/pull/2050 for details
    const query = route.query as Dictionary<string>
    if (query) {
      this.redirect = query.redirect
      this.otherQuery = this.getOtherQuery(query)
    }
  }

  mounted() {
    if (isComplete) {
      this.isLoading = false
    }
    if (this.loginForm.username === '') {
      (this.$refs.username as Input).focus()
    } else if (this.loginForm.password === '') {
      (this.$refs.password as Input).focus()
    }
  }

  private checkCapslock(e: KeyboardEvent) {
    const { key } = e
    this.capsTooltip = key !== null && key.length === 1 && (key >= 'A' && key <= 'Z')
  }

  private showPwd() {
    if (this.passwordType === 'password') {
      this.passwordType = ''
    } else {
      this.passwordType = 'password'
    }
    this.$nextTick(() => {
      (this.$refs.password as Input).focus()
    })
  }

  private handleLogin() {
    (this.$refs.loginForm as ElForm).validate(async(valid: boolean) => {
      if (valid) {
        this.loading = true
        await UserModule.Login(this.loginForm)
        this.$router.push({
          path: this.redirect || '/',
          query: this.otherQuery
        }).catch(err => {
          console.warn(err)
        })
        // Just to simulate the time of the request
        setTimeout(() => {
          this.loading = false
        }, 0.5 * 1000)
      } else {
        return false
      }
    })
  }

  private getOtherQuery(query: Dictionary<string>) {
    return Object.keys(query).reduce((acc, cur) => {
      if (cur !== 'redirect') {
        acc[cur] = query[cur]
      }
      return acc
    }, {} as Dictionary<string>)
  }
}

let isComplete = false

// Service Workerの確認
navigator.serviceWorker.getRegistrations()
  .then(function(registrations) {
    console.log(isComplete)
    console.log('■LoadingScreen')
    console.dir(registrations)
    let count = 0
    for (const registration of registrations) {
      console.dir(registration)
      if (registration.active && registration.active.state === 'activated') {
        console.log('■Service Workerの読み込み完了■')
        count++
      }
    }
    if (count === registrations.length) {
      console.log('■□■すべてのService Workerの読み込み完了■□■')
      isComplete = true
    }
  }).finally(() => {
    console.log(isComplete)
    const element = document.getElementsByClassName('page-container')[0].classList
    element.add('fadeout')
  })
</script>

<style lang="scss">
@supports (-webkit-mask: none) and (not (cater-color: $loginCursorColor)) {
  .login-container .el-input {
    input { color: $loginCursorColor; }
    input::first-line { color: $lightGray; }
  }
}

.login-container {
  .el-input {
    display: inline-block;
    height: 47px;
    width: 85%;

    input {
      height: 47px;
      background: transparent;
      border: 0px;
      border-radius: 0px;
      padding: 12px 5px 12px 15px;
      color: $lightGray;
      caret-color: $loginCursorColor;
      -webkit-appearance: none;

      &:-webkit-autofill {
        box-shadow: 0 0 0px 1000px $loginBg inset !important;
        -webkit-text-fill-color: #fff !important;
      }
    }
  }

  .el-form-item {
    border: 1px solid rgba(255, 255, 255, 0.1);
    background: rgba(0, 0, 0, 0.1);
    border-radius: 5px;
    color: #454545;
  }
}
</style>

<style lang="scss" scoped>
.login-container {
  height: 100%;
  width: 100%;
  overflow: hidden;
  background-color: $loginBg;

  .login-form {
    position: relative;
    width: 520px;
    max-width: 100%;
    padding: 160px 35px 0;
    margin: 0 auto;
    overflow: hidden;
  }

  .tips {
    font-size: 14px;
    color: #fff;
    margin-bottom: 10px;

    span {
      &:first-of-type {
        margin-right: 16px;
      }
    }
  }

  .svg-container {
    padding: 6px 5px 6px 15px;
    color: $darkGray;
    vertical-align: middle;
    width: 30px;
    display: inline-block;
  }

  .title-container {
    position: relative;

    .title {
      font-size: 26px;
      color: $lightGray;
      margin: 0px auto 40px auto;
      text-align: center;
      font-weight: bold;
    }

    .set-language {
      color: #fff;
      position: absolute;
      top: 3px;
      font-size: 18px;
      right: 0px;
      cursor: pointer;
    }
  }

  .show-pwd {
    position: absolute;
    right: 10px;
    top: 7px;
    font-size: 16px;
    color: $darkGray;
    cursor: pointer;
    user-select: none;
  }

  .thirdparty-button {
    position: absolute;
    right: 0;
    bottom: 6px;
  }

  @media only screen and (max-width: 470px) {
    .thirdparty-button {
      display: none;
    }
  }
}
</style>

で保存して、スプラッシュの画像はフリー素材をダウンロードして配置しておきます。

で、npm run serveで、開発用サーバーを起動して、Vue.jsのアプリケーションを起動すると、

⇧ Service Workerのstateがactivatedになっていることは確認できました。

Service WorkerのstateをVue.jsのインスタンスに連携ができなさそうなのよね...

Chromeデベロッパーツールの「Application」タブで、「Service Worker」の「Status」が「activated and is stopped」ってのが気になる...

どうやら、

developer.chrome.com

  • The Status line tells you the status of the service worker. The number on this line (#1 in the screenshot above) indicates how many times the service worker has been updated. If you enable the update on reload checkbox you'll notice that the number increments on every page load. Next to the status you'll see a start button (if the service worker is stopped) or a stop button (if the service worker is running). Service workers are designed to be stopped and started by the browser at any time. Explicitly stopping your service worker using the stop button can simulate that. Stopping your service worker is a great way to test how your code behaves when the service worker starts back up again. It frequently reveals bugs due to faulty assumptions about persistent global state.

https://developer.chrome.com/docs/devtools/progressive-web-apps/

⇧「Service Worker」が「activated」されたものの、停止してる状態らしい...

う~む、「activated」は、

medium.com

⇧「activate Event」が完了したら、「Service Worker」が動いていようが停まっていようが関係なく「activated」な状態になるってことなんですかね?

そもそも、Service Workerのstateを判断材料にして良いのかも分からんのですが...

毎度モヤモヤ感が半端ない...

2022年7月27日(水)追記:↓ ここから

気になった情報を見つけたので、

qiita.com

⇧ 上記サイト様を参考に、Service Worker 部分を書き換えてみました。

変更部分を抜粋してます。

<script lang="ts">
...省略
let isComplete = false

// Service Workerの確認
;(async() => {
  await navigator.serviceWorker.ready

  const registration: any = await navigator.serviceWorker.getRegistration()
  if (registration) {
    console.log('■getRegistration()')
    registration.addEventListener('controllerchange', () => {
      console.log('■')
    })
    registration.addEventListener('updatefound', () => {
      console.log('■□■')
    })
    registration.update()
  }

  navigator.serviceWorker.getRegistrations()
    .then(function(registrations) {
      console.log(isComplete)
      console.log('■LoadingScreen')
      console.dir(registrations)
      let count = 0
      for (const registration of registrations) {
        console.dir(registration)
        registration.addEventListener('updatefound', () => {
          console.log('更新あり')
        })

        if (registration.active && registration.active.state === 'activated') {
          console.log('■Service Workerの読み込み完了■')
          count++
        }
      }
      if (count === registrations.length) {
        console.log('■□■すべてのService Workerの読み込み完了■□■')
        isComplete = true
      }
    }).finally(() => {
      console.log(isComplete)
      const element = document.getElementsByClassName('page-container')[0].classList
      element.add('fadeout')
    })
})()
</script>    

で、アクセスしてみたけども、

残念ながら、updatefoundの情報を取得する方法が分からないから、更新できるService Worker が存在してようがいなかろうが、無差別に、registration.update()するしかない感じになってしまってますが...

developer.mozilla.org

developer.mozilla.org

⇧ 普通に考えたら、updatefound()の結果から判断して、update()を実行したいと思うんだけど、ドキュメントの参考例が実用的で無さ過ぎるんよね...

そもそもとして、「C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-typescript-admin-template\src\pwa\register-service-worker.ts」の中の、

    updated(registration) {
      console.log('New content is available; please refresh.')
      // Add a custom event and dispatch it.
      // Used to display of a 'refresh' banner following a service worker update.
      // Set the event payload to the service worker registration object.
      document.dispatchEvent(
        new CustomEvent('swUpdated', { detail: registration })
      )
    }    

が実行されてないのが、良く分からん...

だって、updatefound()が呼ばれてるんだから、新しいService Workerが存在するはずだから、updated()が呼ばれないのがちょっと意味が分からない...

2022年7月27日(水)追記:↑ ここまで

2022年7月28日(木)追記:↓ ここから

どうやら、production mode でVue.jsのアプリケーションを起動しないと、WorkboxのService Worker のupdated()メソッドは実行されないらしい。

あと、vue-notificationっていうライブラリを自分でインストールする必要があるっぽいです、雛型が中途半端過ぎる...

www.npmjs.com

⇧ 上記をインストールしときます。

package.jsonのあるディレクトリまで移動して、

コマンドプロンプトなどで、以下のコマンドを実行。バージョンとかはインストールした時点での最新バージョンをインストールしてます。

npm install vue-notification@1.3.20 --save-dev 

vue notificationを利用できるように修正。

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-typescript-admin-template\src\main.ts

import Vue, { DirectiveOptions } from 'vue'

import 'normalize.css'
import ElementUI from 'element-ui'
import SvgIcon from 'vue-svgicon'

import '@/styles/element-variables.scss'
import '@/styles/index.scss'

import App from '@/App.vue'
import store from '@/store'
import { AppModule } from '@/store/modules/app'
import router from '@/router'
import i18n from '@/lang'
import '@/icons/components'
import '@/permission'
import '@/utils/error-log'
import '@/pwa/register-service-worker'
import * as directives from '@/directives'
import * as filters from '@/filters'
import Notifications from 'vue-notification'

Vue.use(Notifications)

Vue.use(ElementUI, {
  size: AppModule.size, // Set element-ui default size
  i18n: (key: string, value: string) => i18n.t(key, value)
})

Vue.use(SvgIcon, {
  tagName: 'svg-icon',
  defaultWidth: '1em',
  defaultHeight: '1em'
})

// Register global directives
Object.keys(directives).forEach(key => {
  Vue.directive(key, (directives as { [key: string ]: DirectiveOptions })[key])
})

// Register global filter functions
Object.keys(filters).forEach(key => {
  Vue.filter(key, (filters as { [key: string ]: Function })[key])
})

Vue.config.productionTip = false

new Vue({
  router,
  store,
  i18n,
  render: (h) => h(App)
}).$mount('#app')

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-typescript-admin-template\src\App.vue

<template>
  <div id="app">
    <router-view />
    <service-worker-update-popup />
    <notifications/>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import ServiceWorkerUpdatePopup from '@/pwa/components/ServiceWorkerUpdatePopup.vue'

@Component({
  name: 'App',
  components: {
    ServiceWorkerUpdatePopup
  }
})
export default class extends Vue {}
</script>
    

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-typescript-admin-template\src\pwa\components\ServiceWorkerUpdatePopup.vue

<script lang="ts">
/* eslint-disable no-console */
import { Component, Vue } from 'vue-property-decorator'

@Component({
  name: 'ServiceWorkerUpdatePopup'
})
export default class extends Vue {
  private refreshing = false
  private notificationText = 'New content is available!'
  private refreshButtonText = 'Refresh'
  private registration: ServiceWorkerRegistration | null = null

  created() {
    // Listen for swUpdated event and display refresh notification as required.
    document.addEventListener('swUpdated', this.showRefreshUI, { once: true })
    // Refresh all open app tabs when a new service worker is installed.
    navigator.serviceWorker.addEventListener('controllerchange', () => {
      console.log('■リロード')
      if (this.refreshing) return
      this.refreshing = true
      window.location.reload()
    })
  }

  render() {
    // Avoid warning for missing template
  }

  private showRefreshUI(e: Event) {
    // Display a notification inviting the user to refresh/reload the app due
    // to an app update being available.
    // The new service worker is installed, but not yet active.
    // Store the ServiceWorkerRegistration instance for later use.
    const h = this.$createElement
    this.registration = (e as CustomEvent).detail
    this.$notify.info({
      title: 'Update available',
      message: h('div', { class: 'sw-update-popup' }, [
        this.notificationText,
        h('br'),
        h('button', {
          on: {
            click: (e: Event) => {
              e.preventDefault()
              this.refreshApp()
            }
          }
        }, this.refreshButtonText)
      ]),
      position: 'top-right',
      duration: 10
    })
  }

  private refreshApp() {
    // Protect against missing registration.waiting.
    if (!this.registration || !this.registration.waiting) return
    this.registration.waiting.postMessage('skipWaiting')
  }
}
</script>

<style lang="scss" scoped>
.sw-update-popup > button {
  margin-top: 0.5em;
  padding: 0.25em 1.5em;
}
</style>

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-typescript-admin-template\src\pwa\register-service-worker.ts

/* eslint-disable no-console */

import { register } from 'register-service-worker'

if (process.env.NODE_ENV === 'production') {
// if (process.env.NODE_ENV !== 'production') {
  register(`${process.env.BASE_URL}service-worker.js`, {
    ready() {
      console.log(
        'App is being served from cache by a service worker.\n' +
        'For more details, visit https://goo.gl/AFskqB'
      )
    },
    registered(registration) {
      console.log('Service worker has been registered.')
      // Routinely check for app updates by testing for a new service worker.
      // setInterval(() => {
      //   registration.update()
      // }, 1000 * 60 * 60) // hourly checks
      console.dir(registration)
    },
    cached() {
      console.log('Content has been cached for offline use.')
    },
    updatefound() {
      console.log('New content is downloading.')
    },
    updated(registration) {
      console.log('New content is available; please refresh.')
      // Add a custom event and dispatch it.
      // Used to display of a 'refresh' banner following a service worker update.
      // Set the event payload to the service worker registration object.
      document.dispatchEvent(
        new CustomEvent('swUpdated', { detail: registration })
      )
    },
    offline() {
      console.log('No internet connection found. App is running in offline mode.')
    },
    error(error) {
      console.error('Error during service worker registration:', error)
    }
  })
}

で、保存して、コマンドプロンプトで、production modeで起動。

"C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-typescript-admin-template\node_modules\.bin\vue-cli-service" serve --mode=production  

で、ブラウザでアクセスすると、

⇧ updated()メソッドのconsole.logの内容が表示されてるので、updated()メソッドが実行されたようです。

「@vue/cli-plugin-pwa」の「Workbox」の「Service Worker」に特有の問題なのか分かりませんが、カオスな世界になってますな...

2022年7月28日(木)追記:↑ ここまで

今回はこのへんで。