Free-Style のススメ ~ CSS Modules は解決策ではない

  • 16
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

最初にまとめ: Free-Style とは

CSS in JS の一種
https://github.com/blakeembrey/free-style

CSS in JS の長所はそのままに

  • スタイルをローカルスコープで定義可能
  • JS の仕組みによる依存性解決 (CommonJS, Require.js, ES modules)
  • 使われていないスタイルの自動削除 (UglifyJS 等)
  • スタイル-スタイル間、および JS-スタイル間の定数の共有が容易
  • JS による柔軟なスタイルの拡張・再利用が可能

CSS in JS が苦手としていた点を改善

  • style 属性に指定可能な内容に限らず、 CSS が提供するすべての機能を利用可能
    • 疑似クラス
    • 疑似要素
    • メディアクエリ

前置き: CSS in JS と CSS Modules

前置きの前置き

この記事は、 :rose: の emoji でおなじみ (?) の @basarat さんの以下の記事を読んで「へえ」と思って色々調べた結果をまとめたものです。
煽り気味のタイトルも拝借させていただきました。

https://medium.com/@basarat/css-modules-are-not-the-solution-1235696863d6#.jgvyjjkg0

CSS in JS

使用例

http://qiita.com/koba04/items/0e81a04262e1158dbbe4 より引用

var style = {
  container: {
    backgroundColor: "#ddd",
    width: 900
  }
}

var Container = React.createClass({
  render() {
    return <div style={style.container}>{this.props.children}</div>;
  }
});

CSS in JS の弱点

  • style 属性に指定できないようなスタイルを定義することが不可能、ないし困難 (疑似クラス、疑似要素、メディアクエリ)
  • JS でがんばっている (:hover を mouse event で実現するなど) ライブラリもある
    • もっとも有名なのは Radium
    • JS で CSS のすべての機能を再現できるわけではなく、限界がある

CSS Modules

  • https://github.com/css-modules/css-modules
  • CSS ファイルを JS から import して使う
    • 実際には、 CSS ファイルからビルド時に CSS と JS が出力され、その JS の方が import される(詳細は使用例を参照)
  • 実体は CSS なので、 CSS で実現できることはすべて実現可能
  • ルールセットがローカルスコープだったり、スタイルの再利用ができたり、 CSS in JS で実現していたこともある程度は実現可能

使用例

https://github.com/css-modules/css-modules/blob/master/docs/pseudo-class-selectors.md より引用

/* component/text.css */
.text {
  color: #777;
  font-weight: 24px;
}
.text:hover {
  color: #f60;
}

上記の CSS ファイルを webpack の css-loader でビルドすると、

._23_aKvs-b8bW2Vg3fwHozO { 
  color: #777;
  font-weight: 24px;
}
._23_aKvs-b8bW2Vg3fwHozO:hover {
  color: #f60;
}

みたいな CSS と、

exports.locals = {
  text: "_23_aKvs-b8bW2Vg3fwHozO"
}

みたいな JS が出力されるので、

/* component/text.js */
import styles from './text.css';

import React, { Component } from 'react';

export default class Text extends Component {

  render() {
    return (
      <p className={ styles.text }>Text with hover</p>
    );
  }

};

のように import して使うことができる (class 名の hash 化の仕方は適当です)。

CSS Modules の弱点

  • 定数の共有や、スタイルの拡張などは、 CSS in JS のように JS の世界でできたほうがやりやすそう (慣れの問題かもしれない)
  • CSS と JS の間での定数の共有ができない

そこで、 Free-Style

  • スタイル定義の実体が CSS である点は、 CSS Modules と同じ
    • CSS が提供するすべての機能を利用可能
  • CSS Modules が CSS を入力として CSS と JS を出力するのに対し、 Free-Style は JS を入力として CSS と JS を出力する
    • 開発者が書くのは JS のコードなので、 CSS in JS 的なメリットはすべて享受できる
    • もちろん、 CSS に変換される JS コードと、通常の JS のコード間での定数の共有も可能
  • React 等特定のライブラリに依存しない
    • 例えば Radium は React の使用が前提になっている
    • React Free Style という React 向け拡張も用意されていたりはする

使用例

https://github.com/blakeembrey/free-style より引用、コメントを一部改変

var FreeStyle = require('free-style')

// Create a container instance.
var Style = FreeStyle.create()

// Register a new, uniquely hashed style.
var STYLE = Style.registerStyle({
  backgroundColor: 'red'
}) //=> STYLE には "f14svl5e" が割り当てられ、 Style オブジェクトには ".f14svl5e { background-color: 'red' }" という CSS が登録される

// Inject a `<style />` element into the `<head>`.
Style.inject() // <style>.f14svl5e { background-color: 'red' }</style> が <head> に出力される

// Figure out how to render the class name after registering.
React.render(
  <div className={STYLE}>Hello world!</div>,
  document.body
)

書き方色々

疑似クラス、疑似要素、メディアクエリ等

var
    style1 = style.registerStyle({
        position: 'absolute',
        top: -42,
        '&:before': { // 疑似要素
            content: `'»'`,
            fontSize: 28,
            color: '#d9d9d9',
            padding: '0 25px 7px'
        },
        '&:checked:before': { // 疑似クラス + 疑似要素
            color: '#737373'
        },
        '@media screen and (-webkit-min-device-pixel-ratio:0)': { // メディアクエリ
            background: 'none',
            top: -56
        }
    }),
    style2 = style.registerStyle({
        position: 'relative',
        fontSize: 24,
        borderBottom: '1px dotted #ccc',
        '&.editing': { // 他のクラスとの組み合わせ
            borderBottom: 'none',
            padding: 0
        },
        '&:last-child': { // 疑似クラス
            borderBottom: 'none'
        },
        '&.editing .edit': { // 子孫セレクタの指定
            display: 'block'
        }
    });

上記は以下のような CSS に変換されます (class 名の hash 化ロジックは適当)

/* style1 */
.f1n85iiq {
  position: absolute;
  top: -42px;
}
.f1n85iiq:before {
  content: '»';
  font-size: 28px;
  color: #d9d9d9;
  padding: 0 25px 7px;
}
.f1n85iiq:checked:before {
  color: #737373;
}
@media screen and (-webkit-min-device-pixel-ratio:0) {
  .f1n85iiq {
    background: none;
    top: -56px;
  }
}

/* style2 */
.fk9tfor {
  position: relative;
  font-size: 24px;
  border-bottom: 1px dotted #ccc;
}
.fk9tfor.editing {
  border-bottom: none;
  padding: 0;
}
.fk9tfor:last-child {
  border-bottom: none;
}
.fk9tfor.editing .edit {
  display: block;
}

ポイントは、

  • 変換後の class 名は & で表現する
  • 生で書いた class 名 (上記の例では .editing, .edit) は hash 化されずにそのまま使われるため、グローバルな class となってしまう
    • ただし、 style.registerStyle() の中でしか使わない (素の CSS には書かない) ことを徹底すれば、 hash 化された (fk9tfor のような) class 名との組み合わせにより、実質的にローカルスコープにしか影響を及ぼさないと考えることもできる

TodoMVC の実装例

https://github.com/kimamula/ts-react/tree/feature/free-style

  • CSS 部分をすべて Free-Style (React Free Style) で実装した、 TodoMVC アプリケーション
  • git clone してサクッと動かせるはずなので、よかったら試してみてください

Free-Style の弱点

  • 今のところそんなに流行っていないので、乗っかってしまって大丈夫か不安
  • CSS Modules との比較でいうと、 CSS Modules は CSS で書くので、「いつでも引き返せる」みたいな安心感は CSS Modules の方が強いと思う

類似ライブラリ

JSS: https://github.com/jsstyles/jss

  • Free-Style と同様、 JS を入力として CSS と JS を出力する
  • GitHub の star 数的には、 JSS に軍配
  • 両者の違いについては Free-Style のこの issue での議論が参考になりそう
    • 簡単に言うと、 Free-Style はシンプル、 JSS は多機能
    • 自分は上記 issue での Free-Style の @blakeembrey さんの指摘通り、 JSS の機能は過剰に感じたため、 Free-Style の方が好き

感想

実際にコードを書いてみて、 Free-Style なら CSS とうまくやっていけそう、という実感があったので、是非実戦で使ってみたいです。
あとはチームの他のメンバーが気に入ってくれるか次第。