ElevenBack LLC. Engineering

ElevenBack LLC. の開発者ブログです。

inject と Headless Vue インスタンスを活用したリアクティブな認証管理

この記事は Nuxt.js アドベントカレンダー 24 日目の記事です。

ここ一年ほどほとんど Nuxt.js で Vue.js を単体で使うことがめっきり減った @potato4d です。

今回はニッチな話題として、「JavaScript の世界のオブジェクトに Vue.js のリアクティブ機構をもたせる」という話をしたいと思います。

なお、今回はややこしいコードベースを省く意味でも Nuxt.js 環境を前提とします。

実現したい要件

まずは実現したい要件を定義します。
今回は、現在弊社にて開発中の Web サービスのシステムをベースとして考案します。

  1. Firebase Authentication を使ってユーザー情報をやりとりする
  2. データベースヘの取得・保存操作では、 Firebase 側のユーザーの uid を利用するためグローバルから認証情報にアクセスできてほしい
  3. できればその他の Firebase の機能も、一度初期化した情報を使いまわしたい

と、たったこれだけです。

つまりは /users/:uid/resources のようなエンドポイントへのアクセスを可能にする為にも uid はグローバルで引き回せると嬉しい。というわけですね。

Vuex を利用した認証設計の落とし穴

さて、こういった要件を実現する際、みなさんはどういった形で設計するでしょうか。

真っ先に考えられるのが、~/sdk/firebase.js のようなファイルを作った上で、 ~/store/user.js といった形で Vuex の名前空間を用意する方式ではないでしょうか?以下のようなコードです。

import * as firebase from '~/sdk/firebase'

export const state = () => ({
  user: null
})

export const getters = {
  user: state => state.user
}

export const mutations = {
  userUpdated(state, { user }) {
    state.user = user
  }
}

export const actions = {
  applicationInit({ commit }) {
    firebase.auth().onAuthStateChanged((user) => {
      commit('userUpdated', { user } )
    })
  }
}

確かにこれ自体も要件は実現できますし、ポピュラーな実装なのでわかりやすいかもしれません。一方で、実装が Firebase に大きく依存していること、Vuex 内で独自のライフサイクルが生まれており関係を考慮しづらいことなど、いくつか実装上の課題点があります。

また、こういった外部由来のものが Vuex に入ってくるとテスタビリティの観点でも微妙ですね。Nuxt.js の場合、store ディレクトリ内で分割もしづらいのでなおさらです。

inject による依存性注入を実現した明快な状態設計

そこで今回は、inject と Headless な Vue インスタンスを使って、 Vuex に依存しない明快な認証の管理を実現しています。

inject については、 Nuxt.js を使っていても知らない人も多い機能ではないでしょうか。これはその名の通り、 Vue インスタンスへの注入が可能な関数であり、現状 plugins 内にて利用が可能です。

直接使ったことがない人でも、 axios-module を入れると this.$axios や app.$axios が使えるようになることで、多くの人が体験はしているはずです。inject は、任意の Javascript のオブジェクトや値を、Vue のプロトタイプおよび Nuxt.js の Context オブジェクトへと注入してくれます。

React でいう Context のような概念となり、外部注入可能なグローバルオブジェクトというような認識で利用が可能となります。

もちろん、アプリケーション全体で利用可能であるため細心の注意を払う必要はありますが、認証情報を管理する上では非常に有用です。

実際に Nuxt.js で書く場合はこんな感じでしょうか。

import Vue from 'vue'
import * as Firebase from '~/sdk/firebase'

const FirebasePlugin = (context, inject) => {
  inject('firebase', Firebase.firebase)
  inject('app', Firebase.app)
  inject('firestore', Firebase.firestore)
  inject('storage', Firebase.storage)
  inject('functions', Firebase.functions)
  inject('auth', Firebase.auth)
  inject('currentUser', Firebase.auth.currentUser)
}

export default FirebasePlugin

これで this.$currentUser が firebase.auth.currentUser に繋がってくれました。これを利用することで、アプリケーションのコンテキストとして認証情報を利用することが可能となります。

inject の穴を埋める Headless Vue インスタンス

inject も利用できて一件落着……かと思うと、最後に一つ課題が残っています。実は、inject したデータはいずれも Vue インスタンスのプロパティではないため、自動的にリアクティブにはなってくれないのです。

つまり、 this.$currentUser は SPA の初期化時にだけ動くようになっていしまいます。

Firebase Authentication のような SDK で遅延認証するタイプの認証基盤の場合、SPA の起動時はユーザー情報が null であり、ひと呼吸おいてから認証が完了することが少なくありません。そして、そういった場合にユーザーがずっと空のままになってしまうのはアプリケーションとして必要な要件を満たせていないことになります。

こういった時に使える便利なテクニックが、 Getter を利用した、 render を伴わない Vue インスタンスの組み合わせです。Vue 2.5+ の Vue.observable でも類似機能は実装可能ですが、こちらがよりわかりやすく記述できるのではないでしょうか。

import Vue from 'vue'
import { Plugin } from '@nuxt/types'
import * as Firebase from '~/sdk/firebase'

const _auth = (Firebase.auth as any) as firebase.auth.Auth & {
  __defineGetter__: any
  _vm: any
}
if (!_auth._vm) {
  _auth._vm = new Vue({
    data() {
      return {
        currentUser: _auth.currentUser !== null ? _auth.currentUser : undefined
      }
    },
    created() {
      _auth.onAuthStateChanged((user: any) => {
        this.currentUser = user
      })
    }
  })
  _auth.__defineGetter__('currentUser', () => {
    return _auth._vm.$data.currentUser
  })
  _auth.__defineGetter__('user', () => {
    return _auth._vm.$data.currentUser
  })
}

const FirebasePlugin: Plugin = (context, inject) => {
  inject('firebase', Firebase.firebase)
  inject('app', Firebase.app)
  inject('firestore', Firebase.firestore)
  inject('storage', Firebase.storage)
  inject('functions', Firebase.functions)
  inject('auth', _auth as firebase.auth.Auth)
}

export default FirebasePlugin

ポイントは _vm を持つ部分です。 Vue.js のインスタンスとして認証情報を管理し、そのうえで auth 自体の Getter として currentUser を追加。そこから Vue インスタンスの状態を参照指定ます。

こうすることで、認証情報そのものが Vue インスタンスによってリアクティブな値となり、同じく Vue インスタンスとなるコンポーネントともリアクティブに連携できます。

つまり、 this.$currentUser が自動で更新されるようになるわけです。

これで独立したライフサイクルを、最終的な値にだけ関心を持って取り扱えるようになりました。

こうすることで、Vuex に影響を与えずクリーンかつ、テスト時は this.$currentUser が目的のデータとなるようにモックするだけで OK な、シンプルな構造が完成します。

実際に、弊社 Candy ではこの方式で認証情報を管理していますが、トリッキーな記述に抵抗がない場合、 Vuex と比較して取り回しやすさが段違いでおすすめできます。

まとめ

まとめると

  1. グローバル管理といえば Vuex になりがちだけど意外と不要なシチュエーションも多い
  2. グローバルかつ実行時の環境に依存するものは inject で prototype を拡張してやるとテストしやすくて便利
  3. Headless な Vue インスタンスを作ることで、ただの JavaScript のオブジェクトやインスタンスにリアクティブ性を付与できる

というところでしょうか。

認証情報など、微妙なグローバルさによって成り立っているものはハンドリングが難しいところですが、うまく扱ってやることでより効果的なコード設計の発見に繋がりやすい部分でもあるため、特定のやり方に固執せず、良い塩梅を探すことが大切だと考えています。

Firebase Meetup #15 にて弊社代表がセッションを行いました

CEO の id:potato4d です。

本日、Google Japan 渋谷オフィスにて開催された「Firebase Meetup #15」にてセッション登壇者を務めさせていただきました。Firebase Japan User Group 主催のミートアップであり、記念すべき二周年を迎えるイベントのメインセッションを行うことができてありがたい限りです。

firebase-community.connpass.com

登壇内容について

「Firebase & Google Cloud によるサーバーレス帳票管理」というタイトルで、弊社で運営中の SaaS である「Candy」のアーキテクチャについて紹介させていただきました。

帳票管理という原本の保存がシビアかつ、成果物がレガシーなドメイン領域において、いかに課題をサーバーレスで解決したのか、メリットはどこかということを含めて紹介いたしました。アーキテクチャとしては、Nuxt.js 製の SPA からはじまったリクエストが NestJS による BFF を経て、Cloud Pub/Sub、Functions、Storage、Firestore を経由し、最終的に SPA へと戻っていくような構成を作った理由が中心となります。

詳細が気になるかたは、スライドをご参照ください。

また、オフィシャルにてアーカイブ動画も公開されておりますので、ご興味のあるかたはこちらもご覧ください。

www.youtube.com

イベントについて

今回は Cloud Functions Day という話でしたが、年末の LT 大会らしく、後半の LT では Firebase の様々な課題へのアプローチが見れて楽しいイベントでした。特に私の周りでは Web での Firebase 利用が支配的なので、アプリの事情も聞けたのは個人的に新鮮な体験でした。

あと、懇親会で出てきたケーキがなかなか良かったですね。私もイベントを運営する立場でもあるので、何かの記念の際はケーキを作ってみたいかも知れません。

おわりに

最近頻度が落ちていた純粋な技術トークをできて個人的には非常に楽しいイベントでした。ちょっと早足になってしまったのが反省点なので、ドメイン領域についてはもう少し薄くすべきだったかも知れません。

次回はその辺りの反省を活かしていけると良いかなと思います。お話しいただく機会を頂いた k2wanko さん並びに FJUG のかたがた、貴重な機会をありがとうございました。

NestJS Meetup Tokyo #1 で登壇しました

エンジニアの id:ci7lus です。2019/11/29 に、株式会社エウレカ様にて NestJS meetup Tokyo #1 が開催されました。ショートセッションの登壇者としてイベントに参加したため、ブログにて詳細を共有いたします。

NestJS Meetup Tokyo #1 について

NestJS Meetup Tokyo #1 は、TypeScript ベースの Web アプリケーションフレームワーク「NestJS」について知見を共有するためのミートアップです。

国内ではまだまだ事例の少ない NestJS を活性化する目的で開催されたミートアップで、NestJS Japan Users Group としては初のミートアップ開催とのことです。

登壇内容について

今回私は「NestJS + Passport と Firebase Auth で宣言的な JWT 認証を実装する」というタイトルでショートセッションを行いました。現在弊社で運営している会計管理クラウド Candy の事例をもとに、NestJS と Firebase をどのように組み合わせるかという内容となります。

具体的には、NestJS の Auth Guard の機能と Firebase Authentication を連携する方法を紹介。NestJS で宣言的に認証を定義する嬉しさ、実装でのハマりなど、現場でのユースケースをもとにを共有させていただきました。

なお、スライドは以下からご覧いただけます。実装が気になるかた、同様の課題を抱えるかたの学びと慣れば幸いです。

おわりに

なにかのイベントに対して登壇するというのは初めての経験で非常に緊張しましたが、とにかく無事に発表できてよかったです。

また、参加していただいた皆さんのツイートやフィードバックもとても参考になりました。ありがとうございました。

FRONTEND CONFERENCE 2019 協賛レポート

合同会社ElevenBack 代表の id:potato4d です。去る 11/02(土) 、グランフロント大阪にて FRONTEND CONFERENCE 2019 が開催されました。 弊社はスピーカー・スポンサーとしてイベントに参加。私とイベントの記録用スタッフの 2 名の記録から、スポンサー・スピーカーとしての活動を本ブログにてレポートします。

スポンサーパンフレットの配布

当日来場すると、来場者・登壇者・協賛企業全員に手提げかばんが配布されました。イベントに関するパンフレットのほか、各種ノベルティが同梱。当日会場を回る時は、パンフレットを見ながら移動していく形です。

弊社からは企業紹介のフライヤーを配布しました。

f:id:potato4d:20191108143440j:plain
当日会場にて配布したパンフレット

余談ですが、今回のフライヤー制作はラフデータを Figma、入稿データを Sketch と UI 制作向けのツールで仕上げてあります。印刷データといえば Illustrator で作成し、.ai ファイルで入稿するのが一般的ですが、いくつかの事業者は PDF 入稿を受け付けているので、こちらを利用すると便利です。

f:id:potato4d:20191108145121j:plain
同じく Sketch で制作した名刺の例(役職は氏名はマスクしています)

通常弊社では、名刺やフライヤーなどを全てまとめて ラクスル を利用しています。今回も利用しました。

スピーカー登壇「私たちはなぜ SPA で開発するのか」

また、今回はスポンサーと同時に私もいちスピーカーとしてイベントに参加しました。タイトルは「私たちはなぜ SPA で開発するのか」。 弊社も基本的に SPA / API を中心として開発・請負を行っている会社ですが、SPA を実装する際の技術選定や、選んだ技術への責任、将来に渡って開発を続けるにあたって考えるべきことについて話しました。

f:id:potato4d:20191108143159j:plain

スライドは以下にて公開されています。SPA で日常的に開発を行いながらも、昨今の静的サイトジェネレーターや SSR の流行、フルスタックな開発アーキテクチャの流行に疑問を持っている方も多いかと思います。今回は、そんな方にとって一つの回答となるようなスライドを目標として作成しました。心当たりがあるかたはぜひ。

最後に

合同会社ElevenBack では、SPA のプロフェッショナルによる技術開発や顧問業の依頼を幅広く募集しています。 既に SPA 開発を行っていてアーキテクチャに悩んでいる方はもちろん、段階的な移行や社内リテラシーの向上まで、週1時間から可能となっておりますので、ご興味のあるかたは Web サイトのお問合せフォームよりご連絡ください。

https://elevenback.co.jp

FRONTEND CONFERENCE 2019 へ協賛・弊社代表が登壇いたします

ElevenBack LLC 代表の id:potato4d です。社内の開発ではフロントエンドとユーザーインターフェース、そしてクラウド周りを中心に担当しています。

今回は私 potato4d こと花谷が、来たる11月2日に開催される「FRONTEND CONFERENCE 2019」にて、スピーカーとして登壇することが決定。併せてシルバースポンサーとしてイベントに協賛することになったことのご報告です。

FRONTEND CONFERENCE 2019 について

FRONTEND CONFERENCE は、2016年から大阪で継続的に開催されているフロントエンドのカンファレンスです。関西地区として最大級、規模でいうと東京での大型カンファレンスにもひけをとりません。

毎年関西に限らず、全国各地から第一線で活躍するフロントエンドエンジニアがトークを行うフロントエンドの祭典となっており、個人的にはフロントエンド専任だけでなく、デザインなどのコンテキストを持つ人も登壇する空気感に魅力を感じていたりします

私個人の話としては、 2017 年にも一度登壇。前回は基調講演だったので汎用的な講演をしていたのですが、今回はAトラックの終盤なのでパワー全開で話そうと思います。

トーク内容について

当日は「私たちはなぜ SPA で開発するのか」というタイトルにて登壇予定です。

まだ資料が Fix していない状態なので暫定ですが、トラディショナルな Web アプリケーションから SPA / API スタイルでの開発によって得られたものと失ったもの。 今ではデファクトとして採用されている SPA って、そういえばなんで SPA じゃないといけなかったんだっけ?といった疑問に立ち返り、今一度 SPA の必要性について考えてみます。

パフォーマンスのために SPA を採用する人、UI の改善よるユーザー体験の向上のために SPA を採用する人、はたまた、開発の DX のために SPA を採用する人。それぞれ違う目的から、同じ結果へとたどり着くのはなぜなのか。

SPA を当たり前に使いながらも、その実今のフロントエンドに疑問を抱いている方が、納得感を持って明日の開発へと進められるようなヒントを与えることを目的としています。

2019.kfug.jp

Roppong.vue #2 にて少し話したときのコメント

スポンサーについて

また、今回は併せてイベント協賛として、スポンサー枠にも登録しています。

シルバースポンサー枠となりますが、当日はスピーカー・スポンサー両面からイベントを支援させていただきます。印刷物も発注しており、当日会場にて弊社の紹介資料も頒布いたしますので、見かけたらぜひツイートしてください!

おわりに

FRONTEND CONFERENCE 2019 は 11 月 2 日に、大阪にて開催されます。

SPA が当たり前になった今、改めてなぜ私達が SPA で開発しているのか。会場で一緒に考えてみませんか?

2019.kfug.jp