この記事はLCL Advent Calendar 2020 - 2日目の記事です。
バックエンドエンジニアの染谷です。 主にRuby on Railsの保守開発をやっていますが、ここ1年ほどフロントエンドの仕事も増えてきて、最近はRubyの文法を忘れつつあります。
さて、LCLではバスツアー検索サービスを運営しており、一部のツアー会社様向けにデータ管理機能(以下、バスツアーBiz)を公開しております。
バスツアーBizとは
バスツアー検索サービスでは、多くのツアー会社様からデータを受領し、サイト上で公開しています。 データの入稿方法は現在下記の2種類があります。
- CSVファイルによる「ファイル入稿」
- バスツアーBiz画面からの手入力入稿
バスツアーBizは、ご契約いただいたツアー会社様のみご利用いただけるサービスで、Webサイトにログインし、自社のツアー情報を管理することができます。 このアプリケーションは、Nuxt.jsのSPAモードで構築されており、APIでバックエンドサーバ(Rails API)と通信する比較的モダンな構成を取っています。
Nuxt.jsを採用した理由
そもそもReactでなくVueのフレームワークであるNuxt.jsを選んだのは、
Railsエンジニアでも理解しやすそう
というのが理由でした。 3年ほどRailsを実装してきた私としては、
- ディレクトリ構成をあらかじめ決めておいてほしい
- 設定ファイルだけでいい感じに動く仕組みがほしい
- 昔からある「1ファイル内でHTML, CSS, JSのタグを分けて書く」という形式が良い(erbの名残で)
- JSX怖い
という理由でNuxt.jsはRailsエンジニアがフロントエンドのスキルを新しく習得する上でスタートを切りやすいイメージがありました。 実際Nuxt.jsを使ってローンチまでこぎつけたサービスは初めてでしたが、理解が容易でフロントエンドの知識レベルがそこまで高くなくても実装が進められるフレームワークでした。 とはいえ、1人でアプリケーション開発を進めていた手前、色々と実装上「省エネ」する必要がありました。 実際に開発で使った「省エネテク」を1つご紹介します。
省エネテク:computedを動的に作る
バスツアーBizのメイン機能である「ツアー情報の手入力入稿機能」は、とにかく入力フォームが多く、約70項目前後入力する必要があります。 1ページですべての項目を入力するのはUI的に微妙なので、入力内容の種別に合わせて「①掲載情報」から始まる7種類のフォームをナビゲーションタブで切り替えられるよう分割しました。 こんな感じ↓
実装レベルでは、当然1つのVueファイルにすべてのフォームを実装するわけにはいかず、必然的にフォームのコンポーネントは細かく設計することになりました。 フォームデータはまとめてAPIでPOSTするのですが、 このフォームデータをVue.jsのpropsとemitを使ってコンポーネント間でバケツリレーするのは限界があります。
そこでフォームデータをグローバルなVuexストアで管理することにしました。
フォームをVuexストアで管理する
Vuexストアとは、どのコンポーネントからでもアクセスできるグローバルな変数のことです。 つまり、
- フォームの項目が変更される
- Vuexストアの値が更新される
- POSTする際は、Vuexストアにプールしたフォームデータをまとめて投げる
といった実装が可能になり、考え方がシンプルになります。 フォームの管理が1箇所になるので、修正も容易になります。
一度登録したツアー情報を編集する際は、APIから取得したデータをあらかじめフォームにセットしておく必要があります。
- APIからデータを取得
- Vuexストアにセット
- 各フォームにセットする
といった流れです。 フォームとVuexストアをリンクさせればうまくいきそうですね。 Vuexの公式ページに紹介されていますが、これには2種類の方法があります。
其の1:changeイベントでストア更新
template
<input :value="message" @input="updateMessage">
js
computed: { ...mapState({ message: state => state.formData.message }) }, methods: { updateMessage (e) { this.$store.commit('updateMessage', e.target.value) } }
こんな感じでフォーム1個に対して、computedとmethodsを実装していく感じです。
其の2:双方向算出プロパティ(getter・setter)
computedにv-modelのプロパティを用意し、getメソッドでVuexストアからvalueを取得、setメソッドでVuexストアを更新する方法です。
template
<input v-model="message">
js(component)
computed: { message: { get () { return this.$store.state.formData.message }, set (value) { this.$store.commit('updateMessage', value) } } }
js(vuex store)
updateMessage(state, payload) { state.formData.message = payload },
こんな感じでフォーム1個に対して、computedのgetter・setterを実装していく感じです。
computedのgetter・setterを動的に作る
70個のフォームに対してメソッドやらgetter・setterを実装する・・・
キーが違うだけで70個も・・・
こんな冗長な実装絶対書きたくない!
実際、実装量が増えると可読性が低下しますし、何より同じような実装をなんども繰り返し書くのは大変です。
記事を漁りに漁ったところ、Vue.jsのライフサイクルフックにはbeforeCreateというオプションがあって、コンポーネントが生成される前に処理を入れることができることがわかりました。 動的にcomputedが実装できるようなので、方針としては 「其の2」 の延長です。
js(component)
beforeCreate() { this.$options.computed = { ...this.$options.computed, message: { get() { return this.$store.state.formData.message }, set(value) { this.$store.commit('updateMessage', value) }, }, } }
といった感じです。 これは前述とまったく同じ振る舞いになります。 さらに、これを汎用的なmutationメソッドに対応させていきます。
template
<input v-model="message1"> <input v-model="message2"> <input v-model="message3">
js(component)
beforeCreate() { const keyNames = ["message1","message2","message3"] keyNames.forEach((keyName) => { this.$options.computed = { ...this.$options.computed, [keyName]: { get() { return this.$store.state.formData[keyName] }, set(value) { this.$store.commit('updateFormData', { keyName, value, }) }, }, } }) }
js(vuex store)
updateFormData(state, payload) { state.formData[payload.keyName] = payload.value },
これでVuexストアとフォームのリンクについて効率的に実装できるようになりました。
今後仕様が変更されてフォームが増えたとしても、keyNames
に追加していくだけです。
まとめ
いかがだったでしょうか。 Nuxt.jsを触った所感としては、デバッグまわりは充実していないな、という印象でした。 とくにストアまわりにバグが潜んでいると、トンチンカンなエラーが頻発します。
この辺はある種「慣れ」が必要です。 どの言語・フレームワークにもこういう「慣れ」は必要かと思いますが、私はこれを「友達になる」と表現しています。
Nuxt.jsはVuexまわりで多少苦戦はしたものの、比較的早く友達になれたな、という印象です。 2年前にReactを触ったときは、全然友達になれなくて挫折した経験があります。
最近になってまたReactのアプリケーションを開発していますが、昔よりだいぶ実装しやすくなった実感があります。
とはいえ、フロントエンド未経験者がTypeScriptやHookなどを1から覚えるのは結構大変です。 Reactをこれから仕事で使っていこうという方にとっても、Nuxt.jsはフロントエンド開発のイロハを知るための入門編としてオススメです。