19
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Vue.js + Typescript のディレクトリ構造一例

Posted at

こんにちは。
Web・iOSエンジニアの三浦です。

今回は、最近開発している Vue.js(Typescript)について、実際に使っているディレクトリ構造を紹介します。

はじめに

初めて Vue.js で開発をしたときは、基本に則った Atomic Design を使用していました。
しかしコンポーネントが増加するにつれ、どのように分けるべきか、同レイヤー間の呼び出しを許可するかなど煩雑さが増していきました。
その後別プロジェクトを Vue.js + Typescript で開発する機会があり、前回の反省を踏まえて少し Atomic Desigin をカスタマイズしてみることにしました。

ディレクトリ構造

現状以下のディレクトリ構造を使っています。
基本的な考え方は Atomic Design ですが、こちらの「Atomic DesignをVue.jsで実現するための構成と考え方 | Biscuetでの例をもとに」をかなり参考にさせていただいています。
なおこれ以外は、 Vue CLI のデフォルトの構造から変えていません。

src
├── components      // ページ要素コンポーネント
│   ├── organisms       // 各ページに固有の要素
│   │   └── ...
│   └── pages           // 各ページ
│       └── ...
├── plugins         // グローバルインストールするプラグイン
│   └── materials      // 汎用要素
│       ├── atoms          // 最小単位の汎用要素
│       │   └── ...
│       ├── molecules      // 最小単位の要素を組み合わせた汎用要素
│       │   └── ...
│       ├── index.ts       // materialsをグローバルインストール
│       └── ...
├── constants       // 定数
│   └──  ...
├── managers        // マネージャ
│   └──  ...
├── models          // データモデル
│   └──  ...
├── usecases        // 各ページのユースケース
│   └──  ...
├── utils           // ヘルパー
│   └──  ...
│
│   // 以下はデフォルトで存在する
│
├── assets          // 画像・CSS等アセット
│   └──  ...
├── router          // ルーター
│   └──  ...
├── store           // vuexストア
│   └──  ...
├── App.vue         // 画面フレーム
└── main.ts         // 各ライブラリ等読み込み

以下では、Vue CLIには存在しないこのディレクトリ構造独自の部分を説明していきます。

components

Atomic Designにおける pagesorganisms を格納するディレクトリです。
Atomic DesignをVue.jsで実現するための構成と考え方 | Biscuetでの例をもとに」に倣っており、そちらに説明されている通り冗長になりがちな templates は排除しています。

plugins/materials

Atomic Designにおける moleculesatoms を格納しています。
こちらも「Atomic DesignをVue.jsで実現するための構成と考え方 | Biscuetでの例をもとに」に倣っており、 plugins/materials/index.ts を使ってグローバルインストールさせています。
グローバルインストールさせていることからも分かる通り、ここに格納されるコンポーネントはいわば Vuetify などのようなライブラリ的使い方を想定しており、そのためどこでも使い回せるようなコードにすることが意識付けられます。

上記の Atomic Design の分け方にしたことで、「どのようにコンポーネントを分け、どのレイヤーに入れるべきか」、また「同レイヤー間の呼び出しをどうするか」はかなりスッキリ考えられるようになったと思います。

なお、「Atomic DesignをVue.jsで実現するための構成と考え方 | Biscuetでの例をもとに」で紹介されていた plugins/materials/index.js は、Typescriptとして書き直すと以下のようになります。

import { VueConstructor } from "vue/types/umd";

// @/plugins/materials 配下の全vueファイルを取得
const context = require.context(".", true, /.vue$/);
const components: { [key: string]: any } = {};

// 取得したコンポーネント一覧を、ファイル名がキーのオブジェクトに変換
context.keys().forEach(contextKey => {
  const keys = contextKey.match(/.+\/(.+)\.vue/);
  if (keys) {
    const key = keys[1];
    components[key] = context(contextKey).default;
  }
});

// ファイル名をキーとしたコンポーネントとしてグローバル登録
export default {
  install(Vue: VueConstructor) {
    Object.keys(components).forEach(key => {
      Vue.component(key, components[key]);
    });
  }
};

ここからは、各コンポーネントで使用するデータや処理を、どのようにコンポーネント外で定義するかになります。

constants

定数を定義します。
色々なところで使い回すようなものや、データサイズが大きいものなどはここに入れるようにしています。

例:画面幅の定数

const DISPLAY: {
  sm: {
    start: number;
  };
  md: {
    start: number;
  };
  lg: {
    start: number;
  };
  xl: {
    start: number;
  };
} = {
  sm: {
    start: 600
  },
  md: {
    start: 960
  },
  lg: {
    start: 1264
  },
  xl: {
    start: 1904
  }
};

export default DISPLAY;

managers

例えばAPIへの接続処理など、ある程度複雑で、かつ色々なところで使い回すような処理はここに入れています。

例:データを受け取ってAPIへ投げ、返り値を指定のクラスに変換して返すマネージャ

import axios, { AxiosResponse, AxiosRequestConfig } from "axios";
import { Request, RequestMethodType } from "@/models/request/baseRequest";
import Decoder from "@/models/request/baseDecoder";

const APIManager: {
  request: <P, B, R>(request: Request<P>, decoder: Decoder<B, R>) => Promise<R>;
} = {
  /**
   * APIリクエスト
   *
   * @template P APIリクエスト時に投げるパラメータ
   * @template B APIレスポンスの型
   * @template R レスポンスを変換する型
   * @param {Request<P>} request リクエスト型
   * @param {Decoder<B, R>} decoder デコーダ
   * @returns {Promise<R>} レスポンスを型変換した値
   */
  request: async <P, B, R>(
    request: Request<P>,
    decoder: Decoder<B, R>
  ): Promise<R> => {
    let method: AxiosRequestConfig["method"];

    switch (request.method) {
      case RequestMethodType.post:
        method = "post";
        break;
      case RequestMethodType.get:
      default:
        method = "get";
        break;
    }

    let response: AxiosResponse<any> = await axios({
      method: method,
      baseURL: request.baseURL,
      url: request.path,
      data: request.parameters
    });

    if (typeof response.data === "undefined") {
      throw new Error("data could not be gotten.");
    }

    return decoder.decode(JSON.parse(response.data));
  }
};

export default APIManager;

models

APIのレスポンスの型など、一つのデータとして固めておきたい塊を入れるモデルクラスを定義します。

例:APIへのリクエストモデル

import { Request, RequestMethodType } from "@/models/request/baseRequest";

export type TextParameterType = { text: string };

export class TextRequest implements Request<TextParameterType> {
  private _method: RequestMethodType = RequestMethodType.post;
  private _baseURL: string = "http://localhost:8080";
  private _path: string = "/text";
  private _parameters: TextParameterType;

  constructor(text: string) {
    this._parameters = { text: text };
  }

  get method(): RequestMethodType {
    return this._method;
  }

  get baseURL(): string {
    return this._baseURL;
  }

  get path(): string {
    return this._path;
  }

  get parameters(): TextParameterType {
    return this._parameters;
  }
}

usecases

一定以上の複雑なページを作る時、 pagesorganisms にすべての処理を書くと、ファイルが長くなりすぎて可読性に欠ける場合があります。
そのため、コンポーネント側にはviewとの接続を主に担当させ、具体的なロジックをユースケース側に分離して書くようにすることで可読性を向上させることができます。

コンポーネント側

@Watch("text")
onChangeText() {
  this.submitEnabled = TextUseCase.submitEnabled(
    this.textString,
    this.maxLength
  );
}

ユースケース側

submitEnabled: (text: string, maxLength: number): boolean =>
  0 < text.length && text.length < maxLength

utils

全体的に使う、ちょっとしたヘルパー関数を入れています。

例:スリープ処理

const sleep: (msec: number) => Promise<any> = msec =>
  new Promise(resolve => setTimeout(resolve, msec));

export default sleep;

おわりに

いかがだったでしょうか。
もちろんこれが必ずしも正解とは言えないですし、これ自体に関してもまだ完成ではなく、例えば managersutils の境はどの程度の複雑性になるのか、であるとか、DIをしたい場合はさらに別途ディレクトリ構成が必要になったりはあると思いますが、現状のプロジェクトの規模だとこれで良い感じに回っている印象です。
参考になれば幸いです!

参考文献

以下のサイトを参考にさせていただきました。
ありがとうございました!

Atomic DesignをVue.jsで実現するための構成と考え方 | Biscuetでの例をもとに

19
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?