Vue.jsでPWA(Progressive Web Apps)を実現できるWorkboxと言う名のライブラリを使ってみる

f:id:ts0818:20220122214811j:plain

gigazine.net

Facebookを運営するMetaがあらゆる分野に適応できる自己学習型AI「Data2vec」を開発したと発表しました。

Metaが言語・画像・音声など複数分野に適応できる自己学習型AI「data2vec」を発表 - GIGAZINE

⇧ 分野を問わず汎用的なアプローチでってところは、利用する側にとってはありがたいですな。

というわけで、今回はService Workerなどについてです。

レッツトライ~。

PWA(Progressive Web Apps)とは?

Wikipediaさんに聞いてみた。

progressive web application (PWA), commonly known as a progressive web app, is a type of application software delivered through the web, built using common web technologies including HTMLCSSJavaScript, and WebAssembly. It is intended to work on any platform that uses a standards-compliant browser, including both desktop and mobile devices.

https://en.wikipedia.org/wiki/Progressive_web_application

⇧ とあるように、標準的なブラウザが搭載されているプラットフォームであれば、Webアプリケーションが動かせるってことみたい。

逆に言うと、ブラウザに依存するってことなんですかね。

Since a progressive web app is a type of webpage or website known as a web application, they do not require separate bundling or distribution. Developers can just publish the web application online, ensure that it meets baseline "installability requirements", and users will be able to add the application to their home screen. Publishing the app to digital distribution systems like Apple App Store or Google Play is optional.

https://en.wikipedia.org/wiki/Progressive_web_application

⇧ 頑張れば、ネイティブアプリのように「Apple App Store」や「Google Play」なんかで配信することもできるそうな。

「PWA(Progressive Web Apps)」という言葉は、

In 2015, designer Frances Berriman and Google Chrome engineer Alex Russell coined the term "progressive web apps" to describe apps taking advantage of new features supported by modern browsers, including service workers and web app manifests, that let users upgrade web apps to progressive web applications in their native operating system (OS). Google then put significant efforts into promoting PWA development for Android. Firefox introduced support for service workers in 2016, and Microsoft Edge and Apple Safari followed in 2018, making service workers available on all major systems.

https://en.wikipedia.org/wiki/Progressive_web_application

⇧ 2015年に、デザイナーの方とGoogle Chromeのエンジニアの方が生み出したということみたい。

何をもって「PWA(Progressive Web Apps)」と見なせるのかと言うと、

  • Service Workers
  • Web app Manifests

上記の2つが含まれていれば良いんじゃないの的なノリなんですかね。

ちなみに、

backapp.co.jp

アプリを開発・発注する際にまず決めなければならない「ハイブリッドアプリ」と「ネイティブアプリ」、それぞれのメリットをご存知でしょうか?

https://en.wikipedia.org/wiki/Progressive_web_application

⇧ 上記サイト様によりますと、

  • ハイブリットアプリ
  • ネイティブアプリ
  • Webアプリ

にカテゴライズされてる中の「Webアプリ」に分類されるのが、「PWA(Progressive Web Apps)」ということみたい。

イメージ的には、

www.upwork.com

⇧ 上図のような感じの違いがある模様。一時期話題になった「Flutter」は「ハイブリットアプリ(webview)」を実現するフレームワークの1つということらしい。

Vue.jsでPWA(Progressive Web Apps)を実現できるWorkboxと言う名のライブラリを使ってみる

まぁ、今回は、「PWA(Progressive Web Apps)」ということで、npmのライブラリで「Workbox」というものがあるらしいのですが、Vue.jsのプロジェクトを「Vue CLI」を利用して作成している場合は、

cli.vuejs.org

⇧「@vue/cli-plugin-pwa」ってのを利用すれば良いらしいんだけど、

f:id:ts0818:20220120212922p:plain

⇧ 初回のプロジェクト作成時に「pwa」使うって選択していないと、インストールされてないらしいので、インストールします。

f:id:ts0818:20220120213747p:plain

f:id:ts0818:20220120213811p:plain

追加されました。

f:id:ts0818:20220120213927p:plain

で、ドキュメントがめちゃくちゃ不親切なんだけど、モードが2つあるらしく、

  • GenerateSW
  • InjectManifest

のうち、「InjectManifest」のほうを選ぶと(というかドキュメントのサンプルの設定が「InjectManifest」になっているんだけどね)、設定にあるファイルを自分で作成しとかないといけないらしいというね...分からんわ~!

例えば、Vue.jsの場合は「vue.config.js」ってファイルに設定を書くことになるので、

// vue.config.js

/**
 * @type {import('@vue/cli-service').ProjectOptions}
 */
module.exports = {
  // ...other vue-cli plugin options...
  pwa: {
    name: "My App",
    themeColor: "#4DBA87",
    msTileColor: "#000000",
    appleMobileWebAppCapable: "yes",
    appleMobileWebAppStatusBarStyle: "black",

    // configure the workbox plugin
    workboxPluginMode: "InjectManifest",
    workboxOptions: {
      // swSrc is required in InjectManifest mode.
      swSrc: "src/sw.js",
      swDest: "service-worker.js",
      // ...other Workbox options...
    },
  },
  // webpack系の設定でworkboxの設定は既に含まれているらしい
  //chainWebpack: (config) => {
  //  config.plugin("workbox");
  //},
};

⇧ というような、「workbox」に関する設定をした場合「swSrc」に設定したファイルを事前に作成しておく必要があるらしい...ドキュメントに作成しろとか書いてないから、設定に書いたら自動で作成してくれると思ってしまうよね...

同じ様な疑問を抱えてらっしゃる方がおり、

stackoverflow.com

⇧ とあって、結局、ファイルの中身については、どこから持ってくるかが分からんという...

まぁ、あと、

cli.vuejs.org

⇧「vue.config.js」の設定がいまいちよく分からんのよね、「Vue.js」で「Webpack」系の設定を追記する場合に、「configureWebpack」「chainWebpack」のどっちを使えば良いのか判然としないというね...まぁ、今回は「Webpack」系の設定の追記はないけども。

ちなみに、「vue.config.js」に「Webpack」系のデフォルトの設定がされてるらしく、「vue-cli-service inspect」で確認できる模様。(自分は、「Vue CLI」をグローバルにインストールしていないので、フルパス指定でコマンド実行してます。)

f:id:ts0818:20220123131255p:plain

出力結果をファイルに出力すると確認しやすいかもということで、ファイルに出力してみました。

f:id:ts0818:20220123131511p:plain

ファイルの中身は、productionモード(本番環境)のデフォルトの設定になってます。

f:id:ts0818:20220123131713p:plain

脱線しましたが、とりあえず、「vue.config.js」の「swSrc」に設定したファイルを作成で。

f:id:ts0818:20220120230552p:plain

qiita.com

⇧ 上記サイト様の内容をそのまま流用、元ネタがどこにあるかが分かりませんが...

/**
 * Welcome to your Workbox-powered service worker!
 *
 * You'll need to register this file in your web app and you should
 * disable HTTP caching for this file too.
 * See https://goo.gl/nhQhGp
 */

 workbox.core.setCacheNameDetails({prefix: "my-project"});

 /**
  * The workboxSW.precacheAndRoute() method efficiently caches and responds to
  * requests for URLs in the manifest.
  * See https://goo.gl/S9QRab
  */
 self.__precacheManifest = [].concat(self.__precacheManifest || []);
 // workbox.precaching.suppressWarnings();
 workbox.precaching.precacheAndRoute(self.__precacheManifest, {});    

で、これは現場の環境によって変わるとは思うのですが、process.env.NODE_ENVの値を確認できれば確認しといたほうが良いです。自分は、main.tsでログ出力が出るようにして確認しましたが、現場によっては本番環境ではログを出したくない状況もあると思うので、あくまで開発環境で試すという事で。

/* eslint-disable no-console */
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import "./registerServiceWorker";

Vue.config.productionTip = false;
console.log("■■■" + process.env.NODE_ENV);

new Vue({
  router,
  render: (h) => h(App),
}).$mount("#app");    

⇧ で、自分の環境だと、process.env.NODE_ENVの値がdevelopmentってなったので、以下のファイルを修正。

/* 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() {
      console.log("Service worker has been registered.");
    },
    cached() {
      console.log("Content has been cached for offline use.");
    },
    updatefound() {
      console.log("New content is downloading.");
    },
    updated() {
      console.log("New content is available; please refresh.");
    },
    offline() {
      console.log(
        "No internet connection found. App is running in offline mode."
      );
    },
    error(error) {
      console.error("Error during service worker registration:", error);
    },
  });
}

⇧ 上記のように、条件を変えてます。

で、npm run buildしてみる。

f:id:ts0818:20220121221508p:plain

「.vue」や「.ts」ファイルがビルドされてdistフォルダに静的ファイルとして配置されますと。

f:id:ts0818:20220121221558p:plain

で、Vue.jsのプロジェクトに同梱されてる開発用サーバーを起動して、

f:id:ts0818:20220121221649p:plain

ブラウザから「http://localhost:8080」にアクセスして、ブラウザのページ上で右クリックし「検証」をクリックで、デベロッパーツールを表示します。

f:id:ts0818:20220121221942p:plain

デベロッパーツールの「console」タブでログが確認できます。

f:id:ts0818:20220121221840p:plain

デベロッパーツールの「Application」タグを選んで、

f:id:ts0818:20220121222244p:plain

サイドバーで「Application」の「Service Workers」を選ぶと、http://localhost:8080/でService Workerが登録されてることが分かります。

f:id:ts0818:20220121222359p:plain

Ctrl + C で開発用サーバーを停止し、

f:id:ts0818:20220121222939p:plain

適当にファイルを編集します。

で再度ビルドして、静的ファイルを作り直します。

f:id:ts0818:20220121223240p:plain

で、開発用サーバーを起動してブラウザにアクセス。

Service Workerが更新されない...

ドキュメントを確認してみると、

cli.vuejs.org

というか、デフォルトだとproductionモードってなってるんだが、なぜかdevelopmentモードになってるというのも意味が分からん...

f:id:ts0818:20220122130917p:plain

と思ったら、開発用サーバーの問題だったっぽい...

f:id:ts0818:20220122131105p:plain

とりあえず、Vue.jsプロジェクトのルート直下に、「.env.production」ってファイルを作成で。

f:id:ts0818:20220122213423p:plain

ファイルの中身は、以下で。

NODE_ENV=production    

productionモードで起動してみたけども、Service Workerの更新は変わらんやないか...

どうやら、

dev.classmethod.jp

PWAの動作確認はvue-cli-service buildでは行えず、Webサーバーを用意し生成物をデプロイする必要があります。 面倒なので、Web Server for ChromeというChromeのアプリを使用します。 このアプリを使うと指定されたディレクトリをルートしてWebサーバーとして動作してくれます。

Vue.jsで作ったゲームをインストール可能(PWA)にしてGitHub Pagesで公開してみた | DevelopersIO

⇧ 制約があったらしい...

ドキュメントを確認してみたけども、

cli.vuejs.org

If you are using the PWA plugin, your app must be served over HTTPS so that Service Worker can be properly registered.

https://cli.vuejs.org/guide/deployment.html#pwa

HTTPSであればService Workerが登録できるよ、たぶん、ということなんだけど、Service Workerの登録自体は、vue-cli-service buildでビルドした静的ファイルでも可能だったので、紛らわしいよね...

というか、ドキュメントの情報が少な過ぎるんよね...

mkcertとhttps-serverでHTTPSできるサーバーをローカル環境で実現する

なんか、調べてたら、「mkcert」ってものがあるらしく、

github.com

A simple zero-config tool to make locally trusted development certificates with any names you'd like.

https://github.com/FiloSottile/mkcert

⇧ ローカル環境でHTTPS通信に必要な証明書をお手軽に作成できるらしい。

thundermiracle.com

zenn.dev

⇧ 上記サイト様を参考に導入してみる。

zenn.dev

⇧ Angularの開発用サーバーだと、「mkcert」でCA証明書

まずは、「mkcert」をインストールする必要がありますが、

f:id:ts0818:20220122144808p:plain

⇧ やはりというか、「winget」だと扱ってないらしい...いつになったら「winget」がまともに機能し出すのか...

仕方ないので、「chocolatey」でインストールします。コマンドプロンプトを「管理者権限」で起動して、「mkcert」をインストールします。

f:id:ts0818:20220122145324p:plain

f:id:ts0818:20220122150144p:plain

f:id:ts0818:20220122150254p:plain

f:id:ts0818:20220122150330p:plain

で、Vue.jsを使っている場合は、「vue.config.js」に設定を追加する必要があるらしい。

bharathvaj.me

⇧ 上記サイト様を参考に「vue.config.js」を修正。

// vue.config.js

/**
 * @type {import('@vue/cli-service').ProjectOptions}
 */
const fs = require("fs");
module.exports = {
  productionSourceMap: process.env.NODE_ENV === "production" ? false : true,
  // ...other vue-cli plugin options...
  pwa: {
    name: "My App",
    themeColor: "#4DBA87",
    msTileColor: "#000000",
    appleMobileWebAppCapable: "yes",
    appleMobileWebAppStatusBarStyle: "black",

    // configure the workbox plugin
    workboxPluginMode: "InjectManifest",
    workboxOptions: {
      // swSrc is required in InjectManifest mode.
      swSrc: "src/sw.js",
      swDest: "service-worker.js",
      // ...other Workbox options...
    },
  },
  // webpack系の設定でworkboxの設定は既に含まれているらしい
  //chainWebpack: (config) => {
  //  config.plugin("workbox");
  //},
  // vue-cli-service serve(開発用サーバー)実行時にHTTPSを使用する
  devServer: {
    // [process.platform]windows:win32, mac:darwin, linux:linux
    open: process.platform === "win32",
    host: "0.0.0.0",
    port: 8080,
    https: {
      key: fs.readFileSync("../mkcert/ssl/localhost-key.pem"),
      cert: fs.readFileSync("../mkcert/ssl/localhost.pem"),
    },
    hotOnly: false,
  },
};

で開発用サーバーを起動してみる。

f:id:ts0818:20220122154247p:plain

HTTPSで起動できた、勝った!と思いきや、「保護されてない通信」とかになってしまうし、

f:id:ts0818:20220122154415p:plain

⇧「デベロッパーツール」の「console」にもエラー出てますと。

Error during service worker registration: DOMException: Failed to register a ServiceWorker for scope ('https://localhost:8080/') with script ('https://localhost:8080/service-worker.js'): An SSL certificate error occurred when fetching the script.

ということで、Google先生に確認したところ、

qiita.com

chromeオレオレ証明書を使ったhttps通信上だと、service workerを使うときに、以下のようなエラーを出します。

自己証明でhttpsしてservice workerを動かすときのchromeのオプション - Qiita

blog.splout.co.jp

Service Worker は、その強大な力を抑えるために localhost であるか、https でないと動作しないという制限が課せられています。

オレオレ証明書の入ったサーバーで Service Worker のテストをしたいとき | SPLOUT BLOG

⇧ ということらしく、え~っと、ブラウザの起動オプションで回避はできるらしい。

http-serverってモジュールだけでOKっぽい

その前に、vue-cli-plugin-pwaのドキュメントを見ると

cli.vuejs.org

If you need to test a service worker locally, build the application and run a simple HTTP-server from your build directory. It's recommended to use a browser incognito window to avoid complications with your browser cache.

https://cli.vuejs.org/core-plugins/pwa.html#configuration

⇧ なんか、そもそも、HTTPS使わなくても、HTTP-serverを起動できればテストできるって言ってますな、HTTP-serverってのが「Apache Httpd」「Nginx」といったような一般的なWebサーバーのことを言ってるのかがよく分からん...

とりあえず、npmでインストールできる「http-server」ってのをインストールする方向で。

f:id:ts0818:20220122170904p:plain

と現時点の最新版をインストールしました。

f:id:ts0818:20220122171501p:plain

⇧ 相変わらず、npmモジュールの脆弱性の数が半端ない...

HTTPS使わないことにしたので、「vue.config.js」を再度修正。

// vue.config.js

/**
 * @type {import('@vue/cli-service').ProjectOptions}
 */
const fs = require("fs");
module.exports = {
  productionSourceMap: process.env.NODE_ENV === "production" ? false : true,
  // ...other vue-cli plugin options...
  pwa: {
    name: "My App",
    themeColor: "#4DBA87",
    msTileColor: "#000000",
    appleMobileWebAppCapable: "yes",
    appleMobileWebAppStatusBarStyle: "black",

    // configure the workbox plugin
    workboxPluginMode: "InjectManifest",
    workboxOptions: {
      // swSrc is required in InjectManifest mode.
      swSrc: "src/sw.js",
      swDest: "service-worker.js",
      // ...other Workbox options...
    },
  },
  // webpack系の設定でworkboxの設定は既に含まれているらしい
  //chainWebpack: (config) => {
  //  config.plugin("workbox");
  //},
  // vue-cli-service serve(開発用サーバー)実行時にHTTPSを使用する
  devServer: {
    // [process.platform]windows:win32, mac:darwin, linux:linux
    open: process.platform === "win32",
    host: "0.0.0.0",
    port: 8080,
    //https: {
    //key: fs.readFileSync("../mkcert/ssl/localhost-key.pem"),
    //cert: fs.readFileSync("../mkcert/ssl/localhost.pem"),
    //},
    hotOnly: false,
  },
};

で、適当にファイルを更新して、npm run buildでビルドして静的ファイルを作り直します。今回は、「main.ts」ファイルのconsole.logの文言を修正で。

f:id:ts0818:20220122180604p:plain

で、npm run buildを実行。

f:id:ts0818:20220122180741p:plain

f:id:ts0818:20220122180804p:plain

で、http-serverを実行。

f:id:ts0818:20220123125559j:plain

で、「ブラウザ」で「http://localhost:8080」にアクセスし、「デベロッパーツール」の「console」タブを確認すると、

f:id:ts0818:20220122180058p:plain

⇧「vue-cli-plugin-pwa」の「workbox」が機能していることが確認できました。

「Application」タブを確認すると、

f:id:ts0818:20220122181123p:plain

⇧ 新しい「Service Worker」として、「#20126 waiting to activate skipWaiting」ってのが確認できると思います。

skipWaitingのリンクをクリックすると、新しい「Service Worker」に切り替わります。

f:id:ts0818:20220122181345p:plain

自動でskipWaitingを更新して、新しいService Workerに切り替わるようにしてくれるようにできるらしい。ただ、controllerchangeイベントが起きないというね...仕方ないからコメントアウトしたけども、リロードができんからDOMの再読み込みするためには、結局、ブラウザのリロードボタンをクリックせねばならんという何とも残念な感じになってますと。

/* eslint-disable no-console */
/**
 * Welcome to your Workbox-powered service worker!
 *
 * You'll need to register this file in your web app and you should
 * disable HTTP caching for this file too.
 * See https://goo.gl/nhQhGp
 */

workbox.core.setCacheNameDetails({ prefix: "my-project" });

/**
 * The workboxSW.precacheAndRoute() method efficiently caches and responds to
 * requests for URLs in the manifest.
 * See https://goo.gl/S9QRab
 */
self.__precacheManifest = [].concat(self.__precacheManifest || []);
//workbox.precaching.suppressWarnings();
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});

self.addEventListener("message", (event) => {
  console.log("■□■message event: " + event + "■□■");
  console.dir(event);
  if (event.data.type === "SKIP_WAITING") {
    console.log("■□■self.skipwaiting() done: " + event.data.type + "■□■");
    try {
      self.skipWaiting();
    } catch (e) {
      console.log(e);
    }
  }
});

// self.addEventListener("controllerchange", (event) => {
//   console.log("■□■controllerchange event: " + event + "■□■");
//   console.dir(event);
//   // Here the actual reload of the page occurs
//   window.location.reload();
// });

// if ("serviceWorker" in navigator) {
//   navigator.serviceWorker.ready.then((registration) => {
//     console.log("■□■■□■" + registration + "■□■■□■");
//     console.dir(registration);
//     registration.active.postMessage("ping");

//   });
// }
    
/* eslint-disable no-console */

import { register } from "register-service-worker";

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: ServiceWorkerRegistration) {
      console.log("Service worker has been registered.");
      console.log("registration: ");
      //console.log(JSON.stringify(registration));
      console.log("%j", registration);
      console.dir(registration);
    },
    cached() {
      console.log("Content has been cached for offline use.");
    },
    updatefound() {
      console.log("New content is downloading.");
    },
    updated(registration: ServiceWorkerRegistration) {
      console.log("New content is available; please refresh.");
      console.log("registration: ");
      //console.log(JSON.stringify(registration));
      console.log("%j", registration);
      console.dir(registration);
      if (registration && registration.waiting) {
        console.log("■□■postMessage send■□■");
        try {
          registration.waiting.postMessage({ type: "SKIP_WAITING" });
        } catch (e) {
          console.log("■□■【ERROR】postMessage send:" + e);
        }
        // ※無限ループでリロードされ続けるのでNG
        //window.location.reload();
      }
    },
    offline() {
      console.log(
        "No internet connection found. App is running in offline mode."
      );
    },
    error(error) {
      console.error("Error during service worker registration:", error);
    },
  });
}

で、適当なファイルに変更を加えて、ビルドしたら、http-serverを起動して、ブラウザにアクセスすると、

f:id:ts0818:20220122212611p:plain

f:id:ts0818:20220122212633p:plain

f:id:ts0818:20220122212722p:plain

Service Worker自体は最新に更新はされてるっぽい。

dev.to

⇧ 上記サイト様のように、Javascript のcustomEventとか使えば、新しいService Workerへの更新後にブラウザのリロードとかもイベント経由でいけるっぽい。

まぁ、何て言うか、「vue-cli-plugin-pwa」にしろ「workbox」にしろ、ブラックボックスな部分があり過ぎて、困ったもんですな。

あと、例の如くネットの情報も錯綜し過ぎてて、情報の正当性が担保されてない情報が多くて疲弊しますな...

本当に私の休日を返して欲しいですな...

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

今回はこのへんで。