by @Quramy 2016.05.31
メインサービスの開発ではAngularを使っていますが、 社内向けのツール等でReactも使っています
UI Component と呼ばれる物の特徴:
多くのJS FrameworkがUI Componentの開発をサポート
Card Componentを作ってみる
import React from "react"; import {render} from "react-dom"; import {Card} from "./card"; const title = "React and CSS"; render(( <div> <Card title={title}>Hello, card component!</Card> <Card title={title} primary={true}>Hello, primary card!</Card> </div> ), document.getElementById("app"));
React.Component を使ってComponentを作る:
import React from "react"; import cx from "classnames"; export class Card extends React.Component { render() { const {title, primary, children} = this.props; return ( <div className={cx('card', {primary: primary})} > <header className="title">{title}</header> <div className="body">{children}</div> </div> ); } }
CSSはこんな感じ?
.card { background-color: #fff; padding: 20px; border: 1px solid #f0f2fb; border-radius: 3px; box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16); margin-bottom: 30px; } .card .title { font-size: 18pt; margin-bottom: 10px; } .card.primary { background-color: #62c4a4; border: none; color: white; } .card.primary .title { font-weight: bold; }
...本当にこれで良いのか?
Style定義はとても壊れやすい
.card { background-color: #fff; padding: 20px; /** 中略 **/ }
/** これを追記すると... **/ div .card { background-color: red; }
/* Card Componentの内部利用のために定義された CSS Class */ .card .title { font-size: 18pt; }
// Card Componentの利用コード import { Card } from "./Card"; function renderCard() { return ( <Card> <sectoin className="sub"> {/* 利用者側はCardがtitleというclass名を使っていることを知らない */} <div className="title">sub title</div> </sectoin> </Card> ); }
<!-- 出力されるDOM --> <div class="card"> <header class="title"></header> <div> <sectoin class="sub"> <!-- Cardが内部的に使っている .card .title のルールが適用されてしまう --> <header class="title">sub title</header> </sectoin> </div> </div>
ここまでの結論:
React.Componentを使っただけで CSSが"適切にカプセル化"される訳ではない
CSSセレクタがGlobalであることが最大の問題
CSSのGlobal性と戦うための武器:
今回はCSS in JSとCSS Modulesを中心に紹介
CSS in JSではStyleとなるオブジェクトを直接JSで記述する.
/* CardStyles.js */ export const root = { backgroundColor: "#fff", padding: "20px", border: "1px solid #f0f2fb", borderRadius: "3px", boxShadow: "0 2px 5px 0 rgba(0,0,0,0.16)", marginBottom: "30px", }; // 以下略
/* Card.jsx */ import React from "react"; import * as st from "./CardStyles"; export class Card extends React.Component { render() { const {title, primary, children} = this.props; return ( { /* inline style に展開される */ } <div style={primary ? st.primaryRoot : st.root}> <header style={primary ? st.primaryTitle : st.title}>{title}</header> <div>{children}</div> </div> ); } }
CSS class名を介さずにinline-styleを利用することで, StyleをComponentに閉じ込めることができる
JavaScriptの関数を使えば、Styleの合成も容易
export const root = { backgroundColor: "#fff", padding: "20px", border: "1px solid #f0f2fb", borderRadius: "3px", boxShadow: "0 2px 5px 0 rgba(0,0,0,0.16)", marginBottom: "30px", }; export const primaryRoot = Object.assign({}, root, { backgroundColor: "#62c4a4", border: "none", color: "white", });
Styleを.jsで管理するメリット:
inline-styleに展開する場合、下記が利用できない
2016年5月現在, 最も人気のあるCSS in JSライブラリ (~3,000 )
:hover等、いくつかの擬似クラスセレクタをサポート
React NativeのようにStyleSheet.create を使ってjsからStyleSheetを生成
styleやComponent名(Flex等)を静的解析して.cssを生成
参考: React + CSS in JS の主要なツールの比較:
https://github.com/FormidableLabs/radium/blob/master/docs/comparison/
Example of keyframes animation with Radium
Chromeの開発者ツールでstyle属性を見てみよう
import React from "react"; import Radium from "radium"; @Radium export class AnimatedCircle extends React.Component { render() { const { delay } = this.props; return ( <div style={[styles.root, animation(delay)]}> <div style={styles.movable}> <div style={styles.circle}></div> </div> </div> ); } }
Class Decorators(ES.next)を ComponentのClassに付与
styleに配列が使えるように拡張される
styleオブジェクトに":hover"が利用可能に
const height = "50px"; // アニメーションの定義 const translationKeyframes = Radium.keyframes({ "0%": {transform: "translateX(0%)"}, "50%": {transform: "translateX(100%)"}, "100%": {transform: "translateX(0%)"}, }, "pulse"); const animation = (delay) =>({ animation: `x 2.5s ease ${delay} infinite`, animationName: translationKeyframes, }); const styles = { root: { position: "relative", width: "100%", height, marginBottom: "15px", ":hover": { /* :hover時のstyle */ opacity: 0.6, }, }, movable: { position: "absolute", width: "100%", top: 0, bottom: 0, }, circle: { backgroundColor: "#62c4a4", width: height, height, borderRadius: "25px", } };
Radium.keyframesでアニメーションを定義
':hover'をkeyとしてstyle定義
import React from "react"; import ReactDom from "react-dom"; import { AnimatedCircle } from "./AnimatedCircle"; import { StyleRoot } from "radium"; ReactDom.render( <StyleRoot> <AnimatedCircle delay="0s"></AnimatedCircle> <AnimatedCircle delay=".3s"></AnimatedCircle> <AnimatedCircle delay=".6s"></AnimatedCircle> </StyleRoot> , document.getElementById("app"));
keyframesを利用するためにはStyleRootでラップするが必要ある
Glen Maddern が2015年に提唱
Just look at the range of approaches to handling :hover states among the projects I referenced earlier, something that has been solved in CSS for a long time. So, while we’re bullish about our approach and firmly defend the virtues of CSS .
CSS in JSが.jsでstyleを生成するのに対して CSS Modulesは.cssを.jsから利用する
CSS Modulesでは定義されたClassがES 2015 Modulesのように振る舞う
/* Card.css */ .title { font-size: 18pt; }
/* Card.css.js */ export const title = "title";
冒頭のCard ComponentをCSS Modulesで書き換えてみる
/* src/Card.css */ .root { background-color: #fff; : } .title { font-size: 18pt; : }
/* src/Card.jsx */ import React from "react"; import * as st from "./Card.css"; // .cssからimport export class Card extends React.Component { render() { const {title, children} = this.props; return ( <div className={st.root} > {/* class名がexportされている */} <header className={st.title}>{title}</header> <div>{children}</div> </div> ); } }
RenderingされたDOMを開発者ツールで見てみると...
CSS Class名が書き換えられているのが確認できる
CSS ModulesではModule bundlerのプラグインを利用する:
/* CSS Modules pluginが生成するmoduleイメージ */ module.exports = { "root": "_root_gub7p_1", : };
css-modules/css-modulesify pluginが利用可能
npm i browserify babelify css-modulesify browserify \ -t [ babelify ] \ -p [ css-modulesify -d dist/bundle.css ] \ -d dist/bundle.js
NODE_ENV の値によって, 生成されるCSS Class名が異なる:
デフォルト: prefixにlocal file pathが付与される
production: suffixにfile hash付与される
webpack公式のcss-loaderから利用可能 modules オプションでCSS Modulesとして解釈されるようになる
var path = require('path'); module.exports = { module: { loaders: [ { test: /\.js$/, exclude: /node_modules/, loaders: ['babel-loader'] }, // postcssと併用する場合, importLoaders=1が必須 { test: /\.css$/, loaders: ['style', 'css?modules&importLoaders=1', 'postcss'] }, ], }, postcss: [ require('autoprefixer'), ], entry: './src/index.js', output: { path: path.resolve('dist'), filename: 'bundle.js' }, resolve: { extensions: ['', '.js'] }, };
composes キーワードでCSS Class同士の合成が可能
(Sassの@extend, CSS @apply rule相当)
/* util.css */ .large { font-size: 18pt; margin-bottom: 10px; }
/* Card.css */ .title { composes: large from './util.css'; font-weight: bold; }
/* generated module of Card.css */ module.exports = { "title": "title_xxx_yyy large_zzz_www" }
@value で値を定義すると.js, .cssからimport出来る
/* shared.css */ @value duration 0.6s;
/* animatedBall.css */ /* aliasを付けてimport可能 */ @value duration as bounceDuration from "./animation-values.css"; @keyframes bounce { 33% { transform: translateY(-20px); } 66% { transform: translateY(0px); } } .bounce { animation: bounce bounceDuration infinite ease-in-out; }
css-modules/postcss-modules を利用することでCSS Modulesの事前変換が可能
/* styles.css */ .myClass { color:red; }
/* converted_styles.css */ ._myClass_xxx_yyy { color:red; }
// converted_styles.json { "myClass": "_myClass_xxx_yyy" }
出力されたjsonから対応関係を参照することで他言語(e.g. Ruby)からもCSS Modulesを利用できる
CSS in JSとCSS Modules, どっちを使えばいいの?
現状、優劣は付け難い
"styleを.jsで書きたいか" or "styleを.cssで書きたいか" 天秤に掛けて決めましょう
TypeScriptでもCSS Modulesが使いたい
import * as React from "react"; import * as st from "./Card.css"; // compile errorに... export interface CardProps { title: string; primary?: boolean} export class Card extends React.Component<CardProps, {}> { render() { const {title, primary, children} = this.props; return ( <div className={primary ? st.primaryRoot : st.root} > <header className={primary? st.primaryTitle : st.title}>{title}</header> <div>{children}</div> </div> ); } }
CSS Modulesから.css.d.tsを生成するツールを作りました
/* Card.css */ .root { ... } .title { ... }
// Card.css.d.ts export const root: string; export const title: string;
BEM(Block Element Modifier)について: