When
2017/02/17
At
自己紹介
ちきさん
(Tomohiro Noguchi)
Twitter/GitHub/Qiita: @ovrmrw
ただのSIer。
Angular Japan User Group (ng-japan)スタッフ。
(ここから CSS Modules と React CSS Modules の話)
===
GitHubリポジトリ ovrmrw/react-webpack-css-modules-typescript-example
CSS Modules
よくあるCSS ModulesのJSコード(React)
import React from 'react'
import styles from './table.css'
export default class Table extends React.Component {
render () {
return <div className={styles.table}>
<div className={styles.row}>
<div className={styles.cell}>A0</div>
<div className={styles.cell}>B0</div>
</div>
</div>
}
}
CSSを styles
オブジェクトに格納して {styles.foo}
を className
に代入する。
import styles from './table.css'
↓
<div className={styles.table}>
これでコンポーネントに閉じたCSSを実現できる。
注意点: CSSのクラス名はキャメルケース。
<div className={styles.myStyleName}>
上記のようにstyleをJSオブジェクトとして扱うため、CSSの原則に反してキャメルケースで書かなければならない。
.myStyleName {
color: green;
background: red;
}
.otherStyleName {
composes: className;
color: yellow;
}
キモい。
React CSS Modules
キャメルケース縛りがなくなる。
以下はDecoratorを使ったReact CSS ModulesのJSコード。
import React from 'react'
import CSSModules from 'react-css-modules'
import styles from './table.css'
@CSSModules(styles)
export default class extends React.Component {
render () {
return <div styleName='table'>
<div styleName='row'>
<div styleName='cell'>A0</div>
<div styleName='cell'>B0</div>
</div>
</div>
}
}
CSSを styles
オブジェクトに格納して styleName
プロパティで参照する。CSS Modulesより若干すっきりする。
import styles from './table.css'
↓
@CSSModules(styles)
↓
<div styleName='table'>
CSSのクラス名をキャメルケースで書かなくて良い。
React CSS Modules component automates loading of CSS Modules using
styleName
property.
GitHub: gajus/react-css-modules
Decorator: @CSSModules(styles)
Angularみたい...!!
しかしReact CSS Modulesの物語はここで終わらない
babel-plugin-react-css-modules
babel-plugin-react-css-modulesを使ったJSコード
import React from 'react'
import './table.css'
export default class Table extends React.Component {
render () {
return <div styleName='table'>
<div styleName='row'>
<div styleName='cell'>A0</div>
<div styleName='cell'>B0</div>
</div>
</div>
}
}
Decoratorが消えて、もはや何が起きているのかわからない。しかしこれでCSSは当たっている。
import './table.css'
↓
<div styleName='table'>
CSSのクラス名をキャメルケースで書かなくて良いというメリットは同じ。
GitHub: gajus/babel-plugin-react-css-modules
VERY COOL
でもBabel Pluginなのか...
TypeScriptでも使いたいとかそんなこと...
できるんです!!
ちなみにTSで書くとこんな感じ。
import * as React from 'react'
import './table.css'
export class Table extends React.Component<{}, {}> {
render () {
return <div styleName='table'>
<div styleName='row'>
<div styleName='cell'>A0</div>
<div styleName='cell'>B0</div>
</div>
</div>
}
}
JSで書いたときとほぼ変わらない。
CSS Modulesによりこれらの機能を使ってCSSを書ける。
- Import (@value)
- Custom Properties
- Inheritance (composes)
:root {
--custom-bg-color: yellow;
}
.my-style {
background-color: var(--custom-bg-color); /* using Custom Property */
color: blue;
font-weight: bold;
}
@value appModule: "./app.css"; /* importing .app/css */
:root {
--custom-bg-color: orange;
}
.my-style {
composes: my-style from appModule; /* extending .my-style from app.css */
background-color: var(--custom-bg-color); /* using Custom Property */
}
(DEMO)
(ここからDEMO用に書いたWebpack設定の話。つまり本題)
Webpack構成(module.rules)
-
.ts, .tsx
- awesome-typescirpt-loader
- babel-loader
- plugins:
transform-react-jsx
,react-css-modules
- plugins:
-
.css
- postcss-loader
- plugins:
postcss-cssnext
,postcss-modules-values
- plugins:
- css-loader
- modules:
true
- modules:
- style-loader
- postcss-loader
tsconfig.jsonのコツ
{
"compilerOptions": {
"module": "es2015",
"moduleResolution": "node",
"jsx": "preserve",
"sourceMap": true
}
}
- WebpackかBabelで再コンパイルされるのでtscでは
"module": "es2015"
で出力する。 - JSXの変換はBabelの段階でやる必要があるのでtscでは
"jsx": "preserve"
で出力する。 - ここで
"sourceMap": true
を指定しておくと最終的にTSコードにbreakpointを置けるようになる。
webpack.config.js の全コード。
これぐらいは諳(そら)で書けるようになりたい。
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CompressionWebpackPlugin = require('compression-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = function (env = {}) {
console.log('env:', env)
const isProduction = !!env.production
const context = process.cwd()
const cssModulesScopedName = '[path]___[name]__[local]___[hash:base64:5]'
let entry
let outputFilename
let plugins = []
let moduleRules = []
////////////////////////////////////// entry
entry = isProduction ?
// production
['./config/polyfills.ts', './src/index.ts'] :
// development
{
main: './src/index.ts',
vendor: './config/vendor.ts',
polyfills: './config/polyfills.ts',
}
////////////////////////////////////// output.filename
outputFilename = isProduction ?
// production
'static/js/bundle.[chunkhash].js' :
// development
'static/js/[name].js'
////////////////////////////////////// plugins
plugins.push(
new HtmlWebpackPlugin({
template: 'public/index.html',
filename: 'index.html'
})
)
if (isProduction) {
// production
plugins.push(
new CompressionWebpackPlugin({
test: /\.js$/
}),
new ExtractTextPlugin('static/css/bundle.[chunkhash].css')
)
} else {
// development
plugins.push(
new webpack.optimize.CommonsChunkPlugin({
name: ['vendor', 'polyfills'],
})
)
}
////////////////////////////////////// module.rules
moduleRules.push(
{
test: /\.tsx?$/,
exclude: [/node_modules/],
use: [
{
loader: 'babel-loader',
options: {
presets: isProduction ?
// production
['latest'] :
// development
[],
plugins: [
'transform-react-jsx',
['react-css-modules', { generateScopedName: cssModulesScopedName }],
]
}
},
'awesome-typescript-loader'
]
},
{
test: /\.css$/,
use: isProduction ?
// production
ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 1,
sourceMap: true,
localIdentName: cssModulesScopedName,
}
},
'postcss-loader',
]
}) :
// development
['style-loader',
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 1,
sourceMap: true,
localIdentName: cssModulesScopedName,
}
},
'postcss-loader',
]
}
)
return {
context,
entry,
output: {
filename: outputFilename,
path: 'dist'
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json']
},
module: {
rules: moduleRules,
},
plugins,
devtool: 'source-map',
performance: false,
}
}
Webpackとりあえずこれだけ! "entry"
entry = isProduction ?
// production
['./config/polyfills.ts', './src/index.ts'] :
// development
{
main: './src/index.ts',
vendor: './config/vendor.ts',
polyfills: './config/polyfills.ts',
}
- production
- JSファイルは1つになれば良いのでエントリーポイントを分けない。ただしpolyfillは読み込む。
- development
- バンドルの時間を最小限にするため可能な限りエントリーポイントを分ける。mainエントリーポイントは自分が書いたコードのみをバンドルするようにする。
Webpackとりあえずこれだけ! "output"
outputFilename = isProduction ?
// production
'static/js/bundle.[chunkhash].js' :
// development
'static/js/[name].js'
return {
output: {
filename: outputFilename,
path: 'dist'
}
}
- production
- 生成する度にhashを付与する。
[chunkhash]
の使用が推奨される。
- 生成する度にhashを付与する。
- development
- エントリーポイント毎にファイルが生成されるよう
[name]
を使う。
- エントリーポイント毎にファイルが生成されるよう
Webpackとりあえずこれだけ! "plugins" (1/2)
plugins.push(
new HtmlWebpackPlugin({
template: 'public/index.html',
filename: 'index.html'
})
)
public/index.html
をテンプレートとして、生成されたCSSとJSのリンクを挿入して dist/index.html
に書き出す。
Webpackとりあえずこれだけ! "plugins" (2/2)
if (isProduction) {
// production
plugins.push(
new CompressionWebpackPlugin({
test: /\.js$/
}),
new ExtractTextPlugin('static/css/bundle.[chunkhash].css')
)
} else {
// development
plugins.push(
new webpack.optimize.CommonsChunkPlugin({
name: ['vendor', 'polyfills'],
})
)
}
- production
-
compression-webpack-plugin
を使ってバンドルされたJSファイルの*.js.gz
ファイルを生成する。 -
extract-text-webpack-plugin
を使ってバンドルからCSSを抽出して別ファイルとして書き出す。
-
- development
-
CommonsChunkPlugin
を使って各エントリーポイントをHTMLファイルに挿入するときの順番を指定する。
-
Webpackとりあえずこれだけ! "module.rules" (1/2)
/.tsx?$/ の場合
{
test: /\.tsx?$/,
exclude: [/node_modules/],
use: [
{
loader: 'babel-loader',
options: {
presets: isProduction ?
// production
['latest'] :
// development
[],
plugins: [
'transform-react-jsx',
['react-css-modules', { generateScopedName: cssModulesScopedName }],
]
}
},
'awesome-typescript-loader'
]
}
{
"compilerOptions": {
"module": "es2015",
"moduleResolution": "node",
"jsx": "preserve",
"sourceMap": true
}
}
-
awesome-typescript-loader
の段階ではes2015
でコンパイルJSXはそのままにする。 -
babel-loader
の段階でJSXをコンパイルしてReact CSS Modulesのデコレーション処理を適用する。
Webpackとりあえずこれだけ! "module.rules" (2/2)
/.css$/ の場合
{
test: /\.css$/,
use: isProduction ?
// production
ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 1,
sourceMap: true,
localIdentName: cssModulesScopedName,
}
},
'postcss-loader',
]
}) :
// development
['style-loader',
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 1,
sourceMap: true,
localIdentName: cssModulesScopedName,
}
},
'postcss-loader',
]
}
module.exports = {
plugins: [
require('postcss-cssnext')({ /* ...options */ }),
require('postcss-modules-values'),
]
}
-
postcss-loader
により未来的CSSが標準CSSにコンパイルされる。 -
css-loader
によりCSSファイルをCSS Modulesで扱えるように読み込む。 -
style-loader
によりCSSをDOMに適用する。
あとはこれ見て
https://github.com/ovrmrw/react-webpack-css-modules-typescript-example/blob/master/webpack.config.js
(まとめ)