📑

CompositionAPIで型を意識した処理の分離

2022/11/07に公開

3行で要約

  • ロジックを.vueから分離して.tsに記述する
  • Interfaceを使って.vue(コンポーネント)が対応するオブジェクトを制限
    (型チェックが効くようにする)
  • ロジックの組み合わせを変更できるようにする

背景

Vue.jsを使っていると .vue ファイルに書く処理が長くなってしまい、見通しが悪くなります。
CompositionAPIが流行ってからは useHoge() で処理を外から持ってくることがやりやすくなりました。それだけでも見通しが良くなります。
ただ、使い回しの効くコンポーネントを作ることを考えると、「UI(.vue)はそのままに、ロジック(.ts)の切り替えを型安全に実現する」ことは必須と言えます。
値の追加、名称の変更などをしたときに、動かさないと仕様変更に気づけないのは効率的でないためです。
そこで今回はinterfaceを使って差し込める処理を制限することを考えました。
なんだかJavaっぽいですね。

まずは処理を分離

  1. まずは yarn create vite をしてvue, tsを選んで HelloWorldなプロジェクトを作ります。

  2. ボタンを押すとカウントアップする処理が書いてあるので、それを 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>
    
  3. 処理を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>
    
  4. 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
        }
    }
    
  5. 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による制約を付けているため、
定義側・実装側どちらかを変更したらエラーで該当箇所を見つけられます。

いかがでしたでしょうか。

サンプルのプロジェクトをアップしましたので、興味のある方はチェックしてみてください。

https://github.com/adachi-koichi/vue-interface-example

レスキューナウテックブログ

Discussion