Vue3 × TypeScriptによるSPA開発

# はじめに

SPA開発に用いられるフレームワークとしては、主に以下3つが挙げられます。

- Vue.js
- React
- Angular

今回はその中でもVue.jsについて焦点を当てていきます。
Vue3を使って簡単なTodo管理アプリを作成したので、その過程で学習した点についてお話ししていきたいと思います。
なお、今回バックエンドについては簡略したかったため、TISが公開している[Reactハンズオンコンテンツ](https://fintan-contents.github.io/spa-restapi-handson/)のバックエンドアプリを使用しました。

## 環境情報

- Vue.js 3.0.4
- TypeScript 4.1.5
- Node.js 15.5.1

なお、Vue.jsには「class-style component」,「options api」等様々な書き方がありますが、今回はVue3から主要となるComposition APIで実装しました。
また、型定義ができることやオブジェクト指向言語であることからJavaScriptではなく、TypeScriptを使用します。

# Vue CLI

VueではCLIが提供されており、コマンドで様々な処理を行うことができます。
インストール

```
npm install -g @vue/cli
```

createコマンドを使って、アプリの雛形を作成していきます。

```
vue create vue-todo-app
```


以下のような画面が表示され、上2つでVue2か3で雛形作成をすることができますが、最小限のライブラリかつJavaScriptで構築されてしまいます。
なので、手動で諸々の設定を行うために一番下の「Manually select features 」を選択します。

```
? Please pick a preset: 
  Default ([Vue 2] babel, eslint) 
  Default (Vue 3) ([Vue 3] babel, eslint) 
❯ Manually select features 
```

TypeScriptサポートやルーティング等のライブラリ追加を行いたいので、全てにチェックをつけて実行します。

```
? Please pick a preset: Manually select features
? Check the features needed for your project: 
❯◉ Choose Vue version
 ◉ Babel
 ◉ TypeScript
 ◉ Progressive Web App (PWA) Support
 ◉ Router
 ◉ Vuex
 ◉ CSS Pre-processors
 ◉ Linter / Formatter
 ◉ Unit Testing
 ◉ E2E Testing

```

続けてVueのバージョン等様々な質問を聞かれるので、以下のように回答していきます。

```
? Choose a version of Vue.js that you want to start the project with 3.x　←Vueバージョン3を指定
? Use class-style component syntax? No  ←class-style componentは使用しない。
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfi
lls, transpiling JSX)? Yes　←TypeScriptを使用する。
? Use history mode for router? (Requires proper server setup for index fallback 
in production) Yes　←ルーターのヒストリー機能を有効にする。
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported 
by default): Sass/SCSS (with dart-sass)
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save　←保存時に静的チェックを行う。
? Pick a unit testing solution: Jest
? Pick an E2E testing solution: Cypress
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N) N
```

作成が完了したら以下コマンドでアプリを起動すると画面が立ち上がります。

```
npm run serve
```

![スクリーンショット 2021-05-20 4.35.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498430/7a1631f9-b25c-664c-8013-9724691e0b30.png)

# コンポーネント分割

Vueに限らずSPAを構築する上でまず考えなくてはいけないのがコンポーネント分割です。
コンポーネントとはHTMLをある一まとまりの単位で切り出したものを指しており、SPAにおける1画面は複数のコンポーネントから成り立ちます。
SPAはその名の通り単一のHTML上で稼働するため、jsで動的にコンポーネントを差し替えていくことで画面遷移を実現します。
ヘッダ等画面によって変わらない箇所をコンポーネントに切り出すことで、画面遷移の際にヘッダ部分を切り替えなくて済む等(レンダリングの時間減)等のメリットがあります。
特に分割の基準に決まりはありませんが、大体以下が目安となります。

- ヘッダやメニュー等複数画面に存在するもの(再利用性のあるもの)は切り出す
- 1コンポーネントあたりの役割が大きくなりすぎる場合には、役割によって切り出す

例)![スクリーンショット 2021-05-20 4.49.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498430/337d696c-4814-6d75-03ad-a87c1af5c1b9.png)

- NavigationHeader（黄色）：ナビゲーションメニューのヘッダ
- TodoBoard（オレンジ色）：ToDoを扱うエリア
- TodoForm（青色）：新しいToDoを入力する
- TodoFilter（紫色）：ToDoの表示対象を選択する
- TodoList（緑色）：ToDoを一覧形式で表示する
- TodoItem（赤色）：ToDoを1行で表示する


# コンポーネントの基本形

Vueでは〜.vueというファイルが1コンポーネントになります。
HTMLとそれに対するjavascript、cssが実装されます。

```typescript:TodoList.vue
<template>
  <TodoFilter v-bind:filterType="filterType" @fillter-event="fillterTodo" />
  <ul class="TodoList_list" v-if="filterType === 'ALL'">
    <TodoItem
      v-for="(todo, index) in allTodos"
      v-bind:key="index"
      v-bind:id="todo.id"
      v-bind:text="todo.text"
      v-bind:completed="todo.completed"
    />
  </ul>
  :
  <ul class="TodoList_list" v-else-if="filterType === 'INCOMPLETED'">
    <TodoItem
      v-for="(todo, index) in incompletedTodos"
      v-bind:key="index"
      v-bind:id="todo.id"
      v-bind:text="todo.text"
      v-bind:completed="todo.completed"
    />
  </ul>
</template>

<script lang="ts">
import { computed, defineComponent } from "vue";
import TodoFilter from "@/components/TodoFilter.vue";
import TodoItem from "@/components/TodoItem.vue";
import { useStore, FilterType } from "@/store/index";

export default defineComponent({
  name: "TodoList",
  components: { TodoFilter, TodoItem },

  async setup() {
    // ストアの取得
    const store = useStore();
    // Todo取得API呼び出し
    await store.dispatch("getTodoList");

    // 絞り込み時のコールバック関数
    const fillterTodo = (conditions: FilterType) => {
      const payload = {
        filterType: conditions,
      };
      store.commit("setFilterType", payload);
    };

    return {
      // ストアから取得したTodo全件
      allTodos: computed(() => store.getters.getAllTodos),
      // ストアから取得した完了済みのTodo
      completedTodos: computed(() => store.getters.getCompletedTodos),
      // ストアから取得した未完了のTodo
      incompletedTodos: computed(() => store.getters.getIncompletedTodos),
      // ストアから取得した絞り込みの種類
      filterType: computed(() => store.state.filterType),
      fillterTodo,
    };
  },
});
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.TodoList_list {
  list-style: none;
  padding: 0;
  margin: 20px 0;
}
</style>

```

### ライフサイクルフック

Vueでは10種類のライフサイクルフックが用意されています。
しかし、実際に使用する機会があるのは太字ぐらいかと思われます。
特にsetupはほとんどのコンポーネントで使われます。

- **setup　コンポーネントが生成された時**
- onBeforeMount
- **onMounted 　DOMがマウントされた時**
- onBeforeUpdate
- onUpdated
- onBeforeUnmount
- **onUnmounted　コンポーネントが破棄される時**
- **onErrorCaptured　コンポーネント内処理でエラーが送出された時**
- onRenderTracked
- onRenderTriggered

# 双方向データバインドと関数のバインド

html側の入力ボックスとjs側の変数をバインドさせるには、リアクティブ変数を使用します。
変数をv-model属性で対象の要素に設定することで、値がバインドされるようになります。
また、関数については@イベント名=関数名でバインドします。

```typescript
<div class="TodoForm_input">
  <input
    v-model="data.todoText" // v-modelを使ってjs側の変数をバインド
    type="text"
    placeholder="タスクを入力してください"
  />
</div>
<div class="TodoForm_button">
  <button type="button" @click="onSubmit">追加</button>　//@clickでonSubmit関数をバインド
</div>
:
<script lang="ts">
import { defineComponent, reactive } from "vue";
import { useRouter } from "vue-router";
import { useStore } from "@/store/index";
import { PostRequest } from "@/store//models/PostRequest";

type Params = {
  todoText: string;
};

export default defineComponent({
  name: "TodoAdd",
  setup() {
    const data = reactive<Params>({
      todoText: "",
    });

    const router = useRouter();
    const store = useStore();

    const onSubmit = async () => {
      const req: PostRequest = { text: data.todoText };
      await store.dispatch("addTodo", req);
      router.push("/");
    };

    return { data, onSubmit };
  },
});
</script>
```

# API呼び出し

SPAではバックエンドアプリのAPIをコールすることで、画面表示に必要な情報を取得したり、業務処理をキックしたりします。
フレームワークによっては、APIコール用のライブラリが提供されていますが、Vueにおいてはそれがないため、サードパーティ製の[axios](https://jp.vuejs.org/v2/cookbook/using-axios-to-consume-apis.html)というライブラリを使うのが一般的です。

CLIで作成したアプリにはデフォルトでaxiosがインストールされていないため、個別にインストールする必要がある。

```
$ npm install axios --save
```

実装例)

```typescript
import axios from "axios"; // axiosをインポート
:
async getTodoList() : Todo[] {  // axiosは非同期で実行されるので、同期処理をする場合はasync awaitを使う
    const todos: GetResponse = await axios　//  axios.getでAPI呼び出し
      .get(`${process.env.VUE_APP_API_BASE_URL}/api/todos`)
      .then((res) => {
        return res.data;  // res.dataでレスポンスのbodyオブジェクトが取得できる。
      });
    return todos;
}
```

# ルーティング

ルーティングはVueから提供されているvue-routerライブラリを用いて行います。
1.ルーターの定義
まずどのパスならばどのコンポーネントに遷移させるのかを定義します。
定義はアプリ作成時に生成されたrouterフォルダ配下の、index.tsにて行います。

```typescript:router/index.ts
const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "TodoBoard",
    component: TodoBoard,
  },
  {
    path: "/add",
    name: "TodoAdd",
    component: () => import("../views/TodoAdd.vue"),
  },
];
```

2.ルーティング対象範囲を決める
全体の親となるコンポーネントであるApp.vueに対して、画面のどの範囲をルーティング対象とするか定義していく。
今回はヘッダは全画面共通としたいので、ヘッダコンポーネントとルーティングで切り替わるコンポーネントで構成することにする。

```typescript:App.vue
<template>
  <NavigationHeader />　// ヘッダコンポーネント
  <router-view />　// ルーティングで切り替わるコンポーネント
</template>

<script lang="ts">
import { defineComponent } from "vue";
import NavigationHeader from "@/components/NavigationHeader.vue";

export default defineComponent({
  name: "App",
  components: { NavigationHeader },
});
</script>
```

3.各コンポーネントにルーティング処理を埋め込む
リンク押下時に何も処理を必要としないリンクの場合、templateの中にrouter-linkというタグを埋め込む。

```typescript
<template>
  <div class="TodoBoard_content">
    <h2>Todo一覧</h2>
    <router-link to="/add">新規追加</router-link>　←/addというパスに遷移するリンク
    <Suspense>
      <template #default>
        <TodoList />
      </template>
      <template #fallback>
        <div>Loading...</div>
      </template>
    </Suspense>
  </div>
</template>
```

リンク押下時にAPIの呼び出し等何らかの処理を挟むリンクの場合、その時コールされる関数の中に遷移処理を実装する。

```typescript
import { useRouter } from "vue-router";
：
setup() {
  const data = reactive<Params>({
    todoText: "",
  });

  const router = useRouter();  // setup関数の中でuseRouter()でルーターを取得
  const store = useStore();

　// ボタン押下時メソッド　
  const onSubmit = async () => {　
    const req: PostRequest = { text: data.todoText };
    await store.dispatch("addTodo", req);
    router.push("/"); // router.pushで遷移したいパスを指定する
  };

  return { data, onSubmit };
},

```


# 遅延ロード

VueによるSPAは、アプリの最初の画面を表示するときに全てのSPAコンテンツ(JavaScript)を取得します。
アプリが大規模であればあるほど初回の画面表示が長くなり、ネットワーク帯域も圧迫することから、必要になった時に必要なコンテンツをロードする**遅延ロード**の対応が推奨されます。
遅延ロードの設定はルーティングの設定同様router/index.tsで行います。
TodoAddコンポーネントのようにコンポーネントをimportする形式で記載すれば、その画面に遷移するまではコンテンツがダウンロードされなくなります。

```typescript:router/index.ts
const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "TodoBoard",
    component: TodoBoard,
  },
  {
    path: "/add", // addに遷移した時初めてTodoAddコンポーネントがダウンロードされる
    name: "TodoAdd",
    component: () => import("../views/TodoAdd.vue"),  
  },
];
```


# 状態管理(値の保持)

コンポーネント間の値の受け渡しについては、状態管理ライブラリVuexを用います。
なお、状態管理している変数はリアクティブになります。
[参考リンク](https://zenn.dev/ryo_kawamata/articles/intoroduce-vuex4-with-composition-api)

State：管理する状態そのもの。
      APIの結果やユーザーの入力中等。
Axions:APIの呼び出し等state（保持する情報）に関する操作を行う。
　　　　stateを変更する際は、必ずMutationsの操作で更新する。
Mutations：stateを変更する操作を行う。


![スクリーンショット 2021-05-20 8.45.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498430/71ea23c6-44b1-3534-c692-e6611fdf02f5.png)



# 他フレームワークとの比較

Angularの利用歴が長いので、AngularとVueの比較について個人的な感想も含めお話しします。

### バージョンアップ頻度とサポート

- Vue

特に決まった頻度はない。
サポートは新バージョンが出てから2年は有効。
(2022年にVue4が出るとして、Vue3は2024までサポート対象)
Vue1:2015年
Vue2:2016年
Vue3:2020年

- Angular

約半年に1回の定期リリース。
サポートはリリース後1年半有効。
良くも悪くもリリーススパンが短いので、メンテナンスコストが高い印象。

Angular10:2020/06/24リリース 2020/12/24サポート切れ
Angular11:2020/11/11リリース 2022/05/11サポート切れ
Angular12:2021/05/13リリース 2022/11/13サポート切れ

### ライブラリ

- Vue

サードパーティー製ライブラリと組み合わせることが前提。

- Angular

フルスタックであり、サードパーティー製ライブラリをほとんど必要としない。

### 学習コスト

- Vue

日本語ガイドがあるが、バージョン2のガイドを理解した上でバージョン3のガイドを読まないといけないところがあり少々煩雑。
また、基本JavaScriptベースでガイド化されており、TypeScriptで実装するには読み替えたり別途調査が必要になることが多い。
コーディングガイドも提供されている。
[Vue公式ガイド](https://v3.ja.vuejs.org/guide/introduction.html)

Vue3が割と最近リリースされたこと、書き方が複数存在することから、得たい情報を探すのには時間がかかる。
Vue3から始める人向けには少々学習コストが高くなりそう。

- Angular

日本語ガイドがあり分かりやすい。
また、ハンズオンコンテンツがついており、進めれば基本的なことは習得できる。
コーディングガイドも提供されている。
[Angular公式ガイド](https://angular.jp/docs)

書き方もTypeScriptベース1本、かつJavaに近い書き方ができることから、オブジェクト指向言開発語経験があればとっかかりやすい。
そこまで学習コストは高くない印象。

# おわりに

簡単ではありますが、Vueを学習してみてわかったことについてまとめてみました。
これからVueを学習する方の一助になればと思います。
また、今回作成したアプリについてはGitHubに公開しており、環境構築・稼働確認方法等はReadmeに記載していますので、興味があればご覧ください。
[GitHub](https://github.com/Yoshi034/vue-todo-app)

## 今後学習してみたいこと

- Vueプロジェクトにおける単体・e2eテスト実施方法
- Reactを使った開発手法