Vue.js での開発はいくつか経験したことがあるが、Nuxt.js は触ったことがなかったので入門してみた。
SSR やユニバーサルといったところは置いておいて、ひとまず簡単な SPA を作ってみてメモや雑感をまとめました。
以下は nuxt@v1.0.0-rc11
での内容になります。
作ったもの
Civirization ボードゲームの技術ツリーを管理・閲覧するためのツール。
もともと2年ほど前に Angular + Firebase で作っていたものをベースに、Vue.js + Nuxt.js + VuexFire で書き直したものです。
ゲームの内容はさておき、各自が手元のスマホで操作するとリアルタイムに技術ツリーが同期・共有される。
認証やバリデーションなど細かいところは詰めてないですが、身内で使うには十分かなと。
Nuxt.js とは
詳しくは公式サイトに任せるとして、ざっくり説明すると
- Vue.js 製のフロントエンドフレームワーク
- SSR対応
- ルーティングやストアの自動生成などを行う
- ES6/ES7 のトランスパイル
- SASS, LESS, Stylus などの CSS プリプロセッサのサポート
などがあります。
Nuxt.js はソースコードのバンドルに Webpack を利用しており、CSS プリプロセッサや babel-loader を内包しています。
これらの設定は Nuxt.js が隠蔽しており簡単に使い始めることができます。
(適宜設定を追加することも可能です。)
Firebase と VuexFire
Firebase は BaaS / mBaaS と呼ばれるアレ。
なにやらたくさん機能があるけど、今回使ったのはリアルタイムデータベース (RTDB) のみ。
誰かがデータを更新すると自動で他のユーザにも更新が反映される。
VuexFire は Firebase の Vuex バインディング。
データベースが更新されると、その内容を自動で Vuex のストアに反映してくれる。
※双方向バインディングではない点に注意。詳細は後述。
画面遷移
今回は以下の2ページ構成にしています。
- Index (トップページ: 開始ボタンがあるだけ)
- Game (ゲーム画面: 技術ツリーを表示)
Game 画面はクエリパラメータから gameKey
を受け取ります。
pages ディレクトリは以下のとおり。
/ROOT/
└ /pages/
├ index.vue
└ game.vue
Nuxt.js では、ファイルの配置によって自動的にルーティングが生成されます。
今回は Github Pages 上で公開する予定だったので、静的なルーティングのみ利用しています。
Vuex と VuexFire の設定
ドキュメントに従い、Vuex ストアをモジュールモードで設定します。
store/index.js
を作成し、store
, mutations
, actions
を export するだけで Vuex ストアが自動生成されます。
あわせて VuexFire の設定も行います。
mutations に firebaseMutations
を含め、firebaseAction
の bindFirebaseRef
でバインドする state のキーと Reference
を設定します。
ソースコードの抜粋でわかりにくいですが、LOAD_GAME
アクションを実行すると RTDB の games/<gameKey>/players
が state.players
にバインディングされるようになっています。
import { firebaseMutations, firebaseAction } from 'vuexfire'
import db from '~/plugins/firebase'
import { SET_GAME_ID } from './mutation-types'
import { LOAD_GAME, SET_PLAYERS_REF } from './action-types'
const gamesRef = db.ref('games')
export const state = () => ({
gameId: null,
players: [],
})
export const mutations = {
...firebaseMutations
}
export const actions = {
async [LOAD_GAME] ({commit, dispatch}, gameKey) {
const gameRef = gamesRef.child(gameKey)
const gameSnapshot = await gameRef.once('value')
const game = gameSnapshot.val()
if (game && ('players' in game)) {
commit(SET_GAME_ID, gameRef.key)
dispatch(SET_PLAYERS_REF, gameRef.child('players'))
return true
}
return false
},
[SET_PLAYERS_REF]: firebaseAction(({bindFirebaseRef, commit}, playersRef) => {
bindFirebaseRef('players', playersRef)
}),
}
import firebase from 'firebase'
const config = {
databaseURL: 'https://civbg-support.firebaseio.com/'
}
if (firebase.apps.length === 0) {
firebase.initializeApp(config)
}
export default firebase.database()
データの更新
VuexFire は Firebase → Vuex への片方向バインディングのみを行います。
そのためデータの更新は state の更新ではなく、Firebase API を直接叩いて行う必要があります。
おそらくトランザクション制御や一括書き込みなどを Vuex で表現できないので、あえて片方向にしているんだと思います。
#最初、双方向バインディングだと思い込んでいてハマりました…。
export const actions = {
...
async [ADD_TECH] ({state, commit}, {player, level, techId}) {
const playersRef = gamesRef.child(state.gameId).child('players')
await playersRef.child(player['.key']).transaction((p) => {
if (p) {
p.tree = {first: [], second: [], third: [], fourth: [], ...p.tree}
p.tree[level].push(techId)
}
return p
})
},
...
}
Firebase は要素が null や [] の場合キーごと消えてしまうので都度初期化しています。
VuexFire を利用する場合のデータフローは以下の記事が図解されておりわかりやすかったです。
雑感
ざっと触った感じでは、開発の立ち上がりは非常に楽でした。
Webpack のビルド設定まわりはいつも面倒に感じていたので、省略できるのはありがたいです。
ただ、まだ発展途上を思わせるところもあった。
デバッグのために一時的に Minify (Uglify) をオフにしたかったのだが、client.config.js を見てみると、そもそもオフにする設定が v1.0.0-rc11
にはなかった。
dev ブランチの client.config.js には設定が追加されていたので、近いうち利用できるようになるかも。
追記:下書きしている途中に Nuxt.js v1.0.0
が正式リリースされたようです。上記の内容も取り込まれていました!
正式リリースされたようなので、次は業務で使ってみようかな。