Wed, Aug 29, 2018

React + Reduxアプリケーションプロジェクトのテンプレートを作る ― その4: CSS ModulesとPostCSSとstylelintとstyled-components

React + Reduxアプリケーションプロジェクトのテンプレートを作る ― その4: CSS ModulesとPostCSSとstylelintとstyled-components

ReactReduxを学ぶために、開発環境というかプロジェクトテンプレートをスクラッチから作っている。 (最終的な成果はGitHubに置いた。)

前回はPrettierとESLintをセットアップした。

CSS

前回までで作った環境で、Reactを使ってHTMLのDOMツリーを構築することができるようになったが、これは基本的にUIに表示する情報の構造しか定義しない。 UIの見た目(スタイル)を決めるのはCSSなので、それをアプリに組み込むことを考えないといけない。

組み込み方には現時点で大きく3通りある。

CSSを別途設計する

一つ目はCSSを別途設計する方法。

Reactコンポーネントからレンダリングされる要素にclassが付くようにしておいて、設計したCSSをbundle.jsとは別途読み込んでスタイルを適用することにはる。

この場合、CSSのスタイル定義はすべてグローバルなので、設計効率やメンテナンス効率を維持しつつ、各コンポーネントに意図したスタイルが適用されるようにするため、テクニックを凝らしてCSSクラスを設計する必要がある。 例えばBEM (2009年3月誕生)、OOCSS (2009年3月誕生)、SMACSS (2011年9月誕生)、FLOCSS (2014年4月誕生)など。

CSS自体は、素のCSSを書くことはあまりなく、普通はSassなどのAltCSSやPostCSSを使って書く。

さらに、stylelintでリンティングすることで、CSSの品質を上げられる。 リンティングルールは、stylelintプロジェクトから提供されているstylelint-config-recommendedstylelint-config-standardを使えば十分。 後者がGoogleやAirbnbのCSSスタイルガイドを反映していていい感じ。

書いたCSSは、webpackのcss-loaderで読み込める。 webpackはJavaScriptのimport './App.css';みたいなコードを見つけると、css-loaderに処理を渡す。 css-loaderは、import文で指定されたCSSファイルだけでなく、@importurl()で定義される依存関係をたどって関連するCSSを一通り読み込む。

読み込んだCSSは、webpackのstyle-loaderを使ってDOMに適用できる。 style-loaderは、読み込んだCSSを<style>タグで囲ってHTMLのヘッダに挿入してくれる。


CSSの処理にはPostCSSを使うとして、プロジェクトに以下のパッケージを追加する。 (PostCSSについてはQiitaの記事が参考になった。)

  • css-loader: CSSを読み込むためのwebpackのローダ。
  • style-loader: CSSをDOMに追加するためのwebpackのローダ。
  • postcss-loader: PostCSSを実行するためのwebpackのローダ。
  • postcss-preset-env: CSSのエッジな機能を使うためのPostCSSプラグイン。
  • autoprefixer: CSSプロパティにベンダプレフィックスを追加してくれるPostCSSプラグイン。
  • postcss-flexbugs-fixes: Flexboxのバグを修正してくれるPostCSSプラグイン。
  • cssnano: CSSをミニファイしてくれるPostCSSプラグイン。
  • stylelint: CSSのリンタ。
  • stylelint-config-standard: stylelintのルール設定集。
  • stylelint-config-prettier: Prettierが施すコード整形とコンフリクトするルールを無効にするstylelintルール設定集。
yarn add -D css-loader style-loader postcss-loader postcss-preset-env autoprefixer postcss-flexbugs-fixes cssnano stylelint stylelint-config-standard stylelint-config-prettier


PostCSSとstylelintの設定は、それぞれpostcss.config.jsとstylelint.config.jsを書いてプロジェクトルートに置けばいい。

postcss.config.js:

module.exports = {
  plugins: {
    stylelint: {},
    'postcss-preset-env': {},
    autoprefixer: {},
    'postcss-flexbugs-fixes': {},
    cssnano: {},
  },
};

stylelint.config.js:

module.exports = {
  extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
};

stylelintはPostCSSのプラグインとしてPostCSSから実行する構成。

stylelint.config.jsで、stylelint-config-prettierはextendsの最後に書く必要があることに注意。


webpackにもローダの設定を追加する。

webpack.common.js:

 (前略)
       {
         test: /\.(js|jsx)$/,
         include: [path.resolve(__dirname, 'src')],
         loader: 'babel-loader',
       },
+      {
+        test: /\.css$/,
+        include: [path.resolve(__dirname, 'src')],
+        use: [
+          'style-loader',
+          {
+            loader: 'css-loader',
+            options: {
+              importLoaders: 1,
+            },
+          },
+          'postcss-loader',
+        ],
+      },
     ],
   },
 (後略)

ここで追加した設定は、<プロジェクトルート>/srcディレクトリ内の.cssファイルがimportされたら、postcss-loader、css-loader、style-loaderの順にそのファイルを処理する、というもの。


実際のCSSは普通に書いて、JavaScriptからimportしてやればいい。

components/App.css:

.normal {
  font-size: 5rem;
}

components/App.jsx:

 import React from 'react';
+import './App.css'

 const App = () => (
-  <div>
+  <div className="normal">
     HOGE
   </div>
 );

 export default App;

JSXでHTML要素にclass属性を付けるには、classNameプロパティを使うことに注意。

これでHOGEにfont-size: 5remが適用され、文字が大きくなる。


以上でCSSを適用できた。

これはこれで十分で柔軟なやりかただけど、BEMなどでCSSクラスの設計を頑張る手間がある。 UIコンポーネントの構造とスタイルの構造を1対1対応させるなら、もっと楽な方法がある。

CSS Modules

CSS Modulesは2015年9月に発表された技術で、一つのCSSファイルを一つのモジュールと考え、モジュールごとにCSSクラス名の名前空間を自動生成し、スタイルの影響範囲をモジュールに閉じ込めてくれるもの。 (実際には、子要素に継承されるプロパティもあるので完全に閉じ込められるわけではない。)

ReactによるUIコンポーネントごとにCSSモジュールを作り、コンポーネント単位でスタイリングすることを意図した技術であり、コンポーネント内で閉じたCSSクラス設計をすればいいだけになり、BEMとかを考えなくてよくなる。

CSS Modulesを使うには、babel-plugin-react-css-modulesというBabelのプラグインをセットアップすればいい。 まずはそれをプロジェクトにインストールする。

yarn add -D babel-plugin-react-css-modules


Babelの設定を修正してインストールしたbabel-plugin-react-css-modulesを使うようにする。

.babelrc

 {
-  "presets": ["env", "react"]
+  "presets": ["env", "react"],
+  "plugins": ["react-css-modules"]
 }


webpackのcss-loaderのオプションを追加して、CSS Modulesを有効にする。

webpack.common.js:

 (前略)
       {
         test: /\.css$/,
         include: [path.resolve(__dirname, 'src')],
         use: [
           'style-loader',
           {
             loader: 'css-loader',
             options: {
               importLoaders: 1,
+              modules: true,
+              localIdentName: '[path]___[name]__[local]___[hash:base64:5]',
             },
           },
           'postcss-loader',
         ],
       },
 (後略)

modulesがCSS Modulesを有効化するスイッチ。 localIdentNameはモジュール化したCSSクラスの命名規則で、babel-plugin-react-css-modulesの設定と合っている必要がある。


あとは、コンポーネントの方でclassNameプロパティをstyleNameプロパティに変えればいい。

components/App.jsx:

 import React from 'react';
 import './App.css'

 const App = () => (
-  <div className="normal">
+  <div styleName="normal">
     HOGE
   </div>
 );

 export default App;


以上でCSS Modulesの設定は完了。 App.cssに書いたクラス名はcss-loaderによって変換され、App.jsxに書いたstyleNameはbabel-plugin-react-css-modulesによって変換され、どちらもsrc-components-___App__normal___1fxGxになるようになる。

CSS in JS

3つめはCSS in JS。

これは2014年11月に提唱された技術で、UIコンポーネントとそのスタイルを両方一つのJavaScriptファイルに書いて、完全に一体化させるというもの。

CSS in JSはCSS Modulesの陰でしばらく目立たなかったが、2016年にstyled-componentsという実装がリリースされて注目され、その後いくつかの実装が生まれた。 styled-componentsは2017年ころからCSS Modulesに代わって人気になり、CSS Modules陣営からの反撃もあったものの、今日まで支持を増やしている模様。 SassやPostCSSなど既存のCSSエコシステムを切り捨てているのと、React限定なのが気になるところではあるが、時流に乗って使ってみることにする。

なお、CSS in JSはCSS Modulesとセットアップ方法がかなり異なるので、本稿前節までの変更はいったん全部破棄する。

styled-componentsを使う場合、プロジェクトに追加する必要があるのは二つだけ。

  • styled-components: styled-components本体。
  • babel-plugin-styled-components: styled-componentsのサポートを強化するBabelプラグイン。実際には必須ではないけど、バンドルサイズを削減出来たり、SSRしやすくなったりする。ベンダプレフィックスの付与とかミニファイもしてくれる。
yarn add styled-components
yarn add -D babel-plugin-styled-components

styled-componentsはv3.4.4が入った。


Babelの設定は以下のように修正する。

.babelrc

 {
-  "presets": ["env", "react"]
+  "presets": ["env", "react"],
+  "plugins": ["styled-components"]
 }


App.jsxは、styled-componentsのstyledというAPIを使ってWrapperコンポーネント(スタイル付きdiv)を定義し、これをdivと置き換える。

components/App.jsx:

 import React from 'react';
+import styled from 'styled-components';

+const Wrapper = styled.div`
+  font-size: 5rem;
+`;

 const App = () => (
-  <div>
+  <Wrapper>
     HOGE
-  </div>
+  </Wrapper>
 );

 export default App;

これだけ。CSS Modulesに比べて大分シンプル。

styled.divでスタイルを記述している部分は見慣れない構文だけど、ECMAScript 2015で追加されたタグ付きテンプレートリテラルという構文で、テンプレート文字列の一種。 ここに書くスタイルの構文はCSSと全く一緒。 JavaScriptの構文としては単なる文字列なので、変数を使ったり、if文とかで動的に変えたり、数値を計算したり、自由に書ける。


ややややこしいが、stylelintによるリンティングもできる。 以下のパッケージをプロジェクトに追加する。

yarn add -D stylelint stylelint-config-standard stylelint-processor-styled-components stylelint-config-styled-components stylelint-custom-processor-loader


stylelintの設定は以下。

stylelint.config.js:

module.exports = {
  processors: ['stylelint-processor-styled-components'],
  extends: ['stylelint-config-standard', 'stylelint-config-styled-components'],
};


webpackの設定にstylelint-custom-processor-loaderの設定を追加する。

webpack.common.js:

 (前略)
       {
         test: /\.(js|jsx)$/,
         include: [path.resolve(__dirname, 'src')],
         loader: 'babel-loader',
       },
+      {
+        test: /\.(js|jsx)$/,
+        include: [path.resolve(__dirname, 'src')],
+        enforce: 'pre',
+        loader: 'stylelint-custom-processor-loader',
+      },
     ],
   },
 (後略)

これでstyled-componentsにstylelintを適用できた。

次回Material-UIを導入する。