Vue.jsをコーディングする上で個人的に気をつけていること

## はじめに

1人〜3人程度の小規模のプロジェクトでVue.jsを書き始めて約半年ほど経ち、設計のナレッジが少し溜まりましたので、そのナレッジのアウトプットとしてVue.jsをコーディングする上で個人的に気をつけていることを羅列していきたいと思います。

Vue.jsのバージョンは2.x系を想定しています。  
またstorybook, SSRなどのフロントエンド独自の領域はほとんど扱わず、設計に関わる話を中心に記載していきます、なお投稿の中でMVVMやFluxの言葉をよく使います。

## SPAにはSPAにあった設計を

まず従来のWeb開発ではバックエンド側で画面にて使用する変数を描画前に用意してから、HTMLに変数を埋め込むという手法でViewModelを実現していました。

しかしAjax, SPAの登場により、画面推移を伴わない画面の更新が可能になりました。  

SPAの場合は1つの画面で様々なインタラクティブな操作を提供するため、View, ViewModelが複雑になり、Modelが複数箇所から参照されるようになってきます。

なのでSPAを構築する上では複雑化する画面の動作の可読性や保守性など、SPAならではのいくつかの考慮すべきポイントが出てくると思っています。

以下ではその考慮ポイントをダラダラと列挙していきたいと思います。

## DOMを直接扱わない

React、Vue.jsなどのSPA向けフレームワークでは仮想DOMという技術を採用しています。

仮想DOMについての詳細な説明は扱いませんが、端的に表現するとDOMの直接更新に仮想DOMは追従しません。
この事象に起因するバグは往々にして原因が追跡しづらく動作が不安定です。

JavaScript、jQueryに慣れているとついついDOMを直接操作をしてしまいますが、SPA向けのフレームワークではフレームワーク側で用意されたViewModelのみを参照すると良いと思います。

## Vueの公式スタイルガイドの記載内容を取り入れる

`優先度 A`な規約に関してはほとんどプロジェクトに適応可能だと思いますので、取り入れておくと良いと思います、`優先度 B`以降の規約に関してはプロジェクト毎で考える範囲かなと思います。

[Vue 2.x スタイルガイド](https://jp.vuejs.org/v2/style-guide/index.html)

## コンポーネントを4種類に分類する

コンポーネントの分類方法はかなりプロジェクト毎に変わる部分ではありますが、最低限コンポーネントを4つに分類することで、テスト容易性、保守性、可読性、拡張性などを向上させることができると思います。

その分類とは`単一ファイルコンポーネント`、`密結合コンポーネント`、`基底コンポーネント`、`単一インスタンス`です、Vueのスタイルガイドにも同様の記載があります。

大まかな使い分けとしては下記のようになると思います。

- ページそのものは単一ファイルコンポーネントとして定義する
- そのページでしか使われない様な用途の限られたコンポーネントは密結合コンポーネントとして定義する
- どのページでも共通なヘッダー、フッターは単一インスタンスとして定義する
- 入力欄やボタンなど、使いまわしをするものは基底コンポーネントとして定義する

僕自身は上記の分類を守るために下記の様なディレクトリの切り方が多いです、密結合コンポーネントを配置するために画面毎にディレクトリを切っています。

```
project/
├── components
│   ├── base      # 基底コンポーネントの配置場所
│   │   └── Input.vue
│   └── singleton # 単一インスタンスの配置場所
│       ├── Header.vue
│       └── __tests__
│           └── Header.test.vue
└── pages         # 単一ファイルコンポーネントの配置場所
    └── Login     # 密結合コンポーネントの配置場所
        ├── Login.vue
        ├── LoginForm.vue
        └── __tests__
            ├── Login.test.vue
            └── LoginForm.test.vue
```

## バックエンドとのやり取り

ページ表示前にViewModelをバックエンドで用意し、ページの初期表示時にViewModelを渡そうとすると、親コンポーネントにPropが大量に宣言され可読性が下がる、Propのバケツリレーが行われるなどの問題が発生します。

```Parent.vue
<template>
  <!-- 省略、またここでpropsのバケツリレーが行われる -->
</template>

<script>
export default {
  props: {
    groupName: {
      required: true,
      type: String
    },
    loginUserName: {
      required: true,
      type: String
    }
    // 以降配下のコンポーネントに必要なPropが宣言されていく...。
    // コンポーネントがPropに強く依存していて、可読性と保守性が下がる
  },
}
</script>
```

上記を回避するためにフロントエンドとバックエンドのデータの取得/更新は極力非同期通信にて行うようにすると、親コンポーネントに必要なデータの宣言のみを行うことができ、コンポーネントの責務を明確に分離できます。

また後述するテスト容易性の観点からAPIとの通信などの非同期処理はオブジェクトとして独立させるようにすると良いです。

```Parent.vue
<template>
  <!-- 省略 -->
</template>

<script>
export default {
  inject: ["$api"],
  data: {
    // このコンポーネントに必要なデータのみ宣言すればいい
    loginUserName: () => ""
  },
  mounted: function () {
    const user = this.$api.getLoginUser();

    this.loginUserName = user.name;
  }
}
</script>
```

ただ非同期通信の処理を増やすことでデータのやり取りのオーバーヘッドや、都度都度発生する例外処理や、API通信の待ち時間によるユーザーのストレスなどのデメリットもあります。

そういった場合はローディング処理を入れることでユーザーのストレスを解消し、オーバーヘッドを許容する設計や、例外処理をテストコードによってテストするなどの規約などを設けて、ネットワークの問題による非同期通信の失敗パターンなどを事前検証するなどの対処をすると良いと思います。

## コンポーネント間を跨いだデータのやり取り

SPAを用いた場合、コンポーネントを跨いてデータを扱う場合が多少あります、そういった場合には、様々な手段が取れますが状況に応じて親コンポーネントのPropを介してやり取りするかVuexを利用するかのどちらかを選択すると良いと考えています。  

例えばボタンをクリックした場合、他のUIコンポーネントの表示が1箇所だけ切り替わるといった場合は、データの依存関係が複雑ではないので、Props down/event upな設計で良いと思います。
しかしボタンをクリックすると他の複数のUIコンポーネントの表示や振る舞いが変わる場合には、Vuexでデータを持つと良いと思います、例えばダークモードなどの実装などはデータはVuex側に持たせるべきだと思います。

またAPI通信をActionsに含める、Modelの振る舞いをGettersに定義するなどModelに関する知識をVuex側で定義すると、Modelに知識を集約することができます。
Modelに知識を集約することで簡潔なテストを記述できる、Modelの更新ルートを局所化できる、Modelの振る舞いを共通化できるなどの様々なメリットがあります。

Vuexを採用する際には、[vuex-smart-module](https://github.com/ktsn/vuex-smart-module.git)などの適切な型付けとActions、Mutationsのモック化ができるライブラリを利用すると保守性を向上させることができます。

## テストコード

まず本項で扱うテストコードとは関数に対する単体テストや、Vue ComponentやVuexに対する結合テストを指しています。

僕が思うに今後保守される可能性があるコードを書くうえで、テストコードが残っているとフロントエンドの保守性が上がると考えています。  
テストコードのメリットは世にたくさん出ていますが、基本的には下記の4点に絞られると思います。

**1. コンポーネントの振る舞いに関するドキュメント**

テストコードは、実装コードの振る舞いを語る動くドキュメントとして機能します。

**2. コンポーネントの独立性に寄与** 

依存性が少ないコードはテストがしやすく、再利用性が高くなります。
テストコードを書くことで、実装の独立性は自ずと高くなると思っています。

**3. コンポーネントのバグを減らすことに寄与**

テストコードを記述することによって、コードが意図した振る舞いになるかテストすることができます。
テストツールと合わせてカバレッジ計測ツールを利用することで、実装コードのテスト網羅性を計測できます。
テストの抜け漏れを防ぎ、思わぬバグを防ぐことができます。（例えばawaitのつけ忘れとか、複雑なif分岐とか）

**4. 再実行可能なテスト**

テストコードはプログラムによってプログラムをテストするので、人手とは異なり正確に迅速にテストを実行できます。

またテストコードを書く上で再現性がないテストコードを書いてしまう、観点がバラけたテストが散見するなど回避するべき事象がいくつかあります。 
 
そういった事象を回避するために良いテストのポイントを以下に記載します、テストツールはJestを想定しています。

### テストの可読性を考慮する

例えばVuexでuser modelからrole typeが管理者であるユーザーのみを取得する処理を考えてみます。

```users.ts
export const Role = {
  staff: 0,
  admin: 1
} as const;

export type RoleType = typeof Role[keyof typeof Role];

export interface User {
  id: number;
  name: string;
  role: RoleType;
}

export interface UserListState {
  users: User[]
}

export const users = {
  state: {
    users: []
  },
  getters: {
    getAdminUserList(state: UserListState) {
      return state.filter(user => user.type === Role.admin).map(user => {
        return {
          id: user.id,
          name: user.name
        };
      });
    }
  }
};
```

上記のテストを作る上で、実装コードと同様のコードをテストに書いてしまう、[3A](https://qiita.com/inasync/items/e0b54e62784710c4b42d)を守らないなどテストコードの可読性を下げてしまうようなパターンがあります。

```users.test.ts
import { users } from "@/store/modules/users";

describe("users", () => {
  // 何をテストするのか不明瞭でわからない
  it("getAdminUserList", () => {
    const userListState = [
      // パット見なんのユーザーがわからない
      {
        id: 99,
        name: "user-1"
        role: 0
      },
      {
        id: 100,
        name: "user-2"
        role: 1
      }
    ];

    // テストコードと同じ実装コードを書いてしまっている
    const ret = userListState.filter(user => user.type === Role.admin).map(user => {
      return {
        id: user.id,
        name: user.name
      };
    });

    // 3Aが守られていなく、長くて読みづらい
    expect(user.getters.getAdminUserList(users)).toBe(ret);
  });
});
```

テストコードはドキュメントとして機能するはずなので、可読性を上げて保守したいものです、以下に改善したものを記載します。

```users.test.ts
import { users, Role } from "@/store/modules/users";

// users moduleの責務が記載されている
describe("ユーザーのリストを扱うusers module", () => {
  // テスト対象の振る舞いが記載されている
  it("ユーザーリストの中から権限種別が管理者に一致するユーザーを取得できる", () => {
    const userListState = [
      // ユーザーの権限が適切に宣言されている
      {
        id: 99,
        name: "staff"
        role: Role.staff
      },
      {
        id: 100,
        name: "admin"
        role: Role.admin
      }
    ];

    const actual = users.getters.getAdminUserList(userListState);

    // 関数な返り値が処理ではなく、値として宣言されている
    const expected = [
      {
        id: 100,
        name: "admin"
        role: Role.admin
      }
    ];

    // 3Aが守られていて、Assertの可読性が高い
    expect(actual).toBe(expected);
  });
});
```

上記のようにテストコードの可読性を上げるために、**テストコードに処理は書かず、処理の入出力に必要な値をリテラルな値で宣言する**、**3Aを意識してテストコードの流れを意識する**、**テストケースの切り方を規約化する**などのルールを設けてテストコードの可読性を向上させていくと良いと思います。

### テストの再実行性を考慮する

テストコードは改修時の影響範囲の確認のために実行されるかもしれませんし、ソースコードがコミットされる度にCI/CDパイプライン上で実行される可能性もあります。

良いテストコードは再実行性を兼ね備えているはずなので、変数の宣言場所を留意する、時間に関わる処理はなるだけ時間を固定化できるように設計するなど、いつ再実行しても成功するように再実行性を考慮するようにすると良いと思います。

### テストコードでカバー出来る品質は担保する

品質に関しては抑えて置きたいポイントがいくつかあります。

- テストコードでは関数やVue Componentの振る舞いをテストできます、テストツールのカバレッジ機能を使ってテスト対象の振る舞いの抜けがなるだけなくなるようにしていくと、思わぬバグの混入を防ぐ可能性が高まります。

- 正規表現や境界値テストがマッチする様な処理はカバレッジのみを指標とせず、目視によって条件が網羅するようにすると思わぬバグを防ぐことができるかもしれません。

- mock/stubにロジックを書かないほうがいいです、バグの混入に繋がってしまいます。

ただここで注意したいのが下記の様な目視のレビューで済むようなグルーコードもテストして時間を使ってしまうことです。

```ts
const stream = await this.$window.navigator.getUserMedia({ audio: true, video: { width: 1280, height: 720 });
```

`getUserMedia`メソッドの呼び出しをテストすることはもちろん可能ですが、それはメリットが労力に見合っていません、というかほとんどメリットがありません。

ブラウザやライブラリのAPIを呼びだす、bootstrapの様な共通処理をインポートするなどロジックではなくいわゆるグルーコードと呼ばれる部分の検査をレビューで実施するなど、実装コードのどの部分をテストするか？の観点は必要になります。

カバレッジを常に100%達成することが重要なのではなく、コードのロジックの部分や振る舞いなどが網羅的に検証されていることが重要だと思っています。

### テストコードでカバー出来ない範囲を考慮する

例えばVue ComponentがDOMにマウントされ、ブラウザからどのように動作するか？という部分は基本的にブラウザ上でしか確認できません、テストコードが動作するのはあくまでNode.js上での話です。

なのでテストコードを書いた上で最終的な動作確認として、ブラウザからの確認は必要になってくると思います。

### テスト容易性を考慮する

例えばバックエンドのAPIを呼び出す、ブラウザのAPIを呼び出す（例えば[Location](https://developer.mozilla.org/ja/docs/Web/API/Location)や[Media Stream API](https://developer.mozilla.org/ja/docs/Web/API/Media_Streams_API)）などテストをしているとフロントエンドの範疇を超えてくる存在が登場してきます。

そういった場合はMockオブジェクトに差し替えせるように設計しておき、テスト容易性を確保しておくことなどの考慮が必要になります。

具体的な対処としてはwindowオブジェクトは実装コードからはグローバル変数として参照せず、依存オブジェクトとして参照する、APIアクセスはオブジェクトとして定義するなどです。

### テストデータはテストケース毎に作成する

テストの再実行性で少し触れましたが、テストコードは将来的に再実行される可能性が高いので、テストコードは常に成功するようしておきたいです。

よって新規のテストケースの追加やあるテストケースの修正によって他のテストが失敗するなどの事象は避けたい所です。

テストをしていると関数の引数に渡すデータやモックオブジェクトを扱うことになりますが、これらのテストデータは共通化せず個々のテストごとに作成すると良いです。

というのも例えば共通なテストデータを参照している3つの関数があったとして、1つの関数のインターフェースの変更に伴いテストデータを更新すると、残りの2つの関数のテストが失敗する可能性があるからです。

## 共通化すべきものとそうでないもの

UIが似ている、コンポーネントの振る舞いが似ていると理由でコンポーネントや処理を共通化するなどはなるだけしないほうがいいです。

例えばHTTPリクエストを発行する際に、認証が必要な場合にヘッダにTokenを付与する、サーバーから取得したCSRFトークンをVuexに保存する等のどの様な場合にも共通であって、将来に渡って処理が変更される可能性が低いものは共通化するべきだと思いますが、UIの場合はデザインや機能が変更される可能性が高くなりますので、共通化に関しては慎重に進めると良いと思います。