最近、趣味で作りたいWebアプリがあって、結構クライアント側が要になりそうなので、思い切って勢いで Deep Dive してみた話。かなり長いです。
モダンって言ってるけど、TypeScriptが苦手ならES2015+ES7も全然アリだと思う。前回の記事で触れてるから、TypeScriptの代わりに書き換えればうまく載るはず。というか人口的にはTypeScriptよりES2015使う人のが当然多いだろうけど、今回はTypeScript使ってみたかったんです・・・。
Contents
背景
わたしは今までフロンドエンド開発っていうと、CSS3+HTML5で特別な開発環境も使わず、気合いで頑張って作ってきた勢です。
一応、フリーランスでバックもフロントもまとめて依頼されたり、趣味で書いたり、プログラミングスクールで先生やってた頃はフロントも教えていたんですが、とても苦手でした。特にデザインとか全くセンス無いし、デザインも決まってないようなサイトをまるっと任されたりするとすごくストレスで辛かったのを覚えています。
最近はバックエンドやインフラにフォーカスした仕事をしていて、ほぼほぼフロントを書くこともなくなったのですが、数年前より便利なツールや面白そうなライブラリがいろいろ増えていて、せっかく書くならとことんモダンに!ということでやってみました。
今回のゴール
ここで作る環境の最終的なコードはgithubにあります。
https://github.com/syamn/sakuio-blog-160314
- node.js と npm をインストール
 - JavaScript には TypeScript を使う
 - CSS には Scss (Sass) を使う
 - ビューライブラリには RectJS を使う
 - WebPackだけでビルド (GruntやGulpは使わない)
 - WebPack-Dev-Serverで自動リフレッシュを試してみる
 
ネットで情報を探すと、TypeScriptやScssのトランスパイルには未だにGulpが主流で(Gruntは減ってきましたね)、せっかくWebPackを導入してもGulpと組み合わせて使うみたいな記事を多く見かけます。
せっかくWebPackを導入したのなら、WebPackだけで完結させましょう〜!
nodeとnpmのインストール
モダンな開発にはnodeは必須です。フロントエンドなライブラリも、nodeのパッケージマネージャーであるnpmを使ってインストールするのが基本です。
公式サイトのダウンロードページに簡単にインストールできるパッケージがありますが、可能ならnodebrewを使って入れてみてください。
今回はnodeのインストールが主題ではないので飛ばしますが、nodebrewを使ったnodeのインストールについてはこのサイトがとても詳しいです。参考にどうぞ。
プロジェクトの作成
適当なディレクトリを作ってinit。
initはそのパッケージを公開する予定がなければEnter連打で大丈夫。
| 
					 1 2 3  | 
						$ mkdir ./test $ cd ./test $ npm init  | 
					
TypeScript (AltJS)
まず考えたのがJavaScriptを何の言語で書くかです。素のJSで動的に動くフロントを書くのはつらいので個人的には必須です。
結局はブラウザは素のJS(つまりはES5)しか解釈できないので、これらの言語で書いたプログラムはJSに変換(トランスパイル)してあげる必要があります。JSを生成するための代替言語ということで、これらをまとめてAltJSと言ったりします。
今回はTypeScriptを使うことにしました。モダンなJS開発ということでは勢いもあり、なにより型がある安心感を期待しました。
型が使いたければTypeScript、型いらないよーってことならES2015(+ES2016?)がいいんじゃないかなー。
ちなみにCoffeeScriptは昔に使ってたんですが、今は下火ということで考慮外です。
本体のインストール
今回はできるだけ新しいバージョンに追従して開発を進めていきたかったので、nightly buildを選択。仕事で使うときはstableを使ってください。
| 
					 1 2 3  | 
						$ npm install -g typescript@next $ tsc -v Version 1.9.0-dev.20160312  | 
					
グローバルインストールの場合、後述のビルドツール等から参照するために node_modules からリンクさせます。
| 
					 1  | 
						$ npm link typescript  | 
					
型定義ファイルマネージャをインストール
TypeScriptを便利に使うためには様々なライブラリの型定義ファイルが必須です。これはライブラリを使う際に参照して、型の概念を適用します。
昔は型定義ファイルマネージャーとして tsd が有名だったのですが、どうやら2ヶ月くらい前にdeprecateされたようなので、推奨されている typings を使います。
| 
					 1 2 3  | 
						$ npm install -g typings $ typings -v 0.7.8  | 
					
TypeScriptのコンパイルに必要な設定ファイルを生成しておきます。
| 
					 1 2  | 
						$ tsc --init message TS6071: Successfully created a tsconfig.json file.  | 
					
ReactJS
Facebookが開発した、MVCのビュー(V)に特化したライブラリです。
モノリシックなものではAngularが有名だけど、最近Anguler 2が出現して大きく変わったのがリスクに感じていて、あとはReactはそこまで大きくなくて小回りも利きそうなイメージで採用。
他にもVue.jsやRiot.jsも軽めのビュー特化なライブラリとして使ってみたけどいい感じ。でも大きく作っていくならやっぱりReactかな?
本体のインストール
| 
					 1  | 
						$ npm install --save-dev react react-dom  | 
					
TypeScript用定義ファイルのインストール
ReactJSのクラス定義を落としてきて、TypeScriptから扱えるように。
| 
					 1 2 3 4 5 6  | 
						$ typings install --ambient --save-dev react react-dom react └── (No dependencies) react-dom └── (No dependencies)  | 
					
このとき、 typings/browser.d.ts と typings/main.d.ts が生成されますが、browserはフロントエンド用、mainはサーバー用で同じ定義が2重に作られます。何も設定せずにビルドしてしまうと、定義が重複してしまって error TS2300: Duplicate dentifier が大量に出力されてしまう。
今回はフロントエンド用だから、 main.d.ts の方は使わないようにexcludeして、同時にReactのJSXをコンパイルするオプション jsx: react を追加します。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17  | 
						$ cat tsconfig.json {     "compilerOptions": {         "module": "commonjs",         "target": "es5",         "noImplicitAny": false, -        "sourceMap": false +        "sourceMap": false, +        "jsx": "react"     },     "exclude": [ -        "node_modules" +        "node_modules", +        "typings/main.d.ts", +        "typings/main"     ] }  | 
					
React Developer Tools のインストール
これは任意ですが、Reactのデバッグがとても捗るので是非おすすめします。
Reactの各コンポーネントの今のpropsとstateを確認したり、直接変更できます。
Chromeのエクステンションで、ストアからインストールします。
注意点としては、ローカルファイルを開いてもデベロッパーコンソールにReactのタブは出てきません。何かしらのサーバー上から配信する必要あり。
https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi
Scss (AltCSS)
AltJSがあればAltCSSもある。有名どころはSCSSやLESSで、今はSCSSが一番人気。私も昔からこれを使っていたために迷わず選択。
配布する際は特に分割せず、1ファイルを想定。今作ってるサービスはPC向けだし、ミニファイしてもReactのライブラリは重めだし、SPAだから最初にPleaseWait画面を置いて一気にロードしてくれたほうが嬉しいっていう考え。
今回はWebPackのローダーで済ませてしまうので、ここでは特に何もしません。
WebPack (ビルドツール)
最近流行ってるビルドツール。JSだけじゃなくて、CSSや画像ファイルとかもミニファイしたり結合したり、要は配布できる形にビルドしてくれるツール。
タスクランナーとしてはGruntとかGulpもあるけど、最近はこっちのが人気ぽい。自分の場合、Gruntを少し触ってなんとも言えない嫌悪感があって、そういうのもフロント嫌いの要因になってそう。WebPackは多機能な割に設定ファイルも分かりやすくて素晴らしい。
| 
					 1 2 3 4  | 
						$ npm install -g webpack $ webpack --help webpack 1.12.14 (省略)  | 
					
TypeScript用ローダーをインストール
WebPackは単体でも使えるけど、AltJSやAltCSSを使う時は専用のローダーが必要。
TypeScriptには ts-loader を使います。
| 
					 1  | 
						$ npm install --save-dev ts-loader  | 
					
CSS + SCSS用ローダーをインストール
Scssには sass-loader を採用。
ファイルを独立して出力するために専用のプラグインと、プレーンcssも対応させるためにいくつかローダーを追加。
WebPackをグローバルインストールしている場合、 UNMET PEER DEPENDENCY になってしまうので、先にリンクする必要があります。
| 
					 1 2  | 
						$ npm link webpack $ npm install --save-dev extract-text-webpack-plugin style-loader css-loader sass-loader node-sass  | 
					
WebPackの設定ファイルを作る
最後にWebPackでビルド用の設定ファイル webpack.config.js を次の中身で作ります。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36  | 
						var ExtractTextPlugin = require("extract-text-webpack-plugin"); var dist_dir = __dirname + '/dist';  module.exports = [{   entry: {     app: "./src/scripts/index.tsx"   },   output: {      path: dist_dir + '/scripts',     publicPath: '/scripts/',     filename: "app.js"   },   module: {     loaders: [       { test: /\.tsx?$/, loader: "ts-loader" }     ]   } }, {   entry: {     app: './src/styles/loader.js'   },   output: {     path: dist_dir + '/styles',     publicPath: '/styles/',     filename: 'app.css'   },   module: {     loaders: [       { test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader') },       { test: /\.scss$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader') }     ]   },   plugins: [     new ExtractTextPlugin('app.css')   ] }];  | 
					
このファイルでやってるビルド設定は次の2つ。
- ./src/scripts/index.tsx をTypeScriptのエントリポイントとしてロードして、 ./dist/scripts/app.js に変換されたJavaScriptファイルを出力
 - ./src/styles/loader.js をScss(とCSS)のエントリポイントとしてロードして、 ./dist/styles/app.css に変換されたCSSファイルを出力
 
ソースファイルの準備
実際にビルドできるファイルを置いて、実行までやってみます。
Reactコンポーネント
./src/scripts/index.tsx として次のファイルを配置。
このファイルはシンプルなReactのCommentBoxコンポーネントを作成して、HTMLの#content内にレンダリング。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22  | 
						/// <reference path="../../typings/browser.d.ts" /> import * as React from 'react' import * as ReactDOM from 'react-dom' interface CommentBoxProps extends React.Props<any> {   user: string;   link: string; } class CommentBox extends React.Component<any, any> {   render() {     return <div className="commentBox">         Hello, world! It is comment posted by <a href={this.props.link}>{this.props.user}</a>.       </div>;   } } ReactDOM.render(     <CommentBox user="Sakura Onishi" link="https://saku.io" />,     document.getElementById("content") );  | 
					
Scss + CSSファイル
WebPackではエントリポイントとしてScssファイルを記述することはできないので、CSSの参照役となるJavaScriptファイルをまずは作成する。
ここではScssとCSSファイルどちらもソースとして使用できることを確認するために2つのファイルをエントリポイントとし、Scssのファイルからは他のScssファイルを @import を使って結合できることを確かめます。
| 
					 1 2  | 
						require('./file1.css'); require('./file2.scss');  | 
					
素のCSSには、ページの背景色をラベンダーに変更するコードを。
| 
					 1 2 3  | 
						body{   background: lavender; }  | 
					
Scssには、変数と親要素セレクタ、他ファイルのインポートが行うコード。
| 
					 1 2 3 4 5 6 7 8 9 10  | 
						$my-color: brown; .commentBox {   color: $my-color;   &:hover {     text-decoration: none;   } } @import './file3.scss';  | 
					
次のScssファイルは loader.js からではなく、 file2.scss から読み込まれます。
| 
					 1 2 3 4 5 6 7  | 
						a {   color: red;   &:hover {     text-decoration: none;   } }  | 
					
HTMLファイル
最後に、ベースとなるHTMLファイルを ./dist ディレクトリに設置。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12  | 
						<!DOCTYPE html> <html>   <head>     <meta charset="UTF-8" />     <title>Hello World</title>     <link rel="stylesheet" href="styles/app.css" />   </head>   <body>     <div id="content"></div>     <script src="scripts/app.js"></script>   </body> </html>  | 
					
これでビルドの準備が整いました。
ビルド実行!
ここまでくれば、あとは webpack コマンドを叩くだけ! 開発中は -d を付けることでソースマップファイル(コンパイル前のソースと関連付けられ、エラーが発生したときに元のソースの行番号を表示できる)が同時に出力されるので、これを使います。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25  | 
						$ webpack -d ts-loader: Using typescript@1.9.0-dev.20160312 and /path/to/your/tsconfig.json Hash: 6ab8e7bd9343dc55410ade5fbde73c3511529f62 Version: webpack 1.12.14 Child     Hash: 6ab8e7bd9343dc55410a     Version: webpack 1.12.14     Time: 2669ms          Asset    Size  Chunks             Chunk Names         app.js  717 kB       0  [emitted]  main     app.js.map  795 kB       0  [emitted]  main         + 159 hidden modules Child     Hash: de5fbde73c3511529f62     Version: webpack 1.12.14     Time: 1526ms           Asset       Size  Chunks             Chunk Names         app.css  212 bytes    0, 0  [emitted]  style, style     app.css.map   84 bytes    0, 0  [emitted]  style, style        [0] ./src/styles/loader.js 50 bytes {0} [built]         + 6 hidden modules     Child extract-text-webpack-plugin:             + 2 hidden modules     Child extract-text-webpack-plugin:             + 2 hidden modules  | 
					
うまくいけば上のようなビルド成功のメッセージが表示され、 ./dist の中に scripts と styles のディレクトリが作られているはず!
開発用サーバーの起動
ビルドがうまくいけば、単に ./dist/index.html をブラウザで開くことで確認できます。でもその場合は少し上でも触れた通り、ReactのChromeツールは使えません。折角WebPack使ってるので、開発用のサーバーもWebPackのものを使ってみましょう。
単にローカルのApacheや、nodeのhttp-serverを使ってホスティングすることもできますが、WebPack-Dev-Sererを使うとファイル変更検知で自動ビルド + ブラウザを自動更新してくれます。かしこい!
ただ注意点として、このサーバーでのビルドはインメモリに保存されるため、実際の出力ファイルを変更するためには前述の webpack コマンドを実行する必要があります。他のサーバーを使用したい場合は webpack --watch を実行して、変更を監視し自動でビルドさせるとよいでしょう。
WebPack同様にグローバルインストールにします。
| 
					 1  | 
						$ npm install -g webpack-dev-server  | 
					
webpack-dev-server コマンドを使って起動します。
| 
					 1 2 3  | 
						$ webpack-dev-server --content-base ./dist/ --inline (省略) webpack: bundle is now VALID.  | 
					
エラーにならずに起動できたら、ブラウザから http://localhost:8080 を開いてみます。
うまく表示されました!
./src/scripts/index.tsx などを更新して、自動でページが更新されるかチェックしてみてください。もし動かない場合は、iframeを使う方法を試してみてください。(公式ドキュメント)ただし、その場合はReactのChromeデバッガは使えなくなってしまいます。
最後に package.json のスクリプトに上の一行を追加しておくといいかも。
| 
					 1 2 3 4 5 6  | 
						$ cat package.json (省略)   "scripts": {     "server": "webpack-dev-server --content-base ./dist/ --inline"   } (省略)  | 
					
これで次からは
| 
					 1  | 
						$ npm run server  | 
					
だけで起動できる。
所感
実際この環境を構築しながら執筆していて2日間かかりました・・・。自分のためにまとめてみたものの、思ったより大変でちょっと辛かったです。
でもこれは書いておかないと、また次に作る時すごく大変なのが目に見えているので。。
今回一番考えていたのが、いかにGulpを使わないようにするかってことで、最終的にはすべてWebPackの機能で構築することができて良かったです。
余力があればESLintやテスト系のツールも入れるべきですね。また別記事で書くかもしれません。
環境構築で疲れていては頑張った意味がないので、これからこの環境でアプリ部分がんばって書いて行きますよー!



