CSSのスコープ・モジュール化技術をざっくり追ってみる

PLAID Advent Calendar 2017 10日目。
プレイド @otolabです。

今回はフロントエンド寄り、CSSの話です。

CSSはWebの見栄え部分を担当する強力な言語ですが、現在のWebの複雑性を想定するような設計にはなっておらず、大規模なサイトでの運用や繰り返し改修を行ううちに混沌としてくる...というような課題も抱えています。

同様の問題はJavascriptも抱えており、こちらはAMD, CommonJS, ES6 Moduleなど、モジュール化技法の進展で改善してきました。

CSSも同様に、如何に問題を分割統治するか・疎結合にして正しく再利用できるようにするか、といった課題に対して、いろいろな方法が提案され進化しています。

今回は、それぞれの世代(?)で代表的な手法について幾つか例をあげて見ていき、最後に弊社KARTEで使っている手法について簡単に紹介します。（なおサンプルコードはいずれも実際のコードではなくて、模式的なコードです）


コンテナクラス
------------

古典的な方法で、コンテナ（入れ物）となる要素の子孫にのみ設定を適用できるように、CSSの構文を使います。欠点はコンポーネントの入れ子に対応できないこと、大規模になると管理しきれなくなることです。

LESSなどにはこのような記述をサポートする機能があります。


```less:LESS
.container {
  .text {
    color: red;
  }
}
```

このコードは次のように変換されます。

```css:CSS
.container .text {
  color: red;
}
```

```html:HTML
<div class="container">
  <p class="text">text</p>
</div>
```

ふつうですね。


[BEM](https://en.bem.info/)
------------

class名の命名規則によって、もろもろの問題を解決しようというアイデアです。このあたりでCSSの設計という概念が広まったと思います。

命名規則を適切に作ることで、「CSSの入れ子構造に頼らない」というあたりが面白いところです。きちんとページ全体を設計すれば、大規模で複雑なサイトでも混乱なく作り続けられるという点は画期的でした。

stylusなどはこの記法を上手くサポートします。

```stylus:STYLUS
.main-content
  &__text
    color red
```

変換すると次のようになります。

```css:CSS
.main-content__text {
  color: red;
}
```

HTMLには次のように適用します。

```html:HTML
<p class="main-content__text">text</p>
```

コピペにも強いんですよね、この書き方だと。


[vue-loader](https://vue-loader.vuejs.org/ja/features/scoped-css.html)
-----------

汎用的に使われるコンポーネントを提供しようと思った時、コンポーネントの「外側」と「内側」の干渉を如何に抑えるかというテーマが発生します。名前による一貫した設計も望めません。

vue-loaderでは、要素に固有のattributeを追加し、かつ、CSSにもそのattributeの追加して、コードの記述時に意識することなく、外側に干渉しないように制限する機能を提供しています。

class名が変わらないところがポイントで、外からのCSSを受け付けてしまう代わりに、script内から使うときにとくに意識する必要がありません。

パーツを提供することが目的なので、意図的に内部へ干渉させるケースを想定してこの作りなのだと思います。

```vue:.vue
<template>
  <p class="text">text</p>
</template>

<style scoped>
.text {
  color: red;
}
</style>
```

変換後（実行時）は次のようになります。

```css:CSS
.text[data-xxxyyyzzz] {
  color: red;
}
```

```html:HTML
<p class="text" data-xxxyyyzzz>text</p>
```

独自仕様のCSS拡張言語ではなく、CSSそのものをパースして設定を追加しているあたりもポイントですね。[PostCSS](https://github.com/postcss/postcss)が使われています。

なお最近は、次に解説するcss-modulesも変換モードとして選択出来るようになっています。


[post-cssmodules](https://github.com/css-modules/postcss-modules)
-----------------

[CSS Modules](https://github.com/css-modules/css-modules)公式の実装です。CSSとHTML両方のクラス名変換を行います。

ツールとしては[PostCSS](https://github.com/postcss/postcss), [PostHTML](https://github.com/posthtml/posthtml)などのプラガブルな汎用コンバータを利用して実装されています。それぞれCSS, HTMLのパーサを核としており、言語的に正確なパースを行った上で、その解析済みの内容をプラグインによって書き換えるという構成です。

HTMLに直接あてたclassは変換されず、`css-modules="classname"`と明示的に設定した場合のみ変換後のclass名が適用されます。

```css:CSS
.text {
  color: red;
}
```

```html:HTML（ソース）
<p css-module="text">text</p>
```

CSSとHTMLをセットにして変換します。

```json:変換テーブル
{
  'text': '_text__xxyy'
}
```

```css:CSS
._text__xxyy {
  color: red;
}
```

```html:HTML
<p class="_text__xxyy">text</p>
```

参考:
 * [CSSモジュール ― 明るい未来へようこそ](http://postd.cc/css-modules/)


Shadow DOM
-------------

HTMLの標準としてコンポーネントをサポートしようという規格です。

現時点ではまだ実用可能とは言えない感じですので、今回は割愛します。

次調べるのはこのあたりかなぁ、と思いつつ。



Simple CSS-Modules
---------------

最後に、弊社KARTEで使っているCSS-Modulesのバリエーションです（もしかすると同様の変換を行うツールは他にもあるかもしれません）。

KARTEでは「接客アクション」としてHTML, CSS, Scriptのセットを埋め込み先サイトに配信する機能がありますが、配信されるコードの生成にこの変換を施しています。

導入の背景としては、配信先サイト内のCSSがどのようなものか判らないため、外への流出を防ぐことはもちろん、外からの侵入を抑えることでデザイン崩れ、また配信アクション間の相互干渉を減らすことが重要だったことです。

CSS Modulesではcss-modules属性値を追加する仕様をとっていますが（`<div css-module="module.name"></div>`）、classを自動的にローカルスコープ付きに自動展開します。

半強制的にid, classの付け換えを行いたかったこと、また、実装時に意識せずに済むようにしたかったため、このような仕様になっています。


```css:CSS
.text {
  color: red;
}
```

```html:HTML（ソース）
<p class="text">text</p>
```

変換すると次のようになります。

```json:変換テーブル
{
  'text': '_text__actionId'
}
```

```css:CSS
._text__actionId {
  color: red;
}
```

```html:HTML
<p class="_text__actionId">text</p>
```


むすびにかえて
----------

最後にSimple CSS-Modulesの紹介をおこないましたが、CSSやHTMLをパースするツールも進化していて、意外と簡単に実装できます。実装テストとして作ったコードは[こちら（css-modules-auto-bind.js）](https://gist.github.com/otolab/0299481c14065dc9cc26ae9e481ccf36)。

HTMLの文法違反の修正方法など、現在ではパーサの動作についても規格化されているので、挙動にも安定感があります。BeautifulSoupで苦労した過去のある自分は、感動しながらコード書いていました...。