🎃

フロントエンド環境のコードチェックをツールで自動化

2024/05/08に公開

はじめに

こんにちは、フロントエンド開発グループ Webフロント開発チームの田渕です。普段の業務ではウェルスナビのサービスサイトやコーポレートサイトなどのフロントエンド開発を担当しています。

この記事では、フロントエンド開発のコードチェックをツールで自動化する方法について紹介します。React や Vue.js といった JavaScript フレームワークを使ったフロントエンド開発の環境であれば比較的導入しやすい内容です。フロントエンド開発の環境構築をおこなう際にご参考いただければ幸いです。

進行中のプロジェクト

今回の内容は現在進行中のプロジェクトで新たな環境を構築した時の内容を基に紹介します。
現在進行中のプロジェクトとは、「複数リポジトリで共通の UI パーツのソースコードを一元化する」というものです。

同じ UI なのにソースコードが分散している

ウェルスナビのサービスサイトは、ログイン前の画面とログイン後の画面でリポジトリが分かれています。ログイン前の領域ではさらに以下のようにリポジトリが分かれています。

  • ログイン前全体( Laravel )
  • コラム( WordPress )
  • セミナー( WordPress )
  • よくあるご質問( Zendesk )
  • エラーページ( AWS S3 )

各リポジトリで開発しているシステムは使っているフレームワークやデプロイする環境が異なっており、特にヘッダーやフッターは表示・動作は同じですがソースコードは各リポジトリに書いてあり分かれた状態です。
ヘッダーやフッターは年に数回更新していますが、更新の都度各リポジトリのソースコードを修正することになり、工数がかかっていたのと修正ミスが発生するリスクがある状態です。

上記に対する工数の削減とリスク軽減という課題を解決する手段としてソースコードを一元化する、というのがプロジェクトの目的です。

コンポーネント化して読み込ませよう

プロジェクトの進め方としては、ざっくりと以下のイメージで取り組んでいます。

  1. 共通利用するソースコードを格納する新たなリポジトリを用意する
  2. 共通利用するソースコードをコンポーネント化する
  3. コンポーネント化したファイル群を複数のリポジトリに読み込ませる
  4. コンポーネントを読み込ませたものをリリースする

上記の 3. の複数のリポジトリに読み込ませる方法は Git のサブモジュールの仕組みを使って読み込ませようとしています。今回の内容からは逸脱するので割愛します。
今回は 1. と 2. のコンポーネント化した環境について開発者体験を向上させるように構築したのでご紹介します。

開発しやすい環境を整えよう

新しい環境では UI 部分を切り出すだけなのでフロントエンドだけで完結します。せっかくなのでフロントエンドの開発環境を整えて開発効率が良くなるように構築しました。

環境構築に使用したツール

フロントエンド開発で利用するツールはさまざまなものがありますが、今回はプロジェクトの要件を踏まえて以下のツールを選定しました。

言語 HTML, Sass (SCSS記法), TypeScript
フレームワーク Vue.js
静的コード解析( Linter ) ESLint, stylelint, HTMLHint, CSpell
コードフォーマッター Prettier
コード品質管理 husky, lint-staged
UI管理 Storybook
JavaScript トランスパイラー Babel
テスト Vitest, Jest, Vue Testing Library
※ Jest は storyshots のため導入

今回は、以下に絞って紹介します。

  • 静的コード解析( Linter )
  • コードフォーマッター
  • コード品質管理

静的コード解析

静的コード解析とは、コードを実行せずにコードの品質、信頼性、および、セキュリティを検証する作業のことをいいます。静的コード解析ツールまたは静的コード解析をおこなうことを Lint といいます。ツールを指す場合は Linter と呼ぶこともあります。

Visual Studio Code(以下「 VSCode 」)などのコードエディターでは、Lint を適切に設定することでコードエディター上にエラーや警告に関する部分に色付きの下線等が表示されます。
設定をおこなうことでエラーや警告などの内容がわかるようになります。ツールの設定を適切におこなうことで品質を担保できるのでとても便利です。
フロントエンドで静的コード解析として以下に対して設定しました。

Lint の対象 Lint ツール
TypeScript / JavaScript ESLint
Sass / CSS stylelint
HTML HTMLHint
スペルチェック CSpell

TypeScript / JavaScript

TypeScript や JavaScript は ESLint を使いました。

ESLint をインストールして .eslintrc.json ファイルを作成して設定します。

インストール方法は2種類あるので公式サイトを読んでお好みのほうでインストールすると良いかと思います。(インストール方法の詳細は割愛します)

# クイックスタート
npm init @eslint/config

# 手動でセットアップ
npm install --save-dev eslint

今回は手動でセットアップして .eslintrc.json ファイルを作成しました。

// .eslintrc.json
{
  "root": true,
  "extends": [
    "plugin:vue/vue3-essential",
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier",
    "eslint-config-prettier",
    "plugin:storybook/recommended"
  ],
  "parser": "vue-eslint-parser",
  "parserOptions": {
    "parser": "@typescript-eslint/parser",
    "ecmaVersion": "latest",
    "project": "./tsconfig.json",
    "extraFileExtensions": [
      ".vue"
    ]
  },
  "plugins": [
    "@typescript-eslint"
  ],
  "env": {
    "jquery": true,
    "node": true
  },
  "ignorePatterns": [
    "**/*.js",
    "vite.config.js"
  ],
  "rules": {
    "complexity": [
      "error",
      10
    ]
  }
}

具体的な使い方は公式サイトをご参照いただくとして、要点を以下にまとめます。

  • Vue.js, TypeScript, Prettier, Storybook を使う設定
  • 既存リポジトリで jQuery を使用しているため使えるように設定
  • プログラムが複雑になりすぎないように complexity を設定

Vue.js, TypeScript, Prettier, Storybook を使う設定

既存の各リポジトリでは技術的負債がある状態ですが、新しく読み込ませるコンポーネントは技術的負債をなくした状態にしたいという思いがありました。
そこで新しいコンポーネントは要件とフロントエンドのトレンドを踏まえてツールの技術選定をおこないました。

既存リポジトリで jQuery を使用しているため使えるように設定

技術的負債を解消する一環で jQuery も新しいコンポーネントでは使用しない方針としました。
しかし、ESLint ではあえて jQuery を使用できるように設定しました。理由は、既存のリポジトリからそのまま持ってきても動作するようにしておき、ソースコードを持ってきてからリファクタリングできるようにするためです。

プログラムが複雑になりすぎないように complexity を設定

なるべくソースコードはシンプルにしたいのでルールに complexity を設定しました。
循環的複雑度といってプログラムの複雑度を測る指標があり、complexity を設定しておくことで閾値を超えるとエラーになるようにしました。
設定した数値はプロジェクトの進行具体に応じて適宜、変更予定です。

Sass / CSS

Sass の Lint は stylelint を使いました。

stylelint をインストールして .stylelintrc.json ファイルを作成して設定します。

npm init stylelint
// .stylelintrc.json
{
  "plugins": ["stylelint-scss"],
  "extends": [
    "stylelint-config-standard-scss",
    "stylelint-config-property-sort-order-smacss",
    "stylelint-config-prettier"
  ],
  "rules": {
    "selector-class-pattern": "^[a-z]+[0-9a-z]+(?:-[a-z]+[0-9a-z]+)*((?:__[a-z]+(?:-[a-z]+[0-9a-z]+)*)|(?:--[a-z]+(?:-[a-z]+[0-9a-z]+)*))*$",
    "color-function-notation": "legacy",
    "declaration-no-important": true,
    "max-nesting-depth": 3,
    "selector-disallowed-list": [
      "/#./",
      "/^[a-z]+/"
    ]
  },
  "ignoreFiles": ["src/stories/**/*.css"]
}

こちらも具体的な使い方は割愛して要点を以下にまとめます。

  • scss ファイルを使えるように設定
  • プロパティ順を SMACSS 手法に基づいた順番で並び替え
  • BEM の命名規則に従っていなかったらエラーになる設定
  • ネストの深さが3層を超える場合はエラーになる設定
  • 英字小文字で始まるセレクタはエラーになる設定

scssファイルを使えるように設定

スタイルに関しては Sass を採用しました。
CSSの仕様は近年活発にアップデートされています。以前できなかったネストができるようになったりするなど日々使いやすくなっています。
しかしながらSassには、CSSにはない便利な機能がまだまだあります。
PostCSS を使った方法もありますが、パッケージがたくさんあり学習コストが高かったり属人化する可能性があったりします。今回はメンバーが普段から使い慣れている Sass にを採用して PostCSS の採用は見送りました。

プロパティ順を SMACSS 手法に基づいた順番で並び替え

プロパティ順を並び替えるツールはいくつかありますが、今回は SMACSS 手法に基づいた順番で並び替えるものを採用しました。
SMACSS とは、CSS 設計の手法の一つで、CSS を以下のカテゴリに分類します。

  • ベース
  • レイアウト
  • モジュール
  • 状態
  • テーマ

SMACSS の詳細については今回の内容から逸脱してしまうので割愛します。
プロパティ順を並び替えるツールをいくつか確認して、以下の理由により採用しました。

  • プロパティ順が視覚順におおむね並ぶようになっている
  • SMACSSという有名な手法に乗っかれる(独自に用意したものではない)

今回はstylelint-config-property-sort-order-smacss を使いました。
こちらは css-property-sort-order-smacss のプロパティ順を採用しているのでそちらを眺めてもらえばどのようにプロパティ順を並び替えるか雰囲気がわかるかと思います。

BEM の命名規則に従っていなかったらエラーになる設定

stylelint の selector-class-pattern というルールでクラスセレクタのパターンを制限できます。
正規表現が使えるので、正規表現が使える範囲であればLintの設定がおこなえます。
あらかじめ命名規則を決めておき、正規表現で表現できるものであればルールを設定すると強く制限できます。
このプロジェクトでは命名規則として BEM( MindBEMding )を採用しました。

BEM を正規表現で表現するのには苦労しましたが、想定している範囲でのパターンを網羅した正規表現を設定しました。(下記画像の結果欄の赤字部分が対応しています)

ネストの深さが3層を超える場合はエラーになる設定

Sass のネストは便利なのでよく使用しますが、使い方に気をつけないとネストがかなり深くなってしまい修正したいセレクタを探し当てるのに時間がかかることがあります。
設計の話に少し触れますが、ネストの大元を画面サイズ(メディアクエリ)で分けて書いている場合、修正時に修正対象の要素が画面サイズごとに離れてしまい作業効率の低下や修正ミスの誘発などの課題があります。
CSS の書き方として、以下のルールを設けました。

  • ネスト内に子要素は含まない(疑似要素は除く)
  • メディアクエリは同一の対象内にネストして記述する

こうすることで以下の効果を見込んでいます。

  • 対象の要素に関することはネスト内に収まっているので見通しが良くなる
  • 対象の要素が不要になった時に削除しやすい(削除対象がネスト内に収まっている)

以上を実現するとネストの深さは3層までに抑えることができるだろうという仮説を立てて設定しました。

英字小文字で始まるセレクタはエラーになる設定

英字小文字で始まるセレクタとは、HTML 要素のことであると仮定して設定しました。
今回はコンポーネント単位で作成するので HTML 要素に対して直接指定することがない、というのが前提にあります。
サイト全体に設定するスタイルも存在する場合、コードを無視する設定を入れると良いかと思います。
上記を設定することで a, img, li 等の要素にもスタイルを設定するのであれば class を振ることになります。そうすることによって、意図しないスタイルの上書きを防ぐことができます。

HTML

HTML の Lint は HTMLHint を使いました。
HTMLHint をインストールして .htmlhintrc ファイルを作成して設定します。

npm install --save-dev htmlhint
// .htmlhintrc
{
  "doctype-first": false,
  "attr-lowercase": true,
  "attr-no-duplication": true,
  "attr-no-unnecessary-whitespace": true,
  "attr-unsafe-chars": true,
  "attr-value-double-quotes": true,
  "attr-whitespace": true,
  "alt-require": true,
  "tag-pair": true,
  "tag-self-close": true,
  "tagname-lowercase": true,
  "tagname-specialchars": true,
  "src-not-empty": true,
  "id-unique": true,
  "space-tab-mixed-disabled": "space",
  "spec-char-escape": true
}

HTMLHint は一般的なルールやコードフォーマット等を解析してくれます。
HTMLHint をインストールしていれば設定ファイルがなくてもデフォルトの設定で解析してくれます。
コンポーネントの html ファイルで Lint をおこなった場合、<!DOCTYPE html> が最初の行にないので doctype-first でエラーになりました。このエラーは無効にしたいので設定ファイルを作成してルールを記述しました。
動作確認をして気づいたのですが、設定ファイルを作成して1つでもルールを追加すると他のルールがすべて無効になってしまうようです。そこで改めて必要なルールをすべて記述しました。
HTMLHint を利用することで、例えば以下のような凡ミスを発見できます。

  • 不要な半角スペースが混じってしまう
  • 閉じタグを忘れてしまう
  • インデントにタブとスペースが混じってしまう

スペルチェック

スペルチェックの Lint は CSpell を使いました。
CSpell をインストールして .cspell.json ファイルを作成して設定します。

npm install --save-dev cspell@latest
{
  "version" : "0.2",
  "language" : "en",
  "words" : [
    "WealthNavi",
    // 省略
  ],
  "dictionaries": [
    "softwareTerms",
    "misc",
    "companies",
    "typescript",
    "node",
    "html",
    "css",
    "fonts",
    "filetypes",
    "npm"
  ],
  "ignorePaths" : [
    // 省略
  ],
  "ignoreRegExpList": [
    "/\"data:image/+.*\"/"
  ]
}

CSpell を導入することによって誤字・脱字を防ぐことが期待できます。
固有名詞などの機械が判断できないものは words に追加していくことでエラーを無視できます。
dictionaries でエラーを無視する関連用語を指定しておくことができます。
エラーを無視する用語を登録する一手間はかかりますが、登録してしまえば後は設定した内容をもとに自動で検知してくれます。設定してしまえば、誤字・脱字は機械に任せることができるので他のことに注力できます。

コードフォーマット

コードフォーマットは言語によっていろんなコーディングスタイルがあるのでプロジェクトごと・人によって書き方が変わってきやすいものかと思います。
複数のプロダクトコードに触れているとプロダクトごとにフォーマットが違う場合、うっかり異なるコードフォーマットで書いてしまいプロダクト全体の一貫性がなくなってしまう可能性があります。
コードを自動で整形するツールを導入することにより、違うコードフォーマットで書いても自動修正されプロダクト全体の一貫性が保たれます。
今回は Prettier を使用しました。
Prettier を導入することで「コードレビューでスタイルについて議論する必要はありません」と Prettier が主張しています。
Prettier をインストールして必要に応じて設定ファイルを作成して設定します。

npm install --save-dev --save-exact prettier

Prettier の公式サイトによるとオプションの哲学として設定を追加するのはあまり推奨されていないようです。
今回は1つだけ追加しました。

// .prettierrc.json
{
  "singleAttributePerLine": true
}

上記は HTML, Vue, JSX において属性を1行に1つだけにする設定です。
Vue.js のスタイルガイドでは優先度Bで「複数の属性をもつ要素は、1行に1要素ずつ、複数の行にわたって書くべきです。」とあります。
このプロジェクトは Vue.js のスタイルガイドに沿ったコーディングスタイルにする方針として Prettier のこの設定で対応できました。

VSCode でコーディングして保存時に自動でフォーマットをかけるには、 以下をおこなう必要があります。

  • 拡張機能のインストール
  • .vscode/settings.json への設定

拡張機能は以下をインストールします。

.vscode/settings.json へ以下を設定しました。

// .vscode/settings.json
{
  "editor.formatOnPaste": false,
  "editor.formatOnSave": true,
  "editor.formatOnType": false,
  "standard.autoFixOnSave": true,
  "scss.validate": false,
  "css.validate": false,
  "stylelint.validate": ["css", "scss"],
  "stylelint.configFile": "./.stylelintrc.json",
  "[html]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[scss]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
      "source.fixAll.stylelint": true
    }
  },
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.codeActionsOnSave": [
      "source.addMissingImports",
      "source.fixAll.eslint"
    ],
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.codeActionsOnSave": [
      "source.addMissingImports",
      "source.fixAll.eslint"
    ],
  },
  "eslint.validate": [
    "vue",
    "javascript",
    "typescript"
  ]
}

設定が正しければ VSCode でファイルを保存時に自動でフォーマット修正がかかります。
これで設定したルールに沿ってエラーや警告を表示して保存時にフォーマットされます。

Lint のエラーを取り込まないようにする

上記の設定をおこなうことである程度の品質を向上できました。
しかし、この状態では Lint はエラーを表示するだけで気づかないと本番環境にマージできてしまいます。
そこで今回は Git のコミット前に自動で Lint を実行してエラーが出ていたらコミットを中止する仕組みを導入しました。
自動で Lint を実行するには実行コマンドを用意するが必要があります。
package.json の npm scripts に実行コマンドを用意します。
実行コマンドを手動でまとめておこなう場合のコマンドも用意します。まとめて実行するのを簡潔にするため npm-run-all というパッケージをインストールします。公式サイトに沿ってインストールします。
インストールできたら package.json に各 Lint を実行するコマンドを用意します。

// package.json
{
  "scripts": {
    "lint": "npm-run-all -p lint:*",
    "lint:html": "htmlhint src/**/*.html",
    "lint:css": "stylelint '**/*.scss'",
    "lint:js": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,ts,stories.ts,tsx --fix --ignore-path .gitignore",
    "lint:spell": "cspell lint -c .cspell.json --show-suggestions --dot ."
  }
}

各 Lint で実行できるように用意しました。
コミット前に Lint を実行するには以下のツールを使用します。

公式サイトに従って必要な設定をおこなうことで利用できます。
今回は以下のバージョンを使用しました。

  • husky 8.0.3
  • lint-staged 15.0.2

上記の環境だと以下の手順で環境が整います。

  1. husky の設定

    # husky のインストール
    npm install husky -D
    
    # husky コマンドの実行
    npx husky install
    
    # husky の pre-commit ファイルの生成
    npx husky add .husky/pre-commit "npx lint-staged"
    
  2. lint-staged の設定

    # lint-staged のインストール
    npm install --save-dev lint-staged
    
    // package.json
    {
      "lint-staged": {
        "*": "your-cmd" // ここに対象ファイルと実行したいコマンドを設定します
      }
    }
    

今回は Lint ごとに以下の設定をおこないました。

// package.json
{
  "lint-staged": {
    "src/**/*.html": "yarn lint:html",
    "src/**/*.scss": "yarn lint:css",
    "src/**/*.{vue,ts,tsx}": "yarn lint:js",
    "*": "yarn lint:spell"
  }
}

以上の設定をおこなうことで Git のコミット前に Lint を自動で実行できるようになりました。

さいごに

今回の設定をおこなうことによって Lint とコードフォーマットの面において品質がある程度担保される環境になりました。Lint の設定は他にも有用なものがたくさんあるので興味のある方はぜひ調べてみてください。

品質を向上させる仕組みは他にも様々な方法があるので、プロジェクトの要件やチームメンバーのスキルセットや学習コスト等を踏まえて検討し最適な方法を選択していただくのが良いかと思います。
今回の内容のどれかに合致するものがあればぜひご参考にしていただければ幸いです。

明日は、品質向上 柳澤 の「入社直後に関わったプロジェクトのテスト分析での取り組み」です!
お楽しみに!


📣ウェルスナビは一緒に働く仲間を募集しています📣

https://hrmos.co/pages/wealthnavi/jobs/1839975184289607718


筆者プロフィール
田渕 拓弥(たぶち ひろや)
2022年2月ウェルスナビにフロントエンドエンジニアとして入社。
小さなウェブ制作会社で下積みをおこない、その後フロントエンドエンジニアとして事業会社を数社経験。
現在は UI 構築を得意としつつフロントエンドの開発・運用業務に従事。
趣味はチェスと読書とモノづくり。

WealthNavi Engineering Blog

Discussion