CSS Modulesを色々調べてみた話


この記事は[Uniposアドベントカレンダー2022](https://qiita.com/advent-calendar/2022/unipos "Uniposアドベントカレンダー２022")の９日目の記事です！

フロントエンド開発をメインに働いています。
今回社内でも共有したCSSの歴史のざっくり話とCSS Modulesについて書いていこうと思います。

# CSSの大まかな歴史
## 1. CSSっていつ生まれたの？

1994年、WWW（World Wide Web）生誕の地である
欧州原子核研究機構
（CERN：Conseil Européen pour la Recherche Nucléaire）
に勤務するホーコン・ウィウム・リーさんにより提唱されました。 

ちなみにOperaのCTOをしていた人でもあるそうです。

そこからW3CやWHATWG(Web Hypertext Application Technology Working Group)のような団体が仕様策定をするようになりました。

## 2. 現在のバージョンは？

実はバージョンという概念はなくレベルで区分けされます。

### 1996年: Level1(CSS1)

フォントの装飾変更やホームページの背景、テキストやリスト、ボックスのプロパティが指定可能。

### 1998年: Level2(CSS2)

CSS1の上位互換がこの「Level2」です。表示する媒体によって自動的にスタイルシートを変更にできるようにするなどの改良が加えられた。
仕様書の定義が不明瞭であったことから、実用的ではないという結果に。。

### 2011年: Level 2.1(CSS2.1)

Level2の改訂版。ユーザー側で相互運用を認められなかった機能は削除され、汎用性の高い言語へと進化。
具体的には「音声ブラウザの対応」「自動スタイルシート変更」「印刷用端末への対応」など。

### Level3(CSS3)

「モジュール（まとまり）」といった概念が導入され、ユーザーはモジュールを選択可。2018年時点で15のモジュールが公開中。
2021年時点ではほとんどのWEBサイトがこのLevel3のCSSを使用している。
実は、Media Queriesのような機能も2012年6月に勧告されていて、現在のWebのスタイルでレスポンシブなどができるようになったのもこの頃から。

### Level4(CSS4)

2021年時点において、Level3のモジュールでも勧告に至ってないものも数多く、あくまでLevel4は草案段階。いくつかの草案はすでに公開中。
一部のブラウザでは先に実装されているものも多い。

## 3. CSSの使いづらさからの発明

CSSはDOMのネストに合わせて、記載がしづらかったり、変数や関数がなかったために、そこに不満を持つ人々から、コンパイルしてCSSへ変換しようというものが登場した。

### Sass

すでにデファクトスタンダードな立ち位置で上記機能があり、登場時(2006年)かなり便利なものでした。
コンパイルが必要であり、コンパイラはつい最近まで数種類あって、対応できることもわずかだが違っていた。現在はdart-sassが公式推奨でそれ以外はメンテナンスや仕様の問題から非推奨になっている。
ちなみに記法はSassとSCSSがある。多くはSCSSを使っているかなという感触。

```scss
$font: Helvetica, sans-serif;
$primary-color: #ccc;
$white-color: #fff;

body {
  font-family: $font;
  color: $primary-color;
    .container {
        background-color: $white-color;
    }
}
```

コンパイル後

```scss
body {
  font-family: Helvetica, sans-serif;
  color: #333;
}
body .container {
    background-color: #fff;
}
```

### LESS

Sassに触発された言語。現在見かけることない。

```scss
#header {
  background-color: black;
  .navigation {
    font-size: 12px;
  }
  .logo {
    width: 300px;
  }
}
```

コンパイル後

```scss
#header {
  background-color: black;
}
#header .navigation {
  font-size: 12px;
}
#header .logo {
  width: 300px;
}
```

### Stylus

Node.js向けに開発された。こっちも現在は更新されてないですが、LESSよりは稀に目にする感覚。

```scss
body
  color #f00
  p
    color #000
```

コンパイル後

```scss
body {
  color: #f00;
}
body p {
  color: #000;
}
```

## 4. 設計手法

巨大なサービスでCSSを書いていると「クラス名がかぶる！」ということで、そうした辛みを解消するために、いろいろな設計手法が出ました。

### OOCSS

```html
<!-- ベーススタイル(btn) 色(btn-primary, btn-successなど) -->
<button type="button" class="btn btn-primary">Primary</button>
<button type="button" class="btn btn-success">Success</button>

<!-- サイズ -->
<button type="button" class="btn btn-primary btn-lg">Large button</button>
<button type="button" class="btn btn-primary btn-sm">Small button</button>

<!-- 余白 -->
<div class="mt-1 px-2 py-3"></div>
```

構造・見た目を分離して定義することが思想的には強くて、クラス名・ルールを覚えて仕舞えば、レゴみたいに組み合わせることができることが強みです。
BootStrapとかも、この思想が引き継がれているかなと。(最近だとtailwind CSSも)

### SMACSS

コンポーネントの抽出作業をしやすくするために、指針となるカテゴリを提案
そのカテゴリごとでルールを決めて、カテゴリ外からは影響がないことを担保していた。

構成(カテゴリ)

- Base
    - デフォルトのスタイルでサイト全体に影響
    - 要素セレクタに対して定義する
    - [normalize.css](https://github.com/necolas/normalize.css)などもBaseに該当
- Layout
    - ページをエリアごとに分割
    - `header` `footer` `main` `sidebar` など
    - 命名規則
        - `l-` で始める( `l-header` )
- Module
    - 再利用可能なパーツ
- State
    - `module` `layout` の状態
    - `active` `hidden` など
    - 命名規則
        - `is-` で始める( `is-active` )
- Theme(任意)
    - デザインテーマ

### BEM

**Block, Element, Modifierで構成**

構成
- Block
    構成のルートとなる要素
    
- Element
    Block内の子要素
    
- Modifier
    変化した状態
    

```
.navigation { }
.navigation--left { }
.navigation__content { }
.navigation__item { }
.navigation__item--active { }
```

結構プロジェクトによって、ルール崩壊を起こしているイメージがBEMでした。厳格さを要求するため、ルール浸透が難しい設計でした。

### FLOCSS

構成
- Foundation
    SMACSSのBaseと同じ
    
- Layout
    SMACSSのLayoutとほぼ同じ
    SMACSSと違いgridはLayoutに含めない
    
- Object
    - Component
        - 再利用可能なパーツ
        - `grid` `button` `form` `media` など
        - 命名規則
            - `c-` で始める( `c-button` )
    - Project
        - プロジェクト固有のパーツ
        - `articles` `ranking` `comments` `profile` `login-form` など
        - 命名規則
            - `p-` で始める( `p-profile` )
    - Utility
        - わずかなスタイル調整のための便利クラス
        - `display` `margin` など
        - 命名規則
            - `u-` で始める( `u-mbs` )

昔を振り返ると結果として、FLOCSSにみんな行きついてたかなと。
CSS Modules・Atomic Design以前の最後にフォーカスされた設計方法。

## 5. CSS ModulesとAtomic Design

### CSS Modules

SPAが誕生して、ReactやVue、Elmが出始めた2015年あたりにcss-loaderの実験的な機能として、CSS Modulesが作られました。

今までCSS側で命名がかぶることを考えて、ものすごい名前をつけることに躍起になってましたが、それはコンパイル時にかぶらないようにすればいいのでは？という発想で、とても画期的なものでした。
それによって、現在上記にあるような設計手法は比較的廃れていく傾向になってます。

```scss
.container {
	width: 100%;
}
```

```jsx
import style from '../xxx/xxx.scss';

const Container = () => {
	return (
		<div class={style['container']}>
				...
		</div>
	)
}
```

上記サンプルですが、このようにすることで、containerがcss-loaderでハッシュ値などに変換され、React側もそれに合わせてハッシュ値を使用できるようになりました。(仕組み端折ります)

これによって、命名規則の縛りを意識せずともスタイルをかけるようになりました。

### Atomic Design

CSS ModulesやSPAが出たことによって、パーツのカプセル化ができるようになりました。

それによって、どの粒度でカプセル化するのかという考え方としてデザインシステム含めてAtomic　Designという考え方が出ました。


これによって、再利用可能性を高めていくことができ、組み立てるだけで生産ができるという状態になってきました。
UI管理ライブラリのStorybookなどもその動きをアシストすることになったと思います。

## 6. 現在のCSS

最近だとCSS in JSだったり、CSS自体も新しい機能を備えてCSSだけでカプセル化できるような流れになってきています。

これからスタイルは簡単に書きやすくなったり、できることも増えていくと思います！


# CSS Modulesのお話
[css-loader](https://github.com/webpack-contrib/css-loader "css-loader")を使って、CSS Modulesを使うことができます。SCSSを使うならsass-loaderとかを使います。

Reactとかは元々使いやすいように用意されているので、あまり設定などは意識しないかもしれません。
ただcss-loaderのことを知っておくと、どのように実現されているかのイメージがつくキッカケにはなります。

## 設定
```js
module.exports = {
  module: {
    rules: [
      {
        test: /\.s[ac]ss$/i,
        loader: "css-loader",
        options: {
          modules: {
              auto: boolean | regExp | ((resourcePath: string) => boolean);
              mode:
                | "local"
                | "global"
                | "pure"
                | "icss"
                | ((resourcePath) => "local" | "global" | "pure" | "icss");
              localIdentName: string;
              localIdentContext: string;
              localIdentHashSalt: string;
              localIdentHashFunction: string;
              localIdentHashDigest: string;
              localIdentRegExp: string | regExp;
              getLocalIdent: (
                context: LoaderContext,
                localIdentName: string,
                localName: string
              ) => string;
              namedExport: boolean;
              exportGlobals: boolean;
              exportLocalsConvention:
                | "asIs"
                | "camelCase"
                | "camelCaseOnly"
                | "dashes"
                | "dashesOnly"
                | ((name: string) => string);
              exportOnlyLocals: boolean;
          },
        },
      },
    ],
  },
};
```


全部を説明すると多くなるので、興味深い内容をいくつか選んで書いていきます。
コードなどは公式のものを参考に記載しています。

## scope
`local` `global` `pure` `icss` のスコープがあります。
icssはやや性質が違うので、公式に説明を任せるとして、残りの3つを扱えたらと思います。
([参考](https://github.com/css-modules/icss "icss"))

- pure
    Reset CSSといった変化されるべきでないスコープ
- global
    全体のCSSファイルに影響するスコープ
- local
    ファイルに閉じた影響のスコープ

というような意味合いにざっくりとなってきます。

## mode
上記のscopeを選ぶことが可能です。関数でファイル名のパスを用いて、細かな設定ができるようになっています。
なので、一部部分からCSS Modulesの導入を始めるといった時にglobal/localを選んでいけるようになります。

以前、導入した経験があったのですが、その際もこれを駆使して影響範囲を限定していき、リファクタリングを進めていくという活動が可能になりました。


## local
scopeがlocalなものに対しての様々な設定が可能です。
``` js
localIdentName: string;
localIdentContext: string;
localIdentHashSalt: string;
localIdentHashFunction: string;
localIdentHashDigest: string;
localIdentRegExp: string | regExp;
getLocalIdent: (
    context: LoaderContext,
    localIdentName: string,
    localName: string
) => string;
```

### localIdentName
色々な組み合わせができるのですが、クラス名やフォルダ・パス・ファイル名・ハッシュなどが可能です。
ReactでCSS Modulesやっている場合にハッシュ値になっていたりするのはここのおかげなんですね。
これによって、CSS名設計のような複雑な作業から解放されたわけです。本当にありがたいですね。

ハッシュ値の設定については、localIdentHashSalt, localIdentHashFunction, localIdentHashDigest, localIdentHashDigestLength, localIdentContext, resourcePathをいじることで調整可能です。
実際にドキュメントを見ると色々なことができるんだ！と驚きになるので、ここでは省略しますが、ぜひ読んでいただけたらと思います。

特に調べてみるまでは意識しませんでしたが、localIdentHashDigestLengthは値が何桁かというのを選べるのですが、スケールしていくにつれクラス名衝突の可能性を感じてしまうことがあると思いますが、そこで簡単に調整できるというのは、面白かったりします。

### getLocalIdent
ここでクラス名をどのように生成するかを関数ベースで決めれます。built-inで使用できるフレームワーク・ライブラリを使用していると意識しなくて良いですが、CSS Modulesによって変化したクラス名をHTML/JSと結びつける場合に、約束を作る時に必要なものになってきます。
ここでの仕組みとJavascript側を合わせることで、開発時にはクラス名をCSSと同じものを使用していき、コンパイル後にはここでの約束通りに吐き出される仕組みになります。


## exportLocalsConvention
"asIs" | "camelCase" | "camelCaseOnly"| "dashes"| "dashesOnly" | ((name: string) => string)
から選択ができます。

例えば、"camelCase"を選んだ場合
```scss
.btn-primary {
    color: $primary-color;
}
```

```css
.btnPrimary {
    color: xxx;
}
```

となります。何が嬉しいかというと、camelとdashesで混ざっているCSSファイルをReactで使いたいとなった時に、これを設定しておけば

```jsx
<div className={btnClass["btnPrimary"]} >
    xxx
</div>
```
のようにjsx側の記載でどちらの形式か迷わずに進めれます。
プロダクトが古くなった時のリプレイスなどの時には、非常にありがたい機能になりそうです。
(後からリファクタリングはもちろん必要ですが。。)
関数での指定も可能なので、さまざまな使い方ができそうです。


# まとめ
フロントエンドでスタイルを書くという作業は、ここ数年で本当に進化しました。
今回の記事で、どういった流れでCSSの変化があったのか、今ある技術の裏にはどういったものがあるのかを振り返ることで、今後のより良い開発につながるのかなと思います。技術選定の際も、まだ手法が多いので歴史を見て決断ができるとより良いですね。

まだまだ全ての開発環境がモダンな状態でなかったり、webpackをガンガン使えないこともあると思うので、そうした方にお役に立てたら嬉しいです。
それでは、明日の[アドベントカレンダー](https://qiita.com/advent-calendar/2022/unipos "カレンダー")もお楽しみに！