Vite App+Vue3でクソアプリを作った


これはクソアプリ2アドベントカレンダー11日目の記事です。
今回、9月に正式リリースされたVue3を使ってとてつもなくシンプルなクソアプリを作ったので、その制作について話をします。

作ったアプリ

このアプリのクソポイント

  • UIがクソ
    • ろくすっぽスタイルがあたってません(そもそも要素が少ないというのもある)
  • 機能がクソ
    • 機能は、入力された名前にランダム文字列を追加してAPIに投げ、画像を取得して表示するだけです

このアプリの制作趣意

今回は、Vite Appを使い、Vue3とTypeScriptで、以下を学ぶことを目標としていました。

  • Composition API
  • Provide/Inject

制作過程

環境構築

まず、Vite AppでVue3環境を構築します。
TypeScriptを使いたいので、--templateフラグを利用してTypeScriptのテンプレートを利用します。

$ npm init vite-app <project-name> -- --template vue-ts
# or
$ yarn create vite-app <project-name> --template vue-ts

この時点でのディレクトリ構造はこちら

.
├── index.html
├── package.json
├── public
│   └── favicon.ico
├── src
│   ├── App.vue
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   ├── index.css
│   ├── main.ts
│   └── vue-app-env.d.ts
├── tsconfig.json
└── yarn.lock

ここからこねこねして、最終的に以下のディレクトリ構造になりました

.
├── README.md
├── index.html
├── package.json
├── src
│   ├── App.vue
│   ├── assets
│   │   └── style
│   ├── components
│   │   ├── AnswerBtn.vue
│   │   ├── InputName.vue
│   │   ├── ResetBtn.vue
│   │   ├── SwichingView.vue
│   │   └── UserImage.vue
│   ├── main.ts
│   ├── store
│   │   └── useState
│   │       ├── model.ts
│   │       └── provider.ts
│   └── vue-app-env.d.ts
├── tsconfig.json
└── yarn.lock

親コンポーネントにProvideを設置

前述の通り、今回のアプリの状態管理にはProvide/Injectを採用してます。これは親コンポーネントの状態を子コンポーネントに状態を受け渡すことができる仕組みで、Propsと違って子コンポーネントであればどこまでも受け渡すことができるので、Propsのようなバケツリレーをすることがありません。
正直、今回の構成だとPropsで問題ないんですが、これを試すのが目的なので、使っています。

まず親コンポーネントで、受け渡す値をuseStateという関数にまとめて別ファイルで変数に格納します。

export const useState = () => {
  const state = reactive({
    name: 'whyk',
    newname: '',
    answered: false,
    clicked: false,
  })

  const updateName = (newname: string) => {
    const randString = Math.random().toString(32).substring(2)
    state.name = `${newname}_${randString}`
  }
  const updateAnswered = () => {
    state.answered = !state.answered
  }
  const updateClicked = () => {
    state.clicked = true
  }
  const resetState = () => {
    state.name = 'whyk'
    state.newname = ''
    state.answered = false
    state.clicked = false
  }

  return {
    state,
    updateName,
    updateAnswered,
    updateClicked,
    resetState,
  }
}

また、この関数を識別するキーとして、シンボル化した値を変数に格納します。
型については、InjectionKeyはProvide/Injectのキーとして利用される型定義、型変数にはStateStoreという、また別の場所で作成された上記関数の返り値を型として利用できるようにしたものになります。

export const StateKey: InjectionKey<StateStore> = Symbol('StateStore')

これらを親コンポーネントであるsrc/App.vueで読み込みます。

import { defineComponent, provide } from 'vue'
import { useState, StateKey } from './store/useState/provider'

export default defineComponent({
  setup() {
    provide(StateKey, useState())
    return {}
  },
})

これで、子コンポーネントへの状態の受け渡しの準備が整いました。

子コンポーネントにInjectを設置

次に、子コンポーネントを作成し、親コンポーネントから状態を受け取ります。

import { defineComponent, inject } from 'vue'
import type { StateStore } from '../store/useState/model'
import { StateKey } from '../store/useState/provider'

export default defineComponent({
  setup() {
    const { state } = inject(StateKey) as StateStore

    return {
      state,
    }
  },
})

Vueから提供されているinjectにProvide/Injectのキーを渡し、型アサーションで型をuseStateの返り値で上書きします。
これで親コンポーネントから状態が受け取れるようになりました。

そのあと

Provide/Injectを使う関係で、すべての要素を子コンポーネントにする必要があります。
そのため、最終的な親コンポーネントは以下のようになっています。

<template>
  <user-image />
  <h1>Whats Your Name?</h1>
  <switching-view>
    <template #question>
      <input-name />
      <answer-btn />
    </template>
    <template #answer>
      <p>Hahaha!! Very suitable!!!!</p>
      <reset-btn />
    </template>
  </switching-view>
</template>

<script lang="ts">
import { defineComponent, provide } from 'vue'
import SwitchingView from './components/SwichingView.vue'
import UserImage from './components/UserImage.vue'
import InputName from './components/InputName.vue'
import AnswerBtn from './components/AnswerBtn.vue'
import ResetBtn from './components/ResetBtn.vue'
import { useState, StateKey } from './store/useState/provider'

export default defineComponent({
  components: {
    SwitchingView,
    UserImage,
    InputName,
    AnswerBtn,
    ResetBtn,
  },
  setup() {
    provide(StateKey, useState())
    return {}
  },
})
</script>

最後に

Advent Calendar初参加でしたが、本来出すはずのクソアプリが完成せず、急きょ別のクソアプリで記事を書きました。
ここに出すはずだったクソアプリを作るプランも並行して考えているので、また違うフレームワークで作ってみたいところです。