はじめに
はじめまして。フロントエンド開発グループに所属している岩釣です。
スタンバイの月間ユーザー数が1000万人を突破しました!(2023年4月末)
本記事ではそんなスタンバイのフロントエンド開発のコーディングガイドラインを紹介します。
なぜコーディングガイドラインを作るのか
コーディングガイドラインを作成して運用するメリットは以下です。
コードの可読性の向上
コーディングガイドラインを規定しチーム全員がコーディングガイドラインに基づいて開発することで、コードの統一性が保たれ、コードの可読性が向上し、保守性が高まります。チーム内でのコミュニケーションの円滑化
コーディングガイドラインが規定されている場合、チーム内でのコードの書き方についてのコミュニケーションが円滑になります。コードレビューなどでコードの書き方に関して議論する際に、コーディングガイドラインをベースにして議論することで、論点の整理や修正方針の合意が容易になります。プロジェクトの拡張性の向上
コーディングガイドラインを規定することで、コードの再利用性が高まります。また、コーディングガイドラインに基づいて開発することで、新たな機能の追加や既存機能の変更など、プロジェクトの拡張性を向上させることができます。
開発環境
- Nuxt.js 2.15
- Pinia 2.0
- TypeScript 4.5
- ESLint 7.32
- Jest
- Storybook
2022年11月にNuxt3の安定版がリリースされました。現在フロントエンド開発グループでは、Nuxt2からNuxt3へのバージョンアップを鋭意進めています。執筆時点ではまだバージョンアップが完了していないため、Nuxt2を対象とします。
基本方針
- 基本的にはVue.jsが定義しているスタイルガイドの優先度A ~ 優先度Cまでを守る
- すでに守れていないファイルに関しては必須とせず、リファクタリングを実施していく
- 新規作成ファイルにおいては全て守ること
- 例外的に守らなくて良いものもあるのでそれらは後述する
スタンバイでは、既存のファイルに関しては適宜リファクタリングを実施していく方針にして、徐々に改善していきました。
Vue.jsスタイルガイドの中で守らないルール
単一インスタンスのコンポーネント名
違和感がないので「The」というプレフィックスを付けないことを許容します。テンプレート内でのコンポーネント名の形式
ケバブケース(kebab-case)を使うことを許容しません。スタンバイではパスカルケース(PascalCase)に統一することで一貫性を重視しました。
ファイル命名規則
- .js .tsファイルはケバブケース(kebab-case)を用いる
- .vueファイルはパスカルケース(PascalCase)を用いる
- index.vue、error.vue、default.vueのようにNuxt.jsでファイル名が固定で機能が提供されている場合を除く
- composables/ディレクトリに配置するファイルは以下のルールに従う
- use-foo-bar.tsのように「use-」から始まること
- ケバブケース(kebab-case)を用いること
- 機能が想起できる名前にすること
- Storybookは{対象のコンポーネント名}.stories.jsにする
この辺は好みの問題で、デファクト・スタンダードが無さそうでしたので、チーム内で話し合って決めました。
Atomic Design
- AtomsとMoleculesの区別をつけずまとめてPartsと呼ぶ
- OrganismsのコンポーネントはXxxControlのようにサフィックスをControlにする
- TemplatesはNuxt.jsのlayoutsとみなす
- PagesはNuxt.jsのpagesとみなす
Atomic Designとは、ウェブデザインにおけるデザインシステムの手法の1つで、複雑なUIデザインを簡単に構築するための方法論です。 Atoms(原子)、Molecules(分子)、Organisms(有機体、生命体)、Templates(テンプレート)、Pages(ページ) の5つの構成要素からなります。
Atomic Designの利点は、再利用可能でスケーラブルなUIコンポーネントを作成できることです。これにより、複数のページやアプリケーションで同じUI要素を簡単に再利用できるようになります。 また、小さな部品から大きなコンポーネントを構築することで、保守性が高く柔軟性のあるデザインシステムを実現できます。
一般的に「アトミックデザインを長期的に運用していく上で、Atoms、Molecules、Organismsそれぞれのコンポーネントの定義を明確にすることは重要です。」とされています。
しかし、運用してみるとすぐに気付くのですが、コンポーネントの定義を明確にするのはかなり難しいです。 また、「Molecules」「Organisms」のような舌を噛みそうな単語は馴染めません。
よってスタンバイでは、「Atoms」と「Molecules」の区別をつけずまとめて「Parts」とし、「Organisms」という呼称も使っていません。
Parts(Atoms, Molecules)
- components/parts/に置く
- ファイル名は再利用性を考慮して特定のロジックを想起させないことが望ましい
- 基本的にコンポーネント自身のmarginやpositionやz-indexを持たない(利用側でセットする)
- 基本的にスマホ/PC両方で利用できる(レスポンシブ)
- PC専用・スマホ専用のコンポーネントを作成した場合、ディレクトリpc/やsp/を作成してそこに配置する。
- Partsは内部でPartsコンポーネント以外の利用不可🙅
- ビジネスロジックを書かない
- @clickイベントや@focusイベントはemitで上位に伝える(Controllerで処理する)
- ButtonやRadioなどの基底コンポーネントのファイル名はプリフィックスにBaseをつける
- 状態を持たない(store利用不可🙅)
- APIコール不可🙅
- Storybookを作成しすべてのpropsが操作できるようにする
もしデザイナーがCSS/HTMLに熟練している場合、デザイナーにPartsを作成してもらうと分業が捗ります。 そうでなかったとしても、必ずPartsのStorybookを作成するルールにすることで、UIパーツ単体での確認が可能となります。結果、デザイナーとエンジニアの意思疎通がスムーズになります。
Controller(Organisms)
- components/に置く
- ファイル名はXxxControl.vueにする
- Xxxの部分はロジックを想起させることが望ましい
- Partsからのemitを処理する
- ビジネスロジックを書く
- Composition APIで書く
- できるだけComposableにロジックを切り出して、Composableの単体テストを書く
- 状態を持ってよい(store利用OK🙆)
- APIコールOK🙆
- Storybookは書かなくてもOK
Templates
- layouts/に置く(Nuxt.jsのlayoutsの機能を提供する)
- ファイル名はXxxLayouts.vueにする
- ビジネスロジックを書かない
- 状態を持たない(store利用不可🙅)
- APIコール不可🙅
- Storybookは書かなくてもOK
Pages
- pages/に置く(Nuxt.jsのpagesの機能を提供する)
- ビジネスロジックを書く
- Composition APIで書く
- Composableにロジックを切り出して、Composableの単体テストを書く
- 状態を持ってよい(store利用OK🙆)
- APIコールOK🙆
- Storybookは書かなくてもOK
Composable
Composableとは
Vue.jsのComposableとは、Composition APIで書かれた単一の責任を持つ関数やロジックの塊を指します。
スタンバイではcomposables/ディレクトリを作成し、ロジックをuse-logic-name.tsのように別ファイルに切り出しています。
余談ですが、Nuxt3ではcomposables/ディレクトリ内のComposableは自動importされ<script setup>
内で利用できます。
Composableに切り出すことで以下のメリットがあります。
コードの再利用性の向上
コードを再利用できます。コンポーネントで同じロジックを繰り返し書くことがなくなり、コードの保守性や拡張性が向上します。また、開発時間を短縮できます。テストしやすいコード
テストしやすいコードを作成できます。ロジックの単体テストが行いやすくなります。柔軟なコード構造
ロジックを構造化できます。複雑なロジックを分割でき、コンポーネント内のロジックが複雑になることを防ぐことができます。また、コンポーネントの機能追加や変更に対応しやすくなります。
Composable実装ガイド
- composables/に置く
- ファイル名はuse-logic-name.tsのようにuse-から始まること
- ComposableはAtomic DesignのPagesとOrganismsのみが利用可能
- ロジックを再利用可能な状態にしてComposition APIで実装する
- Composableに切り出したロジックの単体テストを書く
- 単一の責務の単位でファイルを分割する
Composableのサンプルコード
以下は、単純なカウントアップロジックを実装したComposableの例です。
import { ref } from "@nuxtjs/composition-api"; export default function useCount() { const count = ref(0); const increment = () => { count.value++; } return { count, increment }; }
このComposableは、countとincrementという2つのプロパティを返します。countは現在のカウントを保持するrefオブジェクトであり、incrementはカウントを1つ増やす関数です。
このComposableを使用する場合、以下のように呼び出すことができます。
<template> <div> <p>Count: {{ count }}</p> <button @click="increment">Count Up</button> </div> </template> <script> import { defineComponent } from "@nuxtjs/composition-api"; import useCount from './useCount'; export default defineComponent({ setup() { const { count, increment } = useCount(); return { count, increment }; } }); </script>
このComposableの単体テストは以下のように書きます。
import { useCount } from './useCount'; describe('useCount', () => { it('increments count', () => { const { count, increment } = useCount() expect(count.value).toBe(0); increment(); expect(count.value).toBe(1); }); });
コンポーネント実装ガイド
PrettierやESLintではチェックしきれない、コンポーネントの実装に関わるガイドラインです。
HTML内で<template v-if>
を多用してHTMLを制御しない
複雑な表示条件ではcomputed()で書くとシンプルになり、且つテストが可能になります。<template v-if>
を組み立てて表示を制御するのは控えましょう。
悪い例🙅
<template> <a> <template v-if="keyword">{{ keyword }}</template> <template v-if="keyword && location" > - </template> <template v-if="location">{{ location }}</template> </a> </template>
良い例🙆
<template> <a>{{ condition }}</a> </template> <script lang="ts"> import { defineComponent } from "@nuxtjs/composition-api"; export default defineComponent({ setup() { const condition = computed(() => `${keyword}${keyword && location ? ' - ' : ''}${location}`) return { condition }; }, }); </script>
v-htmlを使用してHTMLを埋め込まない
v-htmlを使用すると入力されたHTMLがそのまま出力されるため、XSS攻撃を受ける可能性があります。そのため、基本的にv-htmlは利用しません。
利用する場合は、HTMLとして埋め込む文字列の安全性を担保するためサニタイズします。
悪い例🙅
<template> <p v-html="htmlContent"></p> </template> <script> export default { data() { return { htmlContent: "「<b>営業 未経験</b>」のような条件でも検索できます。" } } } </script>
コンポーネントのname属性
IDEやdevtoolsの補助が受けやすいので、Vueコンポーネントのnameプロパティを付与します。
良い例🙆
export default { name: "FooButton" }
コンポーネントのemitイベント
カスタムイベントをemitさせる際のイベント名は、ケバブケース(kebab-case)にします。
悪い例🙅
this.$emit("clickReset");
良い例🙆
this.$emit("click-reset");
this.$parentは使用不可
this.$parentを使って親にアクセスすると子と親が密結合してしまうのでNGです。
悪い例🙅
this.$parent.foo = 'bar';
Vue.filterの使用禁止
Vue.filterはVue3で廃止されたので、Vue2のプロジェクトでも出来るだけVue.filterを利用しません。
@clickを付与していい要素
div要素やspan要素に@clickを付与してもキーボード操作時に選択できないため、基本的に@clickはbutton要素とa要素のみに付与します。
CSS実装ガイド
コンポーネントの<style>
にscopedを付ける
Vue.jsはローカルスタイルとグローバルスタイルの混在が可能ですが、ローカルスタイルのみにします。
悪い例🙅
<style> /* グローバルスタイル */ </style> <style scoped> /* ローカルスタイル */ </style>
良い例🙆
<style lang="scss" scoped> .example { color: white; .dark { color: black; } } </style>
セレクタのルール
- ベースとなるスタイル以外では、基本的にはClassセレクタを用いる
- idセレクタは使用しない(詳細度が必要以上に上がるため)
- 要素セレクタは使用しない(影響範囲が読みづらくなるため)
- 詳細度を上げすぎない
- Classセレクタ3段階程度の詳細度を上限目安とする(ABテスト時に上書きが面倒になるため)
- セレクタの数を減らすのはパフォーマンスの観点からも有用
- セレクタ内で変数は用いない
- 重複した記述を避けられるなどのメリットはあるが、記述が複雑になるので避ける
Class名のルール
- ケバブケース(kebab-case)を用いる
- 命名は省略しない(冗長でも誰が見ても分かりやすいようにするため)
- 悪い例🙅:
<div class="bkmrk">
- 良い例🙆:
<div class="bookmark">
- 悪い例🙅:
- HTML要素に存在する命名を他の要素に使用しない(混乱を招くため)
- 悪い例🙅:
<div class="section">
- 悪い例🙅:
- 状態を示すものは 「is-」「has-」などを付ける
- 悪い例🙅:selected、error
- 良い例🙆:is-selected、has-error
- Partsのルート要素には parts-{ファイル名}のClass名を付ける
- 悪い例🙅:BaseButtonコンポーネントの場合
<div class="base-button">
- 良い例🙆:BaseButtonコンポーネントの場合
<div class="parts-base-button">
- 悪い例🙅:BaseButtonコンポーネントの場合
細かい記述ルール
- 「0」の後の単位は省略する
- 悪い例🙅:0px
- 良い例🙆:0
- カラーコードが省略可能な時は省略する
- 悪い例🙅:#ffcc00
- 良い例🙆:#fc0
- カラーコードは小文字にする
- 悪い例🙅:#FACB12
- 良い例🙆:#facb12
親から子のスタイルを上書きしない
- CSSの詳細度利用してスタイルを上書きする
- /deep/を使わない
- importantでスタイルを上書きしない
- plugin等で導入したOSSのUIにスタイルを付ける場合は仕方ないのでOK🙆
- ただし将来的にOSSのバージョンアップによって上書きしたスタイルが意味をなさなくなる可能性があるので上書きは推奨しない
TypeScript実装ガイド
enumは使用せずunionで定義する
enumはJavaScriptへのトランスパイルの際に「即時実行関数」に変換されるので、Tree-shakingできません。 そのため、enumは使用せずunionで定義します。
const Color = { RED: "red", BLUE: "blue", GREEN: "green", } as const; type Color = typeof Color[keyof typeof Color];
ESLintとPrettier
ESLintとPrettierはもはや説明するまでもなく、フロントエンド開発においてデファクトスタンダードとなっています。 Vue.jsのESLintプラグイン(eslint-plugin-vue)や、TypeScriptのESLint/Prettier(typescript-eslint)を導入して自動的にコーディング規約をチェックします。
eslintrc.jsは以下を基本として、rulesで細かい調整をしています。eslintrc.jsの調整もチームで話し合って徐々に調整するのが良いでしょう。
また、いちいちプルリクエストで指摘するのは面倒なので、IDEの設定でESLintやPrettierによる整形が自動的にかかるべきです。 スタンバイではVS CodeユーザーとIntelliJ IDEAユーザーがいます。複数のIDEが使われている場合、チーム内でそれぞれのIDEの設定方法を確認すると良いでしょう。
module.exports = { extends: [ "@nuxtjs/eslint-config-typescript", "plugin:vue/essential", "eslint:recommended", "@vue/typescript/recommended", "@vue/prettier", "@vue/prettier/@typescript-eslint", ], rules: { // 省略 } }
運用方法
- プルリクエストのテンプレートに、コーディングガイドラインのリンクを貼る
- プルリクエストのレビュー観点に、コーディングガイドラインに従っているかを加える
コーディングガイドラインが存在していても運用されなかったら意味がありません。 プルリクエストのテンプレートを工夫したり、プルリクエストのレビューのガイドラインも作った方がいいでしょう。
慣れてない時は細かい指摘が沢山でてくる場合もあります。レビュワーの意図が伝わりやすくするため、レビューコメントにはラベルを付けることを推奨しています。
[MUST]
:必ず対応してほしい[SHOULD]
:時間があれば対応してほしい[IMO]
: 自分だったらこうする[NITS]
: 小さな指摘点、typoとか[Q]
:単なる質問
まとめ
本記事では、スタンバイで運用しているコーディングガイドラインの一部を紹介しました。 すべてを載せきれていませんが、StorybookやJestにも実装ガイドがあります。 本記事を参考に、是非コーディングガイドラインの整備と運用をはじめてみてください。
スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com