CSS Advent Calendar 2017 18日目の記事です。
問題点
皆さんCSSで苦しんでますか?はい、私も苦しんでいます。
UIのコンポーネント実装が主流になってきて、JavaScript側が発達したおかげでCSSの各種問題を解決する仕組みもいろいろと出てきました。CSS in JSはそれの代表的なものですが、個人的には以下のような点に少し問題を感じています。
- 完全に埋め込まれてしまうとデザイナー・エンジニア間の協業難易度が増す
- CSS側の実装難度が増す(便利な書き方が制限されがち)
これを解決するために、バンドラー側の設定を変更してうまいことimportするものもありますが、それはそれで設定ファイルが複雑化したりビルドプロセスに影響が出たりと、どうにもJSとCSSの悪魔合体感が問題に感じます。
本当に解決したいこと
CSSが本質的に解消したいことは、やはり常にグローバル変数上で構築しなければいけないという制約があることと考えました。
各種UIが特定のスコープを持つことによって、考慮しなければならないことを圧倒的に減らせます。
(そもそもCSS in JSやShadowDOMを活用する最大の利点もそこにあると思う)
そこで、なるべくJSとCSSを疎結合に保ったままスコープだけをうまく得る方法が無いかを模索してみました。
実践したこと
以下の3点を活用することで、JSとCSSを合体せずに擬似的なスコープを実現してみました。
- UIコンポーネントの管理・分類方法
- ネームスペースとしてランダムな文字列(prefix)付与
- PostCSS
AtomicDesignやデザインシステムなど、UI構築を構造化して考えることでコンポーネントに一意な名前をつけてあげることは容易になりました。
コンポーネントの役割をそのままclass名に使用します。
// UIをカテゴリ毎に分けて詳細にする(Material-UIの例)
button/flat-button -> button_flat-button
button/raised-button -> button_raised-button
list/simple-list -> list_simple-list
list/contact-list -> list_contact-list
// AtomicDesignを活用する
atoms/checkbox -> atoms_checkbox
atoms/primary-button -> atoms_primary-button
molecules/info-table -> molecules_info-table
molecules/checkbox -> molecules_checkbox // 間違えて同じ名前にしても、ディレクトリが異なるのでOK
実際に構築していくときに、ディレクトリを切ってコンポーネント名でファイルを保存していけば事実上同名のコンポーネントは生まれません。
あとはこれに対してランダムに生成した文字列を一律付与して外部からの干渉を防いでしまえば、事実上スコープ化は完成です。
手順としては以下です。
- ランダムな文字列を生成する
- UIコンポーネントに対して文字列付きclassを指定する
- 文字列が付与された状態でCSSを作成
ランダムな文字列を生成する
module.exports = function() {
return '0t3naiN1rG_'; // 適当な文字列
}
prefixが取得できる関数をつくります。
実際は適当な文字列を返すだけのシンプルなものです。
UIコンポーネントに対して文字列付きclassを指定する
作成した関数を使って、UIにclass名を付与します。
今回はReactで作成していたので、サンプルコードはReactですがシンプルな関数なので汎用的に使えるはずです。
import getPrefix from 'getPrefix';
const prefix = getPrefix();
...
render() {
return (
<div className={`${prefix}button_flat-button`}>
...
</div>
);
}
文字列が付与された状態でCSSを作成
CSSは個別にJSの関数をimportすることができないので、PostCSSを利用します。
PostCSSのビルド実行時にすべてのプロパティに一括してprefixを仕込んでくれるプラグインを使います。
https://github.com/thompsongl/postcss-class-prefix
今回webpackを使ってCSSをビルドしていたので、サンプルはwebpack.config.js
です。
これはJSファイルのビルドとは別のプロセスとして、独立して実行されるようになっています。
const getPrefix = require('./getPrefix');
const prefix = getPrefix();
...
entry: [
'./src/index.css' // CSS用のエントリーポイント
],
plugins: [
new ExtractTextPlugin('app.css'),
new webpack.LoaderOptionsPlugin({
options: {
postcss: [
...
require('postcss-class-prefix')(prefix),
...
],
},
}),
],
あとは普通にコンポーネント用のCSSを書いて、PostCSSのビルド対象に含めておきます。
.button_flat-button {
...
}
結果
<div class="0t3naiN1rG_button_flat-button">...</div>
.0t3naiN1rG_button_flat-button {
...
}
これで出来た一意なclass名をフラグメントにすれば、その下にぶらさげるものは結構適当に作ってしまっても、あとでリファクタは容易です。
今回採用していた技術はReact
+Webpack
+PostCSS
でしたが、やっていることは結構原始的なのでまったく異なる環境でも同様の手法はできるんじゃないかなと思っています。
ただし、あくまで擬似的なスコープ化なのでwebcomponentsでShadowDOMを利用するのが長い目で見ると筋が良いんだろうなぁと思いました。おわり。