#前言
センシンロボティクスのサツキです。
前職はフルスタックエンジニアですが、SRには主にWebフロントエンド開発を担当します。
#現状
ドローンの無人運用を実現できるように、フライトプランの作成から実行まで対応する、Vueベースのシングルページアプリ(SPA)が開発していた。
下記は簡素化した構成です。
<base>
<map/>
<details/>
</base>
map
とdetails
間はEventBus
に通じて状態の受け渡しを行います。
#課題
だたし、時間を経過すると、いくつの問題が浮きあがりました。
-
<map/>
の初期化が時間がかかり、時により<details/>
から発火する時点はリスナーがまだ立ち上げなかったので、反映できなかった。 -
<map/>
と<details/>
が両方フライトプランを変更する場合がありますので、アプリが大きくなるとデバッグが難しくなる。 - その後Vue Routerのネストルートを導入し、
<overview/>
、<edit-form/>
、<flight-viewer/>
を分けた。これで状態の受け渡しが難しくなりました。
#一方通行
その答えは、Fluxです。
簡単といえば
- 共通状態をStoreに置く
- StoreはSetterを持たず。Viewは
action
を通じて変更を促進するしかない。 - Storeはdispatcherから渡した
action
に応じて変更して、変更が終わったらViewに通知する。 - ViewはStore状態に応じて表示する。
きちんと設計したFluxアーキテクチャはデータフローが明瞭になり、状態変更の追従もユニットテストも楽になります。一方、Flux設計はハードルが高く、初心者にとっては難しいかもしれません。
Vuexも、そんなFlux実装の一つです。ただし、本家FluxやReduxなどと違い
- VuexはVueに依存する。逆にいえば、VuexはVueとの相性が良く、整合もより簡単です。
- Vuexは最初から
async
が対応する。 - FluxとReduxは新しい
state
を返さなくて検知できない。それに対し、Vuexはstate
値を変更するだけで検知できる。
#例
store.js
import axios from 'axios';
const state = {
currentId: undefined,
allFlightPlans: [],
}
const actions = {
index({commit, state}) {
axios.index(url, (allFlightPlans) => {
commit('receive', allFlightPlans)
if (allFlightPlans.length > 0) {
commit('setCurrent', allFlightPlans[0])
}
}
},
select({commit, state}, flightPlan) {
commit('setCurrent', flightPlan);
},
}
const mutations = {
receive(state, allFlightPlans) {
state.allFlightPlans = allFlightPlans;
},
setCurrent(state, flightPlan) {
state.currentId = flightPlan.id;
},
}
const getters = {
currentIndex(state) {
return state.allFlightPlans.findIndex(item => item.id === state.currentId) + 1;
},
}
export default {
state,
actions,
mutations,
getters
}
viewer.vue
<template>
<div>
<div>表示中ルート: {{selectedIndex}}/{{allFlightPlans.length}}</div>
<ol>
<li :class="{active: item.id === currentFlightPlanId}
v-for="item in allFlightPlans"
@click="setCurrentFlightPlan(item)">
{{ item.name }} (最終更新日時: {{item.updatedAt}})
</li>
</ol>
</div>
</template>
<script type="text/javascript">
import { mapState, mapActions, mapGetters } from 'vuex';
export default {
computed: {
...mapState({
allFlightPlans: state => state.allFlightPlans,
currentFlightPlanId: state => state.currentId
}),
...mapGetters({
selectedIndex: 'currentIndex',
}),
},
methods: {
...mapActions([
setCurrentFlightPlan: 'select',
indexFlightPlans: 'index',
}),
},
created() {
this.indexFlightPlans();
}
}
</script>
<style scoped lang="scss">
/* 略 */
</style>
#最後に
残念しながら、実際の本番アプリは例より遥かに複雑で、リファクタリング作業は思ったより時間がかかってしまった。とはいえ、同僚によると、変更した部分はバグが減っているし、新規開発は今までよりスムーズになるようです。
Flux ライブラリは眼鏡のようなものです: あなたが必要な時にいつでも分かるのです。
-- Dan Abramov、Redux 開発者
そもそもFluxの実装は複雑ですが、うちの場合はメリットあると思います。