オンライン家庭教師マナリンクでCTOを務めている名人です。
マナリンクではフロントエンドをNuxt.js×TypeScriptで実装しています。Nuxt.jsは割と標準で多くのディレクトリを作ってくれますが、開発をしているとそれ以外のディレクトリをオリジナルで作っていくこともあると思います。
本記事では、Nuxt.js×TypeScriptで開発しているマナリンクのディレクトリ構造を公開します。興味を持っていただいた方はぜひ面談しましょう!
プロジェクトルート
$ tree -L 1 -d
.
├── aws
├── client
├── docker
├── node_modules
└── server_side_rendering
- aws: 早速個性的な部分になりますが、マナリンクではCDNにCloudFrontを使っていまして、CloudFrontの設定をaws-cdkで反映しています。このawsディレクトリの配下には、CloudFrontでのキャッシュ設定を反映するaws-cdkのコードだったり、紐付けているLambda@Edge関数を保存しています
- client: Nuxt.jsのルートディレクトリです。srcっていう名前でも良かったかもしれない。気が向いたら変えます
- docker: Nuxt.jsのSSRをDockerコンテナで開発〜本番環境で統一して管理しています。そのDockerfileなどの設定を置いているディレクトリです
- server_side_rendering: SSRするためのFastifyのソースコードを置いています。serverという名前にするとサーバーサイドのコードも管理しているモノリシックなリポジトリに見えてしまうので、ちょっと長い名前ですがこうしています
参考サイト
FastifyはExpress等より高速にレスポンスを返すなどの理由で最近デファクトになりつつある
https://github.com/fastify/fastify
CDKを使うとソースコードでAWSの設定を管理できて便利。色々とExperimentalなので運用時は注意
https://github.com/aws/aws-cdk
CDKでEdge関数をデプロイしたという自分の記事
https://qiita.com/mejileben/items/6f1d272809d6b0ff1aad
CDKを使うことで、フロントエンドのコードとCDNの設定のデプロイを同時に行うという縛りは結構面白くて、キャッシュ戦略を変えるリリースを安全に行えたりします。独自の工夫です。
Nuxtルート
$ tree -L 1 -d client
client
├── apis
├── apisMicrocms
├── assets
├── components
├── composables
├── layouts
├── middleware
├── modules
├── pages
├── plugins
├── static
├── store
├── types
└── utils
- apis, apisMicrocms: HTTP APIの型定義をTypeScriptの型で管理できるライブラリaspidaの設定ファイルを置いています。マナリンクのフロントエンドは、バックエンドにmicroCMSとLaravel製APIを抱えているので、それぞれの型定義を置いています
- composables: Vue3で導入予定のcomposition-apiを使った関数を入れているディレクトリです
- modules: composition-apiを使っていないが、Vueコンポーネントから切り出したいかつマナリンク独自の処理を入れているディレクトリです。例でいうと、マナリンクでは科目毎にアイコンを用意しているのですが、科目IDを渡すと科目別のアイコンを返す。命名をミスったなと思うのが、Nuxtにもmodulesという機能があるので、それを入れているように思えてしまうところです。libといった命名にしたほうが良かったかもしれません。
- types: 型定義をひたすら置いています
- utils: こちらもmodulesと同じでVueコンポーネントから切り出す処理ですが、より一般的な処理を置きます。以下に貼ったのはReact HooksライクなuseStateをVueで作った関数なのですが、こういう便利系かつ、その気になればスニペット単位でGitHubで公開しても良いような汎用的なものを置いています
import {
ref,
UnwrapRef,
} from '@nuxtjs/composition-api'
export const useState = <T>(initialValue: T | null) => {
const state = ref(initialValue)
const setState = (value: T) => {
state.value = value as UnwrapRef<T>
}
return [
state,
setState,
] as const
}
参考サイト
aspidaの解説記事
https://qiita.com/mejileben/items/11f206a51861bb404e1a
https://qiita.com/mejileben/items/6b15d91995927a8df262
microCMSの解説記事
https://zenn.dev/meijin/articles/09b6d3884559ba993751
composition-apiで作るカスタムHook入門
https://zenn.dev/meijin/articles/34b9482dbc856d2523ed
Components
$ tree -L 1 -d client/components
client/components
├── error
├── layouts
├── materials
├── objects
├── pages
├── parts
└── templates
componentsディレクトリ配下は、Atomic Designに則って行う方も多いと思いますが、個人的にはドメインオブジェクトに沿ったコンポーネントが作られるという考え方のほうが好きなので、オリジナルでディレクトリを切っています。
- error: 404.vueといったエラー時の表示内容のComponentを置きます
- layouts: ヘッダーとかフッターといった、ページレイアウトに関連するComponentを置きます
- materials: 何らかの素材や情報を表示するためだけのComponentを置きます。具体的に言うと、アイコンとかバナー、SNSシェアボタンといった固定の内容を表示するComponentです
- objects: ここにドメインオブジェクトに沿ったComponentを置きます。オンライン家庭教師サービスを運営する弊社だと、Teacher.vueとか、TeachingCourse.vueといった感じです。実際は、ページごとに同じTeacher.vueでも表示する実際のコンポーネントは変わるので、objects/teacher/teacher-detail/Teacher.vueといった感じで、オブジェクトの種類及びページごとにさらにディレクトリを細分化しています。具体例をあげると、同じ先生を表示するカードでも、先生一覧に出すカードと、先生詳細に出す類似の先生を示すカード、トップページに表示する先生のカードはデザインが異なるはずです
- pages: ページコンポーネントを置きます。Nuxtのpages/Hoge.vueとは対になる存在です
- parts: 特定のデータに依存せず、単なるUIの再利用しか目的としないコンポーネントです。Atomic Designにおけるatomsに最も近い気がします
- templates: こちらはほぼ使っていないのですが、繰り返しコンテンツを一定のルールでWrapするコンポーネントです。具体的には、ページネーション用のコンポーネントを置いています。ページネーションされている配列を渡すと、ページネーションと一緒に表示してくれるコンポーネントです
<template>
<div>
<div class="d-flex py-2 px-0 flex-column align-center">
<slot />
</div>
<pagination
v-if="paginateResponse.meta && paginateResponse.meta.last_page > 1"
:last-page="paginateResponse.meta.last_page"
:page="paginateResponse.meta.current_page"
class="d-flex justify-center"
/>
</div>
</template>
<script lang='ts'>
import {
defineComponent,
} from '@nuxtjs/composition-api'
import { Paginate } from '~/types/shared/api-response/paginate'
export default defineComponent({
components: {
Pagination: () => import('~/components/layouts/PaginationLinks.vue'),
},
props: {
paginateResponse: {
type: Object as () => Paginate<unknown>,
required: true,
},
},
})
</script>
所感
以下のようなメリデメがあります
メリット
- これだけ細かく分けると、どの定義にも該当しないコンポーネントにぶち当たってどこに置くか迷う、みたいなことはない
- 粒度ではなく、使いどころとかどういう特性のデータを表示するかで分けているので、比較的明確で、間違いにも気が付きやすい
デメリット
- ディレクトリの命名のこれでいいのか感。materialsとか、objectsとか、正直見慣れない言い回しなので、自分自身もREADME.mdをよく見直している
- objects配下の、ページに依存したコンポーネントの命名が大変。TeachingCourseForTopPage.vueみたいに、コンポーネント名にページ名を入れるのはどうなのか、みたいな
まとめ
割と軽率に細かくディレクトリを切るのが好きなのですが、その分命名が悩ましくなりますね。特にlibとかutilsといったディレクトリ名は、(その配下でより具体的な名前にするにせよ)ついつい使いがちになるし、どの単語がイメージにピッタリなのか分からないので苦心している気がします。
ディレクトリ名とはちょっと違う話ですが、BCD Designというコンポーネント設計指針が気になっています。
https://qiita.com/misuken/items/19f9f603ab165e228fe1
機会があれば学んで取り組んでみようと思います。