Global CSSをどう管理するか
まだ@kouさんのAngularアプリケーションにおけるCSS設計手法を読んだことがない人はぜひ読んてみて。要諦はこのスライドに集約されているように思う。
Global CSSを一切使わずに済むのが理想的、だけど、なかなかそう綺麗にはいかないもの。また、CSSライブラリなんかはそもそもGlobal CSSとして読み込む必要があったりして、読み込み順序に気を付ける必要があったりする。これら諸々を適切に管理するために、ITCSSを導入してみた。
管理対象のGlobal CSSの種類
- CSS Variables、Sass variables、mixin、function
- サードパーティのCSSライブラリやフレームワーク(normalize.css, Bootstrap, etc...)
- Componentをまたいで使用したいユーティリティクラス
などなど。
ITCSSを用いたCSS設計
ITCSSとはCSS設計における方法論の一つ。詳しくは以下を参照。非常によくまとまっていて参考になる。
ITCSSレイヤー名のアレンジ
一応、レイヤー名は決まったものがあるものの、馴染みがなかったりしっくりこないものがあるので、アレンジしてみた。
オリジナル | > | アレンジ後 | 変更理由 |
---|---|---|---|
Settings | > | Variables | 『変数』以外の設定はないのでこちらの方が直感的。 |
Tools | > | Tools | - |
Generic | > | Base | アプリケーションのスタイリングの土台という意味を込めて。normalize.cssやminireset.cssなどがそれに当たる。他にも、BootstrapやBulmaなどの土台となるCSSフレームワークもここに入れることを意図している。 |
Base | > | Elements | 要素セレクタに関するレイヤーであることが自明なので。この変更は事例が多い。 |
Objects | > | Objects | - |
Components | > | Components | - |
Trumps | > | Utilities | 実は今はUtilitiesが正式名称。 Harry Roberts氏が自身のTwitterで述べている。 |
Angularプロジェクト上のフォルダ構成例
<project-name>
└── src
└── styles
├── variables
│ ├── _colors.scss
│ ├── _fonts.scss
│ └── _index.scss
├── tools
│ ├── _functions.scss
│ ├── _mixins.scss
│ ├── _xxx.scss
│ └── _index.scss
├── base
│ └── _index.scss
├── elements
│ ├── _xxx.scss
│ └── _index.scss
├── objects
│ ├── _xxx.scss
│ └── _index.scss
├── components
│ ├── _angular-material.scss
│ └── _index.scss
├── utilities
│ ├── _xxx.scss
│ └── _index.scss
└── main.scss
- まず
styles
フォルダを作り、その下にITCSSのレイヤーごとのフォルダを作成。ITCSS的にはフラットな構成が推奨だけど、ファイルが多くなると煩雑になるので、あらかじめレイヤーごとにフォルダを用意しておく。 -
src/styles.scss
->src/styles/main.scss
に変更し、これを単一エントリポイントとした(main.ts
のような使い方)。 -
main.scss
以外は全てファイル名にアンダースコアをプレフィックスし**パーシャルファイル(_xxx.scss
)**として作成。 - 各レイヤーフォルダ直下には
<layer>/_index.scss
ファイルを置いて、他のパーシャルファイルをまとめる(index.ts
のような使い方)。
angular.jsonの変更
...
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
...
"stylePreprocessorOptions": {
"includePaths": ["src/styles"]
},
"styles": [
"src/styles/main.scss"
],
...
},
...
-
src/styles/main.scss
をビルドできるようにsrc/styles.scss
->"src/styles/main.scss"
に変更。 -
"stylePreprocessorOptions.includePaths"
オプションで"src/styles"
を指定しimportパスを簡略化しておく。
実装例
Variables
定義する変数そのものが少ないなら_index.scssに直接書き込んでしまってもいいし、多くなるなら用途ごとにパーシャルファイルに切り出してimportしてもいい。
// colors
$primary-color: red;
$secondary-color: blue;
// fonts
$base-font-size: 16px;
...
// パーシャルファイルに切り出した場合
// @import 'colors';
// @import 'fonts';
...
Tools
_index.scssに直接書き込んでしまってもいいし、膨れ上がってきたら用途ごとにパーシャルファイルに切り出してimportしてもいい。
// functions
@function xxx
// mixins
@mixin yyy
// パーシャルファイルに切り出した場合
// @import 'functions';
// @import 'mixins';
...
ちなみに、VariablesやToolsをComponent CSSなどで使用したい場合は以下のようにimportできる。
@import 'variables/index';
@import 'tools/index';
...
Base
土台となるサードパーティーのCSSをimportする。
@import '~minireset.css/minireset';
Elements
_index.scssに直接書き込んでしまってもいいし、多くなるようならセレクタごとにパーシャルファイル作ってimportしてもいい。ただ、リセット系のCSSを入れているなら、このレイヤーで独自にCSSを書くことはほとんどなさそう。
h1 {
...
}
// @import 'headings';
...
Objects
わざわざGlobal CSSとしてこのレイヤーでスタイリングしなければならないものがあるのか微妙。このレイヤーは必要性が薄いような気がする。
@import 'xxx';
...
Components
Angularアプリケーションの場合、基本的に自作ComponentのスタイルはComponent CSSに書くべきなので、ここに自作Componentのスタイルを書いてはダメ。もしここが膨れるようなら、そもそもComponentとして切り出せないかを検討すべき。
ではこのレイヤーは何のためにあるかというと、Angular Materialなどのサードパーティー製のCSSをimportするために使う。
@import 'angular-material';
@import 'another-third-party-components';
...
Angular Materialの場合、Sassだと記述が多くなるので、こんな感じでパーシャルファイルに分けてimportしたほうがいいと思う。
@import '~@angular/material/theming';
// Plus imports for other components in your app.
// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.
// Be sure that you only ever include this mixin once!
@include mat-core();
// Define the palettes for your theme using the Material Design palettes available in palette.scss
// (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://material.io/design/color/
$candy-app-primary: mat-palette($mat-indigo);
$candy-app-accent: mat-palette($mat-pink, A200, A100, A400);
// The warn palette is optional (defaults to red).
$candy-app-warn: mat-palette($mat-red);
// Create the theme object (a Sass map containing all of the palettes).
$candy-app-theme: mat-light-theme($candy-app-primary, $candy-app-accent, $candy-app-warn);
// Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each component
// that you are using.
@include angular-material-theme($candy-app-theme);
Utilities
サードパーティーのユーティリティ系のCSSをimportする、あるいは、独自のユーティリティのスタイルを書く。他のレイヤーと同様、記述が多くなるならパーシャルファイルに切り出してもいい。
// animate.css
@import '~animate.css/animate';
// fontawesome
// パスの解決のために$fa-font-pathの上書きが必要
// https://github.com/vuejs-templates/webpack/issues/1313#issuecomment-460714033
$fa-font-path: '~@fortawesome/fontawesome-free/webfonts';
@import '~@fortawesome/fontawesome-free/scss/solid';
@import '~@fortawesome/fontawesome-free/scss/fontawesome';
// その他独自のユーティリティ
@import 'aligns';
@import 'fonts';
@import 'widths';
...
main.scss
各レイヤーに_index.scss
を置いたおかげでmain.scss
は以下のようにスッキリ書ける。基本ここにはCSSを直接書かずimportのみにする。
@import 'variables/index';
@import 'tools/index';
@import 'base/index';
@import 'elements/index';
@import 'objects/index';
@import 'components/index';
@import 'utilities/index';
QA
Q.サードパーティのCSSフレームワークやライブラリはVendorsレイヤーとか作ったほうがいいんじゃない?
A. そんなこともない
詳細度の観点からみると、一律Vendorsというレイヤーに押し込むのは無理があるかなという気がしてる。
レイヤー | CSS Library | CSS Framework |
---|---|---|
Variables | Bootstrap Bulma |
|
Tools | ||
Base | minireset.css normalize.css |
|
Elements | ||
Objects | ||
Components | Angular Material | |
Utilities | Animate.css Font Awesome |
一口にCSSライブラリ・フレームワークといっても、上記のように、それらがカバーするレイヤーは異なるので、詳細度の異なるライブラリーを「サードパーティ」と一括りにしてVendorsレイヤーに押し込めると、他のレイヤーのスタイルと詳細度の順序で問題になりそう。なので、サードパーティであっても特別扱いせず、自作スタイルと同じように、それぞれの詳細度に応じたレイヤーに配置したほうが良いかなと思っている。
Q. なんでBootstrapとかBulmaはBaseレイヤーに入れてる?
A. 土台となるCSSだから
いろいろと調べてみると、BootstrapはObjectsレイヤーに入れてるよ、といったコメントがStackoverflowなどで見つかったりする。でも、Bootstrapはそれ自身でVariables ~ Utilitiesに相当するレイヤーを持っているので、ITCSSのレイヤー上のどこにも当てはまらない。とはいえ、どこかのレイヤーでimportしなくてはいけないので、じゃあどこにするか?と考えたときに、Baseレイヤーでimportしておくと、下のレイヤーでオーバーライドしやすく、逆に、変数は基本!default指定されているので、上のレイヤーでオーバーライドできるようになる。
なにより、フレームワーク≒土台としてのライブラリなのでBaseレイヤーに入れると概念的にしっくりくる。
Q. Componentsレイヤーは必要なの?
A. サードパーティーのコンポーネントCSSをimportするのに必要
Angularアプリの場合、このレイヤーは一見不要では?と疑問に感じるかもしれない。なぜならComponentのCSSにはComponent CSSに記述すべきで、ここにあってはならないので。もしここにスタイルを書こうと思ったら、その前にComponentとして切り出せないかを検討すべき。
では、このレイヤーの存在意義は何かというと、それはAngular MaterialなどのUI Component用のCSSをimportするため。例えばAngular MaterialのComponentはViewEncapsulation.Noneで実装されていて、CSSはGlobal CSSとして存在しているので、どこかでimportしないといけない。それをimportするのがこのレイヤーの役割となる。その他サードパーティ製UI ComponentもGlobal CSSとしてimportが必要になるものは同様。Components/_index.scssで直接importしてもよいし、記述量が多くなるようなら以下のように別途パーシャルファイルを作ってもよい。
Sassファイル内でCSSファイルインポートすると、読み込み順序変わっちゃうから期待どおりの読み込み順序にならないよ?
Sassでよくあるトラブルとして、CSSファイルをSassファイルと混在してimportをすると、ビルド後のimport順序が変わってしまうというものがある。
この問題は「.css」拡張子なしでインポートすることで解決することができる。詳しくは以下を参照。
https://github.com/sass/node-sass/issues/2362#issuecomment-388634848
やってみた所感
- 初期のタイミングでは若干オーバースペックな感じもするけど、レイヤーごとに管理することで、どこにどんなスタイルをおけば良いか分かりやすくなるので、スタイルが増えてきたときにスケールしやすい感じ。
- CSSライブラリの読み込み順序は意外とおざなりになりがちだけど、ITCSSを使うとどのレイヤーでimportすべきかを考えざるをえなくなるので、詳細度をしっかりとコントロールできる。結果として、意図しないスタイルのオーバーライドを未然に防ぐことにつながる。
- Angularに限らず、React とか Vue でも適用可能だと思う。