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)について: