はじめに
以前、React JSX with TypeScript(1.6)にて書いたことがありますが、TypeScript 1.6以降ではReact JSXを直接コンパイルできるオプションが追加されています。
一般的にクライアントサイドWebアプリ開発は以下の3言語が土台ですが、
- JavaScript
- HTML
- CSS
TypeScriptのお陰で、1.と2.についてはタイプセーフな世界を手に入れたことになります(HTMLの要素名や属性名まで含めて、コンパイル時のチェックがされる)
- JavaScript -> TypeScript
- HTML -> React JSX(.tsx)
- CSS -> ???
こうなると、3. におけるCSSについても、何とかできんものかと期待するのが人情です。
今回のエントリは、CSS ModulesをTypeScript JSX環境に統合することで、タイプセーフなWeb開発フローを手に入れよう、というお話です。
なお、話の流れ上React JSX(.tsx)を登場させていますが、TypeScript + CSS Modulesを組み合わせて使う部分はJSXと直接関係しないので、他のフレームワークやテンプレートエンジンでも応用できる内容です。
CSS Modules
CSS Modules の概要については、CSSモジュール - 明るい未来へようこそ 等が分かりやすいです。
ここでは、今回のテーマに関連する部分に絞って紹介します。
CSS Modulesでは以下のように.cssを書いておき、
/* style.css */
.className {
color: green;
}
アプリケーションのコードからimportして、変数をcss class名として使うという開発スタイルが基本となります。
import styles from "./style.css";
// import { className } from "./style.css";
element.innerHTML = '<div class="' + styles.className + '">';
.cssをあたかもES6 modules形式のモジュールのように扱う点が最大の特徴です。
./style.css
モジュールの実体は、css-modulesify(browserifyのplugin), css-loader(webpack のloader. CSS Modules向けオプションがある) 等を使うことで、bundle.jsの作成時に自動で作成されます。
上記の例に則して説明すると、className
という変数に対して一意な実体クラス名を付与し、変数名と実体名のマッピングが生成されるイメージです。
module.exports = {className: 'xxx_yyy_zzz_className'}
.xxx_yyy_zzz_className {
color: green;
}
(xxx_yyy_zzz
部分はCSS Modulesに対応したツール側が適当に一意なprefixを割り当てます。開発者は意識しません。)
TypeScriptとCSS Modules
さて、ここで問題となるのが下記です。
import styles from "./style.css";
// : テンプレートの作成コード
只のJavaScriptコードであれば、import from
や require(...)
に何を書こうが、実行時に対応するモジュール実体があれば動作するので何の問題もありませんが、今回はTypeScriptです。
下記を書いて保存しようものなら、TypeScriptのコンパイラが「そんなモジュールねぇよ!」ってお怒りになられ、1行目からつまずくのは目に見えています。
import * as styles from "./style.css";
// : テンプレートの作成コード
TypeScriptのモジュール解決関連と言えば TypeScriptを魔改造してjspmのモジュールをimport出来るようにするにてModule Resolutionを乗っとる話を書いたことがあります。
この際に、ある程度のカスタマイズ方法は身についたのですが、さすがにtsc
やtsserver
を自分でビルドし直すような方法が万人受けするとは思ってないので、今回は別アプローチを考えました。
TypeScriptには、モジュールの実体とは別にそのモジュールの定義体を用意する、という文化があります。
そう、.d.ts
です。
通常はDefinitly Typedのように、既に存在しているモジュールに対して後付けで定義体を追加できる仕組みとして捉えがちですが、モジュールの実体を作る前に.d.tsを作ったって良い筈です。
webpackやbrowserifyでCSS Modulesの実体を作るよりも先行して、下記のようにstyle.css.d.tsという名前で、style.cssと同階層にファイルを配置したとしましょう。
export const className: string;
こうしておけば、TypeScriptは「style.cssっていう名前のモジュールが提供されてるのね」って解釈してくれるので、先述のimport文もスルッとコンパイルが通るようになります。
そして、ここが本エントリの主眼でもありますが、.d.tsによって、style.cssが提供するモジュールからは .className
というクラス名しか選択出来ないことが保証されます。
.tsコードを記述する中でCSSクラス名をtypoした場合、コンパイルエラーとしてチェックされることになりますし、エディタ・IDEのコード補完サポートも得られます。
具体的にどうすんの?
さすがに手作業で.cssに対応する.css.d.tsを作成していくのもアホ臭いので、.cssファイルから.css.d.ts ファイルを生成するツールを作りました。
npm -g install typed-css-modules
tcm <cssが格納されているディレクトリ>
で実行します。例えば、tcm src
とすると、src配下の*.cssを探索し同階層にcss.d.tsを作成してくれます。
(project root)
- src/
| myStyle.css
| myStyle.css.d.ts [created]
css-modules-loader-core を使って、CSS Modulesとしてexportされるtokenから.d.tsを作っていますが、多分エラーハンドリング等は大分甘いです。issue書いてくれれば適宜直していこうかと。
使ってみよう
ようやく当初の目的であったタイプセーフなWebアプリ開発の下準備が整いました。
折角ですので、下記の構成でworking demoを作ってみました。
- TypeScript
- React JSX (.tsx)
- CSS Modules
http://quramy.github.io/typescript-css-modules-demo/
見よ、HTMLエレメントも、CSSクラス名も型チェックされる世界を。。。!
import * as styles from './ScopedSelectors.css';
import * as React from 'react';
export default class ScopedSelectors extends React.Component<any, any> {
render() {
return (
<div className={ styles.root }>
<p className={ styles.text }>Scoped Selectors</p>
</div>
);
}
};
@value niceGray: #777;
.root {
border-width: 2px;
border-style: solid;
border-color: niceGray;
padding: 0 20px;
margin: 0 6px;
max-width: 400px;
}
.text {
color: niceGray;
font-size: 24px;
font-family: helvetica, arial, sans-serif;
font-weight: 600;
}
ソースコードはhttps://github.com/Quramy/typescript-css-modules-demoに配置しているので、設定ファイル等の細かい部分が気になるのであれば、こちらも参考にして頂ければと。
先述のtyped-css-modules含め、CSS Modulesに特有のビルドフローが必要となるため、補足しておきます。
- typed-css-modulesを用いて、
tcm styles
実行してcss.d.tsを作成 -
tsc
コマンドで .ts, .tsxを.jsにトランスパイル -
browserify -p [css-modulesify -o dist/bundle.css src/components/App.css] -o dist/bundle.js src/index.js
にて、bundle.js, bundle.cssを作成. 1 - bundle.css, bundle.jsをロードするindex.htmlを作成、ブラウザで読み込むと実行
3.のcss-modulesifyはbrowerifyのpluginです。browserifyがreguire(...)
を解決するときにCSS Moduleの.cssを一緒に探索した上で1ファイルに結合したcssを作ってくれます。
先述したように、2.で先行して.d.tsが作られ、後追いで3.の対応するモジュール実体が生成される順番になります。
なお、tcm
コマンドには --watch
オプションを用意しているので、上記の1〜4のコマンドを以下のように書き換えることで、LiveReload環境も作れます。
tcm --watch styles
tsc --watch
watchify -p [css-modulesify -o dist/bundle.css src/components/App.css] -o dist/bundle.js src/index.js
browser-sync start --server --files bundle.js --files bundle.css
この辺りは、Gulpに頼らない!CLIで作るフロントエンド開発環境に色々と書いているので、こちらも参考まで。
(というより、このエントリ書いた手前、--watch
の無いCLI書くのも負けだよなぁ、と思って作りました)
-
tsc
コマンドではなく、tsifyをbrowserifyのプラグインとして与えるパターンも試してみたのですが、恐らくcss-modulesifyとの処理順序が制御できないためにbundle.jsが作れませんでした ↩