CompositionAPIで型を意識した処理の分離
3行で要約
- ロジックを.vueから分離して.tsに記述する
- Interfaceを使って.vue(コンポーネント)が対応するオブジェクトを制限
(型チェックが効くようにする) - ロジックの組み合わせを変更できるようにする
背景
Vue.jsを使っていると .vue
ファイルに書く処理が長くなってしまい、見通しが悪くなります。
CompositionAPIが流行ってからは useHoge()
で処理を外から持ってくることがやりやすくなりました。それだけでも見通しが良くなります。
ただ、使い回しの効くコンポーネントを作ることを考えると、「UI(.vue)はそのままに、ロジック(.ts)の切り替えを型安全に実現する」ことは必須と言えます。
値の追加、名称の変更などをしたときに、動かさないと仕様変更に気づけないのは効率的でないためです。
そこで今回はinterfaceを使って差し込める処理を制限することを考えました。
なんだかJavaっぽいですね。
まずは処理を分離
-
まずは
yarn create vite
をしてvue, tsを選んで HelloWorldなプロジェクトを作ります。 -
ボタンを押すとカウントアップする処理が書いてあるので、それを
Counter.vue
として切り出します。<template> <div class="card"> <button type="button" @click="logic.count++">count is {{ logic.count }}</button> <p> Edit <code>components/Counter.vue</code> to test HMR </p> </div> </template>
-
処理をPropsで受けられるようにしておきます。その際、interfaceを使って受け取るオブジェクトの型を制限しておきます。
<script lang="ts"> import {Ref} from "vue"; export interface ICounterLogic { count: Ref<number> } </script> <script setup lang="ts"> import {reactive} from "vue"; const props = defineProps<{ logic: ICounterLogic }>(); const logic = reactive(props.logic) </script>
-
Couter.vueに渡したい処理をCounterLogic.tsに記述します
(今回はrefな値を渡すだけ)
import {ref} from "vue"; import {ICounterLogic} from "../components/Counter.vue"; export const useCounterLogic: () => ICounterLogic = () => { const count = ref(0) return { count } }
-
Couter.vueを呼び出すHelloWorld.vue上で
useCounterLogic()
を呼び出し、処理が書いてあるオブジェクトを生成→Counterのpropsとして渡してあげます。<script setup lang="ts"> import Counter from "./Counter.vue"; import {useChildLogic} from "../usecase/CounterLogic"; defineProps<{ msg: string }>() const logic = useCounetLogic() </script> <template> <h1>{{ msg }}</h1> <Counter :logic="logic"/> ・ ・ ・ ・ </template>
型の制約が効くようになり、適合しなくなるとエディタ上でエラーが出るようになります。
こうなるとビルドができなくなります。そのため、CI上でエラーを出すことができ安心して開発できますね 🕺
この時点での参照関係は
という構造になっています。
ここまででも十分型の恩恵を得ていますが、今回は更に外部からのロジックの差し込みを実現してみます。
外部からのロジックの差し込み
今度は更に処理を分離してみます。参照関係のイメージはこんな感じです。
先ほどと同じように、App.vue → HelloWorld.vueの処理を変更してみます。
同じようにinterfaceを使って処理を分離し、
<script lang="ts">
export interface IHelloWorldLogic {
message: Ref<string>
counterLogic: ICounterLogic
}
</script>
App.vueから処理を挿入します。
<script setup lang="ts">
const helloWorldLogic = useHelloWorld()
</script>
<template>
・
・
・
<HelloWorld :logic="helloWorldLogic"/>
</template>
export const useHelloWorld: ()
=> IHelloWorldLogic = () => {
const message = ref<string>(initialMessage)
return {
message,
counterLogic
}
}
ここから更にHelloWorldのmessageの初期値とcounterLogic自体を外部から受け付けられるようにします。
export const useHelloWorld: (initialMessage: string, counterLogic:ICounterLogic)
=> IHelloWorldLogic = (initialMessage: string, counterLogic:ICounterLogic) => {
const message = ref<string>(initialMessage)
return {
message,
counterLogic
}
}
<script setup lang="ts">
const helloWorldLogic = useHelloWorld("Vite + Vue", useCounterLogic())
</script>
すると、このように「処理の組み合わせを自由に変更できる」構造にすることができました。
もちろんここでもinterfaceによる制約を付けているため、
定義側・実装側どちらかを変更したらエラーで該当箇所を見つけられます。
いかがでしたでしょうか。
サンプルのプロジェクトをアップしましたので、興味のある方はチェックしてみてください。
Discussion