エンジニア

2019.09.27

Vue.jsで作ったSPAだけどアプリケーションの更新をお知らせしたい!

Vue.jsで作ったSPAだけどアプリケーションの更新をお知らせしたい!

お疲れ様です。
MDグループの志村(@naokiur)です。
エンジニアブログ初投稿です。よろしくお願い致します。
入社して2年、私服OKな弊社ですが、
個人的な好みで、ほとんどスーツで出社しています。

フロントエンドにVue.js、
バックエンドにDjango REST Frameworkを利用し、
業務アプリケーション開発に従事しております。

この度、
リリース後、フロントエンド側のバージョンアップがあったとき、
ユーザーにどうやってお知らせするのか? の解決策を1つ、
構築したので、その話を記載させて頂きます。

前提

Angular, React, Vue.jsなど、様々なフレームワークを用いて作成される、
シングルページアプリケーション(SPA)。

そのSPAと、バックエンドにREST APIを用いたオーソドックスなシステムの動きとして、
以下のような流れがあると認識しております。

1. ブラウザがリクエストを送信し、SPAの情報をWebサーバーから取得する

ブラウザがリクエストを送信し、SPAの情報をWebサーバーから取得するイメージ

2. SPAがリクエストを送信し、APIサーバーのAPIを実行する

SPAがリクエストを送信し、APIサーバーのAPIを実行するイメージ

以降、情報のやりとりはAPIサーバーと行われます。

問題提起

月日が経ち、フロントエンド側の機能追加やバグ修正で、
バージョンがアップした場合、どのようになってしまうでしょうか?

ユーザーがブラウザを逐次閉じていて、
毎日必ず一度は、Webサーバーへアクセスしていてくれれば、
特に問題は発生しないかもしれません。

しかし、
業務アプリケーションにおいて、
ブラウザを開きっぱなしで、マシンもシャットダウンせず、
毎日の業務を行っている、ということもあるかもしれません。

その場合、以下のようになってしまう、
と考えております。

新しい機能を使えなかったり、バグが修正されていない状態で使い続けてしまったりするイメージ

調査

みなさん悩んでいると思うので、Google先生にお伺いをしたところ、
React.jsでこの問題について、コンポーネントを作成し、
対処している方がいらっしゃいました。
https://marmelab.com/blog/2016/08/29/auto-reload-spa-on-mobile-setinterval.html

ざっくりと概要をお話しすると、

・SPAのソースコードを変更して、トランスパイルすると、index.htmlの内容が変化する
・index.htmlの中身をハッシュ化した値を保持する
・定期的にindex.htmlへのリクエストを送信する
・取得したindex.htmlをハッシュ化した値と、保持した値を比較する
・比較結果が等しくなければ、ブラウザを更新するよう、ユーザーに促す

です。

これの、Vue.jsバージョンを作成しました!

実装

このコンポーネントには、以下のロジックが必要となります。

・index.htmlを取得すること
・ハッシュを計算すること
・ハッシュ値を比較すること
・リロードすること

これらのロジックを、コンポーネントの methods()に定義し、
created() で、 setInterval()によって、定期的に実行する、
という寸法です。

created() {
    setInterval(this.fetchServer, 1000 * 60 * this.normalizedFrequencyMinutes);
},

また、ユーザーに通知するためのHTMLも書いておきます。
isChangedがtrueになったら表示して、ユーザーに更新を促す、という内容です。

<template>
    <div id="notification" v-if="isChanged>
        <button class="cancelButton" @click="cancel">×</button>
        <span class="notificationTitle">更新通知</span>
        <span>アプリケーションの更新を確認しました。更新しますか?</span>
        <button class="okButton" @click="reload">OK</button>
    </div>
</template>

index.htmlを取得すること

fetchメソッドを用いて、index.htmlの内容を取得します。
Promiseが返ってくるので、成功したときにハッシュ値を比較するようにします。

fetchServer() {
  fetch(this.normalizedUrl)
    .then(res => {
      if (res.status !== 200) {
        throw Error('Cannot access server.');
      }
      return res.text();
    })
    .then(html => {
      const hash = this.createHash(html);
      this.judgeHash(hash);
    })
    .catch(err => console.log(err));
},

ハッシュを計算すること

与えられた文字列のハッシュ値を計算します。
このメソッドに、index.htmlの内容をすべて渡し、
ハッシュ値を計算してもらう、という寸法です。
StackOverFlowの記事を参考にさせて頂きました。

createHash(str) {
  const len = str.length;
  let hash = 0;
  if (len === 0) return hash;
  let i;
  for (i = 0; i < len; i++) {
    hash = ((hash << 5) - hash) + str.charCodeAt(i);
    hash |= 0; // Convert to 32bit integer
  }
  return hash;
},

ハッシュ値を比較すること

data()に 用意しておいた `previousHash` と、
計算したハッシュ値を比較します。
差分があれば、新たなハッシュ値として、
計算したハッシュ値を保持します。

isChangeをtrueにしたので、
前述の通知用HTMLが表示されます。

judgeHash(hash) {
  if (!this.previousHash) {
    this.previousHash = hash;
    return;
  }
  if (this.previousHash !== hash) {
    this.isChanged = true;
  }
},

リロードすること

location.reloadメソッドをすることで、
ブラウザの更新を実行します。
trueを渡して、キャッシュを使用せず、Webサーバーから取得してもらいます。

reload() {
  location.reload(true);
},

サンプル

おなじみかと思いますが、
Vue CLIでcreate projectした際に作成されるアプリケーションに、
上記のコンポーネントを追加しました。

buildして、S3のWebサイトホスティングで、
ホストしています。

このときのindex.html(の一部)は、以下のようになっています。

「Welcome to …」の部分を修正し、再buildします。

index.htmlの内容が変わったことがわかります。
そして、S3に配置し、しばらくすると…

でました!!
「OK」を押すと…

リロードされ、「Welcome to …」の部分が変わりました!!

結び

SPAにおける課題の1つを解決できたかなと思います。

フロントエンドエンジニアとして自分はまだまだぴよぴよですが、
より一層、課題の解決に取り組み、
成長していきたいと思っております。

そしてハンズラボ、Vue Fes Japan 2019でシルバースポンサーをさせていただきます!
ブースも出店させていただきますので、
ご来場の際はぜひお立ち寄り下さい!!

一覧に戻る