📑

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

adachi_koichi2022/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

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

日本で唯一の危機管理情報を専門に取り扱う防災Techのスタートアップ、(株)レスキューナウです。当社で活躍するエンジニアの技術ブログを中心に公開していきます。

Discussion

ログインするとコメントできます