2019-12-19

nuxt × BFF開発における技術スタックとフロントエンド構造

hiroki_hayashi

対象の読者

Nuxt.jsで新規開発を行う方

フロントエンドの技術選定に迷っている方

前提

現在、スマートショッピング/SaaS事業部では、バックエンドの大幅リファクタ及びフロントエンドのフルリプレイスを行っています。

今回、フロントエンドの技術選定及びアプリケーション設計を担当させていただきました。

12月16日現在、まだ開発中ではありますが、選定した技術スタックと設計方針と現在の実際の実装を一部ご紹介します。

技術スタック

  • JavaScript
  • Nuxt.js / SPA
  • Scss
  • Vuex
  • Buefy
  • ESlint/prettier

全てNuxt.jsのインストール時に選択できます

概ねオーソドックスな構成となっていると思いますが、実際に開発を進めていく上で感じたことを書いていきます。

JavaScript

TypeScriptとの選択は非常に迷いましたが、今回はあえてJavaScriptを選択しました。

理由としては以下の3つです。

  • 開発スピードの低下
  • Vuexとの相性の悪さ
  • TypeScriptへの移行の容易性

Nuxt.jsは軽く触ってはいましたが、TSの経験はそれほどなく、開発工数の都合上、開発スピードの低下を加味した選択です。

しかし、コード品質の担保、チーム開発での効率の高さという意味ではTSは非常に選択肢として魅力的です。

ただ、Nuxt.jsは既存のJSプロジェクトをTypeScriptに置き換えることを前提としており、移行の容易性が非常に高いため、一旦JSを選択しました。

コードが肥大化する前に、早めにTypeScriptへの移行は着手したいところです。

Nuxt.js / SPA

本来は、SSRや静的ジェネレートが強みのように見えるNuxt.jsですが、今回それらの機能は使用していません。

しかし、それらの機能を利用せずともNuxt.jsを利用する価値は大きいと思っています。

私が感じたNuxt.jsの恩恵は以下の2つだと感じています。

  • routingを意識せず実装可能
  • 特定のディレクトリの直下にファイルを配置することでstoreなどが定義可能

個人的にroutingを意識する必要がないという点が非常に大きいです。

mock作成段階でディレクトリさえ切ってしまえばroutingを一切考えることなく、そのまま本開発に移行できるため、チーム開発という点でも恩恵を受けることができました。

storeなどを利用する場合、デフォルトで作成されているディレクトリ直下に置く必要があるため、コードの可読性も高くなるのもポイントです。

Buefy

toBのIoT管理画面ということもあり、デザイン要件についてはそこまで高くない、開発スピードが求められるという条件では、UIコンポーネントは存在は非常に重宝します。

Buefyは選択した理由としては、純粋なcssフレームワークであるBulmaをベースにしているので柔軟性が高いということを評価しました。

チーム内で、css力にばらつきがある場合、担当した人によって画面の完成度が大幅に違う、ということが起きがちです。

それをだれが担当しても同一のクオリティを維持できる他、marginやpaddingなどの細かい調整もbuefyが吸収してくれるので画面設計や、mockupも非常に楽だったことから、利用する価値はあったと思います。

ただ、UIコンポーネントを利用するので、デザインにオリジナリティがでにくいのはご愛嬌というところです。

設計方針

  • Nuxt.jsのデフォルトのディレクトリ構造を踏襲する形で実装する
  • pages/components/storeを同一のディレクトリ構造に保ち、コードの可読性をあげる
  • pagesごとにstoreを定義しカプセル化することで、コンポーネント間の依 存関係を最小限にする

routing

ログイン状態に伴って、遷移先を制限する場合、vue-routerを使用する場合、ナビゲーションガードを用いるのが一般的ですが、Nuxt.jsではMidlewareによって実現します。

middleware/authenticated.js:

export default function ({ store, redirect }) {
  // ユーザーが認証されていないとき
  if (!store.state.authenticated) {
    return redirect('/login')
  }
}

次に、middlewareを適応させたいページにmiddlewareを設定します。

pages/secret.vue:

<template>
  <h1>シークレットページ</h1>
</template>

<script>
export default {
  middleware: 'authenticated'
}
</script>

今回の例では、pagesに対して指定をしていますが、共通headerなどを定義することができるlayoutsにも指定することができ、

今回のプロジェクトでは、ログイン後のheaderはすべて共通であるため、ログイン後に適応するlayoutsに対してのみauthenticated.jsを適用させています。

状態管理設計

状態管理には、Vuexを用いています。

その設計として基本的に、pages(画面)ごとに、storeも同一のディレクトリ構造しています。

利点としてはstore定義だけでそのpagesで何が行われているかを把握できるということがあげられます。

加えて、Vuexを介すことでコンポーネント間の依存性を最小限にすることできる上、より柔軟なコンポーネント設計を実現できます。

actionsの責務

今回、actions内でAPIリクエストを行なっていますが、それらをservice/daoをとして切り出す、という方法もよく見られます。

ただ、今回のプロダクトでは上記の手法は利用していません。

理由としては、BFFがデータ整形などの責務を担っているということが挙げられます。

必然的にactionsの責務はほぼAPIリクエストをし、commitするのみという状況に留まっており、見通しの悪さは現状では問題となっていません。

stateの永続化

stateはページリロードで全て消去されてしまうため、なんらかの方法で永続化する必要があります。

今回は、以下のプラグインを用いてlocalstrageに保存する形で実現しています。

https://github.com/robinvdvleuten/vuex-persistedstate

ただ、localstrageからstateに復帰するタイミングなどに癖があり、ハマりポイントが多数あるのでSSRを用いる場合は、注意が必要です。

定数及び、共通関数の定義

Vueで共通関数を定義する方法はいくつか存在します。

mixinやvue.prototypeにオーバーライドするなどが一般的かと思いますが、今回のプロダクトでは、pluginでinjectすることで定数定義及び、関数の共通化を実現しています。

plugin/pagenation.js

const getCurrentPageNum = (limit, offset) => {
  return offset / limit + 1
}

const getOffset = (limit, pageNum) => {
  let offset = 0
  if (pageNum > 0) {
    offset = (pageNum - 1) * limit
  }
  return offset
}

export default ({ app }, inject) => {
  inject('getCurrentPageNum', getCurrentPageNum)
  inject('getOffset', getOffset)
}

inject関数を用いることで、Vueインスタンスを生成しなくてもcontextを受け取ることで共通関数へのアクセスが可能となります。

vue componentからも当然アクセスすることができます。

const offset = this.$getOffset(this.limit, pageNum)

関数名の重複を防ぐため、inject関数の第一引数の先頭に$がつく形で定義されます。

Ajaxの共通化

Ajaxのライブラリにはnuxt/axiosを利用しています。

request/response時の共通処理はaxiosのinterceptersを利用しますが、Nuxt.jsではinterceptersの簡略記法が存在します。

export default function({ $axios, store, app, req, redirect, error }) {
  $axios.onRequest((config) => {
    store.commit('common/setLoding', true)
  })

  $axios.onResponse((config) => {
    store.commit('common/setLoding', false)
  })

  $axios.onError((error) => {
    if (error.response == null) {
      return
    }
    store.commit('common/setLoding', false)
    const message = error.response.data.Message
    if (error.response.status === 401) {
      redirect('/login')
      return
    }
    Vue.swal('エラー', message, 'error')
  })
}

onRequest / onError / onResponseでそれぞれのアクションの直前で呼び出されます。

上記の例では、loading処理と、エラーレスポンスの共通化を行なっています。

エラーレスポンスについては、エラーコードによって分岐させていけばさらに詳細にハンドリングが可能です。

まとめ

Nuxt.jsはドキュメントが充実しており、今回利用した機能もほぼ公式のドキュメントを読んだだけで実装したものがほとんどです。

Vueに慣れている人がNuxt.jsを使うと多少覚えることはありますが、ほとんど抵抗なく利用することができるのかなと思います。

しかし、ブラックボックス化されている部分が各所に存在しているため、要求される要件が複雑だとVueCLIのほうが都合が良い可能性もあるなと感じました。

最新の記事