RAKSUL TechBlog

ラクスルグループのエンジニアが技術トピックを発信するブログです

フロントエンドでもForceUpdateがしたい

こんにちは。 物流サービスのハコベルで一般貨物 (普通トラックを使用して荷主の荷物を運送する業界)向けのサービス「ハコベルコネクト」のフロントエンドを担当している丸山です。

サービスの開発・運用している中で、フロントエンドのコードが古いまま使用してしまっているユーザーがいて、APIの変更と食い違い困ったことなどないでしょうか。今回は我々がその問題に直面して考えた、強制アップデートする方法をご紹介します。

背景

ハコベルのフロントエンドでは Vue.js を使用して、遷移やリロードを行うことなくタブの移動や絞り込み、案件の受注などの操作をできるようにしています。便利さの一方で、長時間にわたって一覧ページをリロードしないでも使い続けることができるため、JavaScriptのコードが古いまま使っているケースがあります。 また、サーバーサイドの変更のため、エンドポイントやレスポンスの形式を変える必要がある場面もあります。その際に、古いフロントエンドのコードと新しいAPIのレスポンスの組み合わせによりエラーが発生してしまうという課題がありました。

そういった事態が稀に起きていたので課題ではあったのですが、先日APIを大幅に変更する必要がありその機会に手を入れようということで、クライアントサイドを最新のバージョンにアップデートさせる方法を検討することにしました。

検討

実現するための方法としては以下の2つが出てきました。

  • ログインセッションの有効期限を短くして強制ログアウトさせる
  • HTTPヘッダーなどを活用して強制リロード機能を作る

セッションの有効期限を変更する方針は、API変更をリリースするタイミングで同時にセッションを無効にし、有効期限を短くするという方法です。 しかし、頻繁に再ログインが必要となりユーザビリティを損ねてしまうこと、セッションを無効にすることでユーザーが普段と違う挙動を感じてしまい問い合わせの増加することなどが懸念としてありました。

HTTPヘッダーを利用する方針であれば、リリースのタイミングに合わせて強制アップデートを有効にすることができます。 またリロードの必要がないような小さい変更であればversionを変更しないといった柔軟な運用が可能です。ずっとリロードせずに使っているユーザーには届かないという課題はありますが、APIの変更前に一定期間を置くことで浸透させることは可能だと考えました。そのバランスからHTTPヘッダーを利用する方針を採用することとしました。

またこの問題を、採用プロセスで技術ディスカッションのトピックに採用したこともありました。 採用の中でリアルな悩みに触れてもらい、こちらとしても熱の入るトピックで、採用の場でしたがこちらも刺激を受ける内容となることが多かったです。その際の候補者の皆さまへの感謝もこの場を借りて伝えさせてください。

実装

方針は決まったのでフロントエンドとサーバーサイドをそれぞれ実装していきます。 HTTPヘッダーで受け渡しするバージョン番号は、Semantic Versioning のフォーマットに則っても良いのですが、

  • Major, Minor, Patch の共通認識・ルール整備が必要
  • 今回の要件としては、強制アップデートが必要かがわかればよい

などの理由で見送りました。今回はUNIXタイムスタンプを使い強制アップデートの判定をすることにしました。

サーバーサイド

サーバーサイドではバージョン番号の設定、アップデートが必要かの判定を行います。リクエストヘッダーに X-Hacobelltms-Version からバージョン番号を取り出します。そのバージョン番号とサーバーに設定されたバージョン番号を比較し、更新が必要と判定されれば X-Need-Updatetrue を設定します。

def set_update_flag
  version = request.headers['X-Hacobelltms-Version'].to_i
  response.headers['X-Need-Update'] = true if version < FORCE_UPDATE_VERSION
end

フロントエンド

API通信部分には axios を使っているので、axios の interceptors を使用してheader周りの処理を行います。

まずリクエストを送信する場合。versionを定数に定義しておき、interceptors.request でリクエストヘッダー X-Hacobelltms-Version にその値を設定します。

// versionを定義(UNIXタイムスタンプ形式)
const CURRENT_VERSION = Math.floor(new Date(2020, 6, 1, 0, 0, 0).getTime() / 1000)

// versionをheaderに付与する interceptor
const attachRequestHeaders = config => {
  config.headers = { ...config.headers, 'X-Hacobelltms-Version': CURRENT_VERSION }
  return config
}

そしてレスポンルを受け取った時の処理も実装します。サーバーサイドでアップデートが必要と判定されていたら、レスポンスヘッダーに X-Need-Update = true が設定されています。interceptors.response で判別して、強制的にリロードを行うようにします。

// x-need-updateが付与されていればリロードする interceptor
const forceUpdateIfNeeded = response => {
  if (response.headers['x-need-update']) location.reload()
  return response
}

このようにして作成した interceptors を axios に設定すれば準備完了です。

axios.interceptors.request.use(attachRequestHeaders)
axios.interceptors.response.use(forceUpdateIfNeeded)
export const client = axios

今回はリロードしないとサービスを使うことができないので自動でリロードされるようにしています。リロードする部分については一度ダイアログを表示するなどした方がユーザーにとって親切かもしれませんね。 また、モバイルアプリのように継続利用はできるけどアップデートしてほしい場合に、強制的なリロードではなく選択できるようにしてもいいかもしれません。

結果・まとめ

このようにして強制アップデート処理を作成しました。ユーザーが強制アップデート機能を含んだ JavaScript のコンテンツを使用するようになったら、APIの変更など破壊的な変更のあるリリースを安心して実行できます。今回はAPIの変更に先行してリリースしましたが、プロダクトのローンチ時に仕込んでおくとさらに良かったですね。

ハコベルではユーザー環境で実際に正しくアップデートが行われるかをウォッチするためにSentryでSlackに通知を送るようにしていました。我々の期待通り、APIの変更をリリースした翌日にアップデートが行われていました。

SPAなどモダンフロントエンドを使っているとUI/UXを向上させやすい一方で、実際にリリースして改善を続けていく中でユーザーの使っているリソースが古くなるという問題に遭遇しました。 フロントエンドにこだわったサービスが増えてきているので、このようなサービスを続けていく中での知見がもっと増えていくといいなと思います。 ぜひ皆さんも悩み・ノウハウがあれば積極的に共有して行きましょう! 読んでいただきありがとうございました!