CSS Modules とゎ?
ざっくりいうと CSS は全部が global 定義なので、コンポーネントごとにクラス名をハッシュ化したモジュールにしてしまってスタイルの影響範囲をコンポーネントの中に閉じ込めようというもの
e.g.
.logo { color: red; }
import stytles from './style.css'
'<div class="' + styles.logo + '">'
👇コンパイル
._23_aKvs-b8bW2Vg3fwHozO { color: red; }
exports.locals = {
logo: "_23_aKvs-b8bW2Vg3fwHozO",
};
'<div class="' + styles.logo + '">';
SCSS を CSS Modules にする最低限の webpack の設定
css-loader
の module オプションを true
にすれば OK
webpack.config.js
module.exports = {
rules: [{
test: /\.scss$/,
use: [
'style-loader',
'css-loader?modules',
'sass-loader',
]
}],
}
CSS を .scss
ファイルとして出力する場合は mini-css-extract-plugin を使う
cf. webpack css-loader と style-loader の違いについて学んだ - かもメモ
webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
rules: [{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader?modules',
'sass-loader',
]
}],
}
CSS Modules の Mode
CSS Modules 化で全ての CSS の指定が hash 化されてしまうと、normalize.css とか共通の utility クラスが作れなくなってしまうので、CSS Modules には hash 化される local なスタイルと共通で使える global なスタイルという概念があります。
css-loader は内部的に local なスタイルを :local(.className) {...}
, global なスタイルを :global(.className) {...}
という形に変換した上で local なスタイルを hash 化して最終的な CSS に変換しているようです。
SCSS では明示的に :local
, :global
キーワードで囲って定義してあるスタイルが local なスタイルなのか、global なスタイルなのかを表すことができます。
CSS Modules への変換方法には local
, global
という2つの mode があり、local
スタイルと global
スタイルへの変換方法が異なります。
local
モード (default) では CSS ファイルにベタ書きしたスタイルを local なスタイルに変換する
global
モードでは CSS ファイルにベタ書きされたスタイルを global なスタイルとして扱う
local モード
css-loader の modules オプションの mode を 'local'
又は true
又は css-loader?modules
と指定する
webpack.config.js
module.exports = {
rules: [{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
mode: 'local',
}
}
},
'sass-loader',
style.scss
.ichigo { color: red; }
:local {
.aoi { color: blue; }
}
:global {
.ran { color: purple; }
}
👇 コンパイル
._2EvoXuCC19CRPsPIfLzc34 { color: red; }
.yqLGVvuQreTgr2chBe--O { color: blue; }
.ran { color: purple; }
global モード
css-loader の modules オプションの mode を 'global'
に指定
webpack.config.js
module.exports = {
rules: [{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
mode: 'global',
}
}
},
'sass-loader',
style.scss
.ichigo { color: red; }
:local {
.aoi { color: blue; }
}
:global {
.ran { color: purple; }
}
👇 コンパイル
.ichigo { color: red; }
.yqLGVvuQreTgr2chBe--O { color: blue; }
.ran { color: purple; }
default import したオブジェクトの中に local なスタイル指定が 元のクラス名: hash化されたクラス名
という形で渡される
.ichigo { color: red; }
:local {
.aoi { color: blue; }
}
:global {
.ran { color: purple; }
}
👇 local モードで JS で読み込んだ場合
import styles from './style.scss';
console.log(styles);
console.log(styles.ran);
const Soleil: FC = () => (
<>
<p className={styles.ichogo}>星宮いちご</p>
<p className={styles.aoi}>霧矢あおい</p>
<p className="ran">text 紫吹蘭</p>
</>
);
css-loader は CSS ファイル内の local なスタイルだけを exports.local = { className: "hashed_className" }
という形に変換して JavaScript に渡す。
local なスタイルと global なスタイルの扱いの違い
import されるオブジェクト内に global なスタイルの指定は含まれない。
つまり、global なスタイルは CSS Modules にしても JS ファイルのサイズには影響を与えない。
逆に言えば local なスタイルの指定は JS ファイルとして読み込まれるので、使用していないスタイルを含む CSS ファイルを import した場合 不要なJSなコードが含まれてしまうので、モジュール単位で分解されていないようなCSS を読み込むと JS のサイズが肥大してしまうので CSS ファイルの分割に注意が必要になる
SCSS から JS に変数を渡したい
SCSS / Sass で作れる変数を JS に渡して使いたいようなケース
$
で始まる変数はそのままでは JS ファイルに渡すことは出来ない。
:export
キーワード内に $
無しで定義すると JS 側では変数として扱うことができる
$widthTablet: 720px;
:export {
widthTablet: $widthTablet
$widthMobile: 320px;
}
import styles from './styles.scss';
console.log(styles);
$
で始まる SCSS の変数は :export
キーワード内に定義しても JS には渡されません。
また、 :export
キーワードブロック内にスタイルを定義しても JS では扱えず、最終的にコンパイルされた CSS に :export .className {...}
の様な使えないスタイルが出力されてしまうので、:export
キーワード内には JS に渡したい変数だけを定義するのが良さそうです。
map な変数はそのままでは渡せない
SCSS の map な変数は、そのままおオブジェクトとして JS に渡せるのかと思ったのですがエラーになるようで、 map を展開した変数として渡してあげる必要があるようです。
$soleil: (
ichogo: red,
aoi: blue,
ran: purple,
);
:export {
seleil: $soleil;
}
@each $name, $color in $soleil {
:export {
#{$name}: $color;
}
}
👇
import styles from './styles.scss';
console.log(styles);
:local
, :global
内に変数を定義した場合
:local { maxWidth: 720px }
の様に :export
でないキーワードのブロック内に JS に渡したい変数を定義しても、JS では扱うことが出来ず 最終的に出力される CSS にもこの変数定義が出力されてしまいました。
SCSS と JS 共通して扱いたい変数は必ず :export
キーワード内に記述する必要がありそうです。
JS 内で使用するだけ、CSS としては不要な場合は style-loader, MiniCssExtractPlugin は使わなくてもOK
レアケースだけど、CSS と JS で変数だけを共有して使いたいような場合。CSS を出力する必要がなければ CSS として書き出すための style-loader, MiniCssExtractPlugin は使わなくても OK
webpack.config.js
module.exports = {
rules: [{
test: /\.scss$/,
use: [
'css-loader?modules',
'sass-loader',
]
}],
}
CSS Modules を named import したい
.ichigo { color: red; }
:export {
widthTablet: 720px;
}
import { ichigo, widthTablet } from './style.scss';
console.log(ichigo);
console.log(widthTablet);
css-loader のオプションには namedExport
オプションがありこれを使うと named import ができるようになる。
css-loader の後に実行される style-loader や MiniCssExtractPlugin にもこのオプションを指定してあげる必要がある。
この設定を忘れると default import は空オブジェクト {}
になり named import は undefined
になってしまう。
cf. namedExport css-loader
style-loader の場合
- style-loader v2.0.0
- css-loader v5.0.0
cf. namedExport style-loader
webpack.config.js
module.exports = {
rules: [{
test: /\.scss$/,
use: [
{
loader: 'style-loader',
options: {
esModule: true,
modules: {
namedExport: true,
},
}
},
{
loader: 'css-loader',
options: {
esModule: true,
modules: {
namedExport: true,
},
}
},
'sass-loader',
]
}],
}
MiniCssExtractPlugin プラグインを使うパターン
- mini-css-extract-plugin v1.1.0
- css-loader v5.0.0
CSS ファイルを作成する mini-css-extract-plugin
プラグインにも namedExport
のオプションがありこれを true
にしてあげれば OK
cf. namedExport MiniCssExtractPlugin
webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
rules: [{
test: /\.scss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
esModule: true,
modules: {
namedExport: true,
},
}
},
{
loader: 'css-loader',
options: {
esModule: true,
modules: {
namedExport: true,
},
}
},
'sass-loader',
]
}],
}
これで CSS Modules で named import ができるようになりました!
namedExport
が設定されていると default import は undefined
になる
namedExport を設定した場合内部的には JS に渡される CSS のクラス名・変数が下記のような形でになっているので default import はできなくなる
.ichigo { color: red; }
:export {
widthTablet: 720px;
}
JS には下記の形に変換されたものが渡される
const widthTablet = "720px";
const ichigo = "_2EvoXuCC19CRPsPIfLzc34";
👇 なので default import は undefined
になる
import styles from './style.scss';
console.log(styles);
CSS Modules は named import でも tree shaking はされないっぽい
mode = production
でビルドされたJSファイルの中を見てみた感じ、JS に import していないクラス名も全て変数として定義されていたので CSS Modules は named import にしても tree shaking は効かないみたいです。
.ichigo { color: red; }
:local {
.aoi { color: blue; }
}
:global {
.ran { color: purple; }
}
:export {
widthTablet: 720px;
}
.app { font-size: 1rem; }
import './base.scss';
import { ichigo, widthTablet } from './style.scss';
👇 ビルドされた JS ファイルの中
const app = "w54bpbuIyvtuFEpomSGPt";
const widthTablet = "720px";
const ichigo = "_2b0mtVMhYRrlk4p9CgzKCc";
const aoi = "_3fxOvTsExbr0n7UyKw53cD";
js では import していない api
も変数として定義されているので、 named import する場合も CSS ファイル内にある local 変数は全て JS に読み込まれるみたいなので named import しない場合もコンポーネントで使うスタイルだけ書かれた小さな CSS ファイルを import するように心がける必要は変わらずありそうです。
namedExport
した際の ES5 対応について
namedExport
された CSS Modules は const className = "hashed_class_name"
の形で JS ファイルに書き出されます。
css-loader から渡されたコードは JavaScript / Typescript の loader を通らないようなので、これらで ES5 に変換する設定をしていても最終的に出力されるコードに const
が残ってしまいます。
webpack.config.js
module.exports = {
rules: [
{
test: /\.js$/,
use: [
exclude: /node_modules/,
loader: 'babel-loader',
options: {
"presets": [
"@babel/preset-env"
],
},
],
},
{
test: /\.scss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
esModule: true,
modules: {
namedExport: true,
},
}
},
{
loader: 'css-loader',
options: {
esModule: true,
modules: {
namedExport: true,
},
}
},
'sass-loader',
},
]
],
}
流石にもう const
は大丈夫だと思いますが最終的に出力されるスクリプトを Linter などでチェックしている場合は css-loader の後に babel などで ES5 の形に変換してあげる必要があります。
webpack.config.js
module.exports = {
rules: [
{
test: /\.scss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
esModule: true,
modules: {
namedExport: true,
},
}
},
{
loader: 'babel-loader',
options: {
"presets": [
"@babel/preset-env"
],
},
},
{
loader: 'css-loader',
options: {
esModule: true,
modules: {
namedExport: true,
},
}
},
'sass-loader',
},
]
],
}
css-loader で JS 形式に変換されたコードを babel で ES5 にトランスコンパイルをすると named import したスタイルは var className = "hashed_class_name"
の形式に変換されるので、最終的に出力されるコード
に const
が残ることはなくなりました。
まとめ
これで CSS Modules が使える設定を作ることができました。
CSS の設計は全部がグローバルなので難しい。 CSS Modules ならモジュールごとにスタイルを閉じ込めることが出きて便利!
ただし、閉じ込めるスタイルの分だけ JS ファイルが肥大化するので、本当に必要なスタイルだけを読み込む様に気をつける。
という印象でした。
つまり、CSS Modules にしても CSS はちゃんと設計する必要がある。ということだと思います。
人類は CSS 設計からは逃れられない…
[参考]