(訳者注: これは、JavaScript Stack from Scratchを翻訳し、まとめて読めるように1ファイルにしたものです。元の翻訳と各種ファイルについては、日本語訳forkリポジトリを参照してください。また、原文が活発に更新されているため、訳文も追従して更新されます。ご了承ください。)
ゼロから始めるJavaScript生活
モダンJavaScriptスタックチュートリアル、ゼロから始めるJavaScript生活へようこそ。
⚠️️ このチュートリアルのメジャーアップデート版を3月初旬に公開する予定です。ご期待下さい! より詳しく(英語).
これはJavaScriptスタックを使い始めるための最短最速のガイドです。このガイドは一般的なプログラミングの知識とJavaScriptの基礎を前提としています。これら全てのツールを一緒につなぎ合わせることにフォーカスしており、各ツールについて可能な限りシンプルな例を提供します。このチュートリアルは、独自のボイラープレートをゼロから準備するための方法として見ることもできます。
もちろん、ちょっとしたJSによるインタラクションを含むだけのシンプルなウェブページを作るのであれば、ここで紹介するスタックを全て使う必要はありません(ES6コードを複数のファイルに書いてCLIでコンパイルしたいのであれば、Browserify/Webpack + Babel + jQueryの組み合わせで十分です)。しかし、スケールするウェブアプリを構築するにあたってセットアップの手助けが必要であれば、このチュートリアルが重宝するでしょう。
このチュートリアルの目標はさまざまなツールを組み立てることにあるため、これらのツールの仕組みについて、それぞれの詳細については触れません。それらのより深い知識を習得したい場合、それぞれのドキュメントを参照するか、他のチュートリアルを探してください。
このチュートリアルで説明しているスタックはその大部分にReactを使用しています。もし初心者の方がReactについてのみ学びたいのであれば、create-react-appで設定済みのReact環境をすぐに試すことができます。例えばすでにReactを使っているチームに加わり、学習用playgroundでキャッチアップするということであれば、このようなアプローチをお勧めします。このチュートリアルでは、内部で何が行っているかをすべて理解してもらえるよう、既存の設定を使わないようにしています。
コード例は章ごとに利用できます。それぞれyarn && yarn start
またはnpm install && npm start
で実行できます。各章のステップバイステップの指示に従って全てゼロから入力することを推奨します。
すべての章には、それまでの章のコードが含まれています 。そのため、全てを含んだプロジェクトのボイラープレートを探しているのであれば、最終章をcloneすればそれで使えます。
注意: 章の順序は必ずしも最も教育的なものではありません。たとえば、テスティング/型検査はReactを導入する前に行うこともできます。しかしながら、章を移動させたり以前の章を修正するのは、以降の章を全て変更する必要があるため、かなり困難です。いったん状況が落ち着いてから、より良い方法のために配置を変更するかもしれません。
このチュートリアルのコードはLinux、macOS、Windows上で動作します。
目次
1 - Node、NPM、Yarn、そして package.json
2 - packageのインストールと使用
3 - Babel と Gulp による ES6 のセットアップ
4 - ES6のクラス構文を使う
5 - ES6モジュール構文
6 - ESLint
7 - Webpackによるクライアントアプリ
8 - React
9 - Redux
10 - Immutable JS と Redux の改良
11 - Mocha、Chai、Sinonによるテスティング
12 - Flowによる型検査
今後の予定
本番/開発環境, Express, React Router, サーバサイドレンダリング, Styling, Enzyme, Gitフック。
Credits
Created by @verekia – verekia.com.
License: MIT
1 - Node、NPM、Yarn、そしてpackage.json
この章ではNode、NPM、Yarn、そして基本的なpackage.json
ファイルのセットアップを行います.
まず、バックエンドのJavaScriptとして使われるだけではなく、モダンなフロントエンドスタックを構築するためのツールとしても使われる、Nodeをインストールする必要があります。
macOSとWindowsバイナリ用ダウンロードページに行くか、Linuxディストリビューション用のpackage manager installations pageに行きます。
たとえば、UbuntuかDebian であれば、Nodeをインストールするために以下のコマンドを実行します。
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
sudo apt-get install -y nodejs
Node のバージョンは6.5.0以降であればどれでも構いません。
Nodeの標準パッケージマネージャー、npm
はNodeに付属しているため、別途インストールする必要はありません。
注意: Nodeがすでにインストールされている場合、nvm
(Node Version Manager) をインストールし、nvm
を使って最新のNodeをインストールしてください。
YarnはNPMよりも高速なもう一つのパッケージマネージャで、オフライン環境にも対応し、依存しているものをより期待通りに取得します。Yarnは2016年10月にリリースされて以来、パッケージマネージャの新たな選択肢としてJavaScriptコミュニティに急速に受け入れられつつあります。このチュートリアルではYarnを使っています。NPMを使いたい場合は、yarn add
やyarn add --dev
などのコマンドを、npm install --save
やnpm install --save-dev
などと読み替えてください。
-
インストール手順に従ってYarnをインストールします。
npm install -g yarn
やsudo npm install -g yarn
などでインストールすることもできます(そうです、YarnをインストールするのにNPMを使うのです。Internet ExplorerやSafariを使ってChromeをインストールするようなものですね!) -
新しい作業用フォルダを作り、
cd
で移動します。 -
yarn init
を実行し、質問に答えると(yarn init -y
で全ての質問をスキップできます)、package.json
ファイルが自動生成されます。 -
console.log('Hello world')
と書かれたindex.js
ファイルを作ります。 -
そのフォルダ内で
node .
と実行します(index.js
はNodeがカレントフォルダ内で探すデフォルトのファイル名です)。"Hello world"と表示されるはずです。
node .
でプログラムを実行させるのはやや低レベル寄りです。代わりにNPM/Yarnスクリプトを使ってコードを実行します。より複雑なプログラムを実行する場合でも、必ずyarn start
で実行できるようにするのは、適切な抽象化になっています。
-
package.json
内のscripts
オブジェクトに"start": "node ."
と追加します。
"scripts": {
"start": "node ."
}
package.json
は構文的に正しいJSONファイルでなければならないので、末尾のカンマを追加してはいけません。package.json
ファイルを手で編集する時はよく注意してください。
-
yarn start
と実行します。Hello world
と表示されるはずです。 -
.gitignore
を作り、以下の内容を追加します。
npm-debug.log
yarn-error.log
注意: 私が作ったpackage.json
ファイル内には、各章に対してtutorial-test
スクリプトがあります。このスクリプトはyarn && yarn start
で各章が正しく実行されることを私自身がテストするためのものです。他のプロジェクトに使う場合は、ここは削除しても構いません。
(原文: 1 - Node, NPM, Yarn, and package.json)
2 - パッケージのインストールと利用
この章では、パッケージをインストールして使ってみます。「パッケージ」とは他の誰かが書いたコードの集まりのことで、これから書くコード内で利用することができます。どんなコードでもです。ここでは、例として色を扱うためのパッケージを使ってみます。
- コミュニティ製のパッケージ
color
をインストールするため、yarn add color
と実行します。
package.json
を開くと、Yarnがdependencies
にcolor
を自動的に追加したのがわかります。
そしてnode_modules
フォルダが作られ、そこにパッケージが保存されます。
-
.gitignore
ファイルにnode_modules/
を追加します(まだgit init
を実行していなければここで実行します)。
yarn.lock
というファイルをYarnが生成したことにも気づくでしょう。このファイルはリポジトリにコミットしておくべきです。こうしておけば、チーム内のメンバー全員が同じバージョンのパッケージを利用するようになります。もしYarnではなくNPMを使いたい場合は、shrinkwrapがこのファイルの代わりとなります。
-
index.js
ファイルにconst Color = require('color');
と追加します。 - 例として以下のような形でパッケージを使ってみます:
const redHexa = Color({r: 255, g: 0, b: 0}).hex();
-
console.log(redHexa)
を追加します。 -
yarn start
と実行すると、#FF0000
と表示されるはずです。
おめでとうございます、パッケージをインストールして使うことができました!
color
はこの章の中でしか使わない、パッケージの使い方を説明するためだけのシンプルなパッケージです。もう不要になったので、アンインストールして構いません。
-
yarn remove color
を実行します
注意: パッケージの依存関係には、"dependencies"
と"devDependencies"
の2種類あります。"dependencies"
は"devDependencies"
より一般的なもので、後者は開発している間だけ使うパッケージであり、本番環境では使いません(典型的には、ビルド関係のパッケージや、linterなどがあります)。"devDependencies"
に追加するには、yarn add --dev [package]
と実行します。
(原文: 2 - Installing and using a package)
3 - BabelとGulpによるES6のセットアップ
ES6構文を使ってみましょう。ES6構文は「古い」ES5構文を大幅に改善したものです。全てのブラウザとJS環境はES5を理解しますが、ES6は理解するとは限りません。そのため、Babelと呼ばれる、ES6ファイルをES5ファイルに変換するツールを使います。Babelを実行するには、タスクランナーのGulpを使います。package.json
ファイルのscripts
以下に書くタスクに似ていますが、タスクをJSファイルに書く方がJSONファイルに書くよりもシンプルで汚くなりにくいです。そのため、Gulpと、Gulp用のBabelプラグインもインストールしておきましょう:
-
yarn add --dev gulp
と実行します -
yarn add --dev gulp-babel
と実行します -
yarn add --dev babel-preset-latest
と実行します -
yarn add --dev del
と実行します (これは後述の通り、clean
タスクのためです) -
package.json
ファイルに、Babelの設定のためのbabel
フィールドを追加します。最新のBabelプリセットを使うため、以下のようにします:
"babel": {
"presets": [
"latest"
]
},
注意: .babelrc
ファイルがプロジェクトのルートにあると、package.json
のbabel
よりもそちらが優先されます。ルートフォルダはそのうちどんどん膨れ上がっていくため、package.json
が巨大にならないうちは、Babelの設定はpackage.json
内に記述するようにします。
-
index.js
ファイルを新たなsrc
フォルダに移動します。これはES6のコードを記述するためのフォルダです。lib
フォルダはコンパイル後のES5コードが置かれます。GulpとBabelはこのコード生成の面倒を見ます。index.js
にある前章のcolor
関連のコードを削除し、以下の簡単な内容に書き換えます:
const str = 'ES6';
console.log(`Hello ${str}`);
ここではテンプレート文字列リテラルを使っています。これはES6の新しい構文で、${}
を使った文字列連結なしで変数を直接文字列内に埋め込むためのものです。テンプレート文字列リテラルはバッククォートを使っていることに注意してください。
- 以下の内容で
gulpfile.js
を作ります:
const gulp = require('gulp');
const babel = require('gulp-babel');
const del = require('del');
const exec = require('child_process').exec;
const paths = {
allSrcJs: 'src/**/*.js',
libDir: 'lib',
};
gulp.task('clean', () => {
return del(paths.libDir);
});
gulp.task('build', ['clean'], () => {
return gulp.src(paths.allSrcJs)
.pipe(babel())
.pipe(gulp.dest(paths.libDir));
});
gulp.task('main', ['build'], (callback) => {
exec(`node ${paths.libDir}`, (error, stdout) => {
console.log(stdout);
return callback(error);
});
});
gulp.task('watch', () => {
gulp.watch(paths.allSrcJs, ['main']);
});
gulp.task('default', ['watch', 'main']);
全体を理解するために少し時間をとりましょう。
GulpのAPI自体は非常に率直なものです。gulp.task
を定義し、その中ではgulp.src
でファイルを参照し、そのファイルに対し一連の操作を.pipe()
を使って適用し(この例のbabel()
のように)、新しいファイルをgulp.dest
に出力します。Gulpのタスクは、gulp.task
の第2引数として(['build']
のような)配列の形で渡された必要なタスクを先に実行することができます。より完全な紹介は、ドキュメントを参照してください。
まず最初にpaths
オブジェクトを定義します。これは異なるファイルパスを格納し、DRYに保つためです。
続いて5つのタスク: build
、clean
、main
、watch
、そしてdefault
を定義します。
-
build
はsrc
以下のソースファイルを変換しlib
に保存するため、Babelが呼ばれるところです。 -
clean
はbuild
を行う前に、lib
フォルダに自動生成されるファイル全てを削除するタスクです。このタスクは、src
内のファイルをリネームや削除したり、ビルドに失敗したことに気づかない場合でもlib
フォルダをsrc
フォルダに確実に同期させるため、生成されたファイルを削除するのに便利です。Gulpストリームと連携してファイルを削除するために、ここではdel
パッケージを使っています(これはGulpでのファイルの削除方法として推奨されている方法です)。 -
main
は前章でnode .
と入力し実行させているのとほぼ同じもので、lib/index.js
を実行することだけが異なっています。index.js
はNodeが探しにいく標準のファイル名なので、単にnode lib
とだけ書いています(DRYを守るためにlibDir
変数を使っています)。タスク内のrequire('child_process').exec
とexec
の部分はシェルコマンドを実行するNodeの標準関数です。stdout
はconsole.log()
に渡し、gulp.task
のコールバック関数を使って潜在的なerrorを返します。この部分がはっきりと理解できなくても気にする必要はありません。このタスクは基本的にnode lib
を実行するためのものであるとだけ覚えてください。 -
watch
は特定のファイルに更新があった場合にmain
タスクを実行します。 -
default
は、コマンドラインで単にgulp
と実行した場合に呼び出される特殊なタスクです。
注意: どうしてGulpファイル内でES6コードが使えるのか、気になっているかもしれません。なぜならこのコードはBabelでES5コードにトランスパイルされないからです。その理由は、ここではES6を標準でサポートしているバージョンのNodeを使っているためです(node -v
を実行し、バージョン6.5.0以上のNodeを使っていることを確認してください)。
さあ、動かしてみましょう!
-
package.json
内のstart
スクリプトを"start": "gulp"
に変更します。 -
yarn start
と実行します。"Hello ES6"と表示され、変更の監視が始まるはずです。src/index.js
に間違ったコードを書いてみて、saveしたときにGulpが自動的にエラーを表示するか試してみましょう。 -
/lib/
を.gitignore
に追加します。
(原文: 3 - Setting up ES6 with Babel and Gulp)
4 - ES6のクラス構文を使う
- 新しいファイル
src/dog.js
を作り、以下のES6クラスを書きます:
class Dog {
constructor(name) {
this.name = name;
}
bark() {
return `Wah wah, I am ${this.name}`;
}
}
module.exports = Dog;
以前から他の言語でOOPをしたことがあるなら、特に意外な点はないでしょう。しかし、JavaScriptにクラス構文が取り入れられたのはつい最近なのです。このクラスはmodule.exports
へ代入することにより、外の世界にさらされます。
典型的なES6コードはクラスやconst
、let
、 bark()
で使われているような(backtickによる)"テンプレート文字列"、アロー関数((param) => { console.log('Hi'); }
)などが使われます。この例では使われていないものもありますが。
src/index.js
ファイルに、以下のように書きます:
const Dog = require('./dog');
const toby = new Dog('Toby');
console.log(toby.bark());
このように、先ほど使ったコミュニティ製のcolor
パッケージとは異なり、自分で作ったファイルをrequireするときには、require()
内で./
を使います。
-
yarn start
を実行します。'Wah wah, I am Toby'と表示されるはずです。 -
lib
に生成されたコードを見て、コンパイルされたコードがどのようになっているかを確認します(例えばconst
の代わりにvar
があるなど).
(原文: 4 - Using the ES6 syntax with a class)
5 - ES6モジュール構文
ここでは、const Dog = require('./dog')
をimport Dog from './dog'
に置き換えます。これは("CommonJS"のモジュール構文と比べると)より新しいES6モジュールの構文です。
dog.js
内のmodule.exports = Dog
をexport default Dog
に置き換えます。
dog.js
では、Dog
の名前はexport
の中でしか使われていないことに注意してください。そのため、次の例のように、匿名のクラスを直接エクスポートすることも可能です:
export default class {
constructor(name) {
this.name = name;
}
bark() {
return `Wah wah, I am ${this.name}`;
}
}
index.js
ファイルのimport
での'Dog'という名前は実際には使う人次第だと思われたかもしれません。実際、このように書いても問題なく動きます。
import Cat from './dog';
const toby = new Cat('Toby');
もちろん、インポートしたclass / moduleの名前をそのまま使うことがほとんどです。そうしない例としては、Gulpファイル内でconst babel = require('gulp-babel')
とする場合が挙げられます。
では、gulpfile.js
ファイル内のrequire()
はどうでしょうか? 代わりにimport
が使えるでしょうか? 最新版のNodeはES6のほとんどの機能に対応していますが、ES6モジュールにはまだ対応していません。ありがたいことに、GulpはBabelに手助けをしてもらえます。gulpfile.js
をgulpfile.babel.js
にリネームすれば、Babelはimport
されたモジュールをGulpに渡す役割を担ってくれます。
-
gulpfile.js
をgulpfile.babel.js
にリネームします。 -
require()
を以下のように書き換えます:
import gulp from 'gulp';
import babel from 'gulp-babel';
import del from 'del';
import { exec } from 'child_process';
child_process
からexec
が直接展開されるシンタックスシュガーに注意してください。実に美しいです!
-
yarn start
を実行すると "Wah wah, I am Toby" と表示されるはずです。
(原文: 5 - The ES6 modules syntax)
6 - ESLint
潜在的な問題を見つけるため、コードをlintしましょう。ESLintはES6コード用のlinterです。ルールを自身で細かく設定する代わりに、Airbnbが作った設定を利用します。この設定はプラグインをいくつか使うため、プラグインをインストールする必要があります。
-
yarn add --dev eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
を実行します
このように、複数のパッケージを一つのコマンドでインストールできます。いつものように、これらは全て`package.jsonに追加されます。
package.json
に以下のようなeslintConfig
フィールドを追加します:
"eslintConfig": {
"extends": "airbnb",
"plugins": [
"import"
]
},
plugins
の部分はES6のimport構文を使うことをESLintに教えています。
注意: プロジェクトのルートにある.eslintrc.js
や.eslintrc.json
、.eslintrc.yaml
ファイルの代わりに、package.json
ファイルのeslintConfig
フィールドを使うこともできます。Babelの設定と同様、ルートフォルダが多くのファイルで溢れるのを避けるためこのようにしていますが、ESLintの設定が複雑化してきたら、この代替策を検討してください。
ESLintを実行するためのGulpタスクが必要です。そのため、ESLint Gulpプラグインをインストールします。
-
yarn add --dev gulp-eslint
を実行します
次のようなタスクをgulpfile.babel.js
に追加します:
import eslint from 'gulp-eslint';
const paths = {
allSrcJs: 'src/**/*.js',
gulpFile: 'gulpfile.babel.js',
libDir: 'lib',
};
// [...]
gulp.task('lint', () => {
return gulp.src([
paths.allSrcJs,
paths.gulpFile,
])
.pipe(eslint())
.pipe(eslint.format())
.pipe(eslint.failAfterError());
});
Gulpにこのタスクを伝えるため、gulpfile.babel.js
と、src
以下にあるJSファイルをインクルードする必要があります。
以下のように、Gulpタスクbuild
の前提条件にlint
タスクを追加します:
gulp.task('build', ['lint', 'clean'], () => {
// ...
});
-
yarn start
を実行すると、Gulpfile内のたくさんのlintのエラーと、index.js
内でconsole.log()
を使っている警告が表示されるはずです。
警告の一種として、'gulp' should be listed in the project's dependencies, not devDependencies (import/no-extraneous-dependencies)
といった出力があるとか思います。これは実際には誤検知に当たります。ESLintはビルド時のみに使われるJSファイルがどれで、そうではないJSファイルがどれかを知ることができません。そのため、コード内のコメントを使ってESLintに教えてあげる必要があります。gulpfile.babel.js
ファイルの先頭に、以下を追加します:
/* eslint-disable import/no-extraneous-dependencies */
こうすることで、ESLintはこのファイルにimport/no-extraneous-dependencies
ルールを適用しなくなります。
残る問題はUnexpected block statement surrounding arrow body (arrow-body-style)
です. これは貴重な指摘です。ESLintは以下のようなコードのもっと良い書き方を教えてくれているのです。
() => {
return 1;
}
これは次のように修正するべきです:
() => 1
関数がreturn文しか含まない場合、ブレースとreturn文とセミコロンを省略できるためです。
それではGulpファイルを正しく修正してみましょう:
gulp.task('lint', () =>
gulp.src([
paths.allSrcJs,
paths.gulpFile,
])
.pipe(eslint())
.pipe(eslint.format())
.pipe(eslint.failAfterError())
);
gulp.task('clean', () => del(paths.libDir));
gulp.task('build', ['lint', 'clean'], () =>
gulp.src(paths.allSrcJs)
.pipe(babel())
.pipe(gulp.dest(paths.libDir))
);
最後の問題はconsole.log()
についてのものです。この例で問題の元となっているindex.js
内でのconsole.log()
は正しいものであることを伝えましょう。ご想像の通り、/* eslint-disable no-console */
をindex.js
の先頭に置きます。
-
yarn start
を実行すると、すべて解決されている状態に戻ります。
注意: この章ではコンソール用にESLintを設定しています。ビルド時/pushする前にエラーを見つけられるのは素晴らしいことですが、もしかするとIDEにも統合したくなるかもしれません。しかし、IDEに付属しているES6用のlintツールを使わないでください。そのツールがlinting用に使っているバイナリはnode_modules
フォルダにあるように設定してください。そううするとそのツールはプロジェクトの全ての設定、Airbnbのプリセットなど全てが利用できるようになります。そうでなければ、一般的なES6のlintingしか使えるようになりません。
(原文: 6 - ESLint)
7 - Webpackによるクライアントアプリ
アプリの構造
- プロジェクトのルートに
dist
フォルダを作り、以下のindex.html
ファイルを追加します:
<!doctype html>
<html>
<head>
</head>
<body>
<div class="app"></div>
<script src="client-bundle.js"></script>
</body>
</html>
src
フォルダには、次のサブフォルダを作ります: server
、shared
、client
。そしてカレントにあるindex.js
をserver
フォルダに、dog.js
をshared
フォルダに移動します。client
フォルダにはapp.js
ファイルを作ります。
Nodeバックエンドは使わないのですが、この分割はそれぞれの所属をはっきりさせるのに役立ちます。server/index.js
内のimport Dog from './dog';
をimport Dog from '../shared/dog';
に変更する必要があり、そうしなければESLintは解決できないモジュールのエラーを検出してくれます。
client/app.js
ファイルに以下を書きます:
import Dog from '../shared/dog';
const browserToby = new Dog('Browser Toby');
document.querySelector('.app').innerText = browserToby.bark();
package.json
のeslintConfig
に以下を追加します:
"env": {
"browser": true
}
こうすると、window
やdocument
といったブラウザでは必ずアクセスできる変数を使っていても、ESLintは未定義変数の警告を出さないようになります。
さらに、Promise
など、最新のESの仕様を使いたい場合には、Babel Polyfillをクライアントコードに含める必要があります。
-
yarn add babel-polyfill
を実行します
そしてapp.js
の先頭に、次のようなimportを追加します:
import 'babel-polyfill';
このpolyfillを使うと、バンドルのサイズが増えるため、これらの機能を使いたい場合のみ追加するようにします。このチュートリアルでは無駄のないボイラープレートのコードを提供するために、このpolyfillを導入し、次章以降のコードサンプルで使っていきます。
Webpack
Node環境では、いろんなファイルを自由にimport
しても、Nodeはファイルシステムを使って適切に解決してくれました。一方ブラウザでは、ファイルシステムがないため、import
もファイルを参照することができません。エントリポイントのファイルであるapp.js
が必要なインポートのツリー構造を探してこれるよう、依存関係のツリー全体を1つのファイルに「バンドル」しなければなりません。このためのツールがWebpackです。
WebpackはGulp同様、webpack.config.js
という設定ファイルを使用します。GulpはBabelを利用してES6のimportとexportを使えるようにしていましたが、Webpackも同様のことができます。そのためには、webpack.config.babel.js
という名前のファイルを使います。
-
空のファイル
webpack.config.babel.js
を作ります -
Gulpの
link
タスクにwebpack.config.babel.js
を、paths
定数にいくつかパスを追加します。
const paths = {
allSrcJs: 'src/**/*.js',
gulpFile: 'gulpfile.babel.js',
webpackFile: 'webpack.config.babel.js',
libDir: 'lib',
distDir: 'dist',
};
// [...]
gulp.task('lint', () =>
gulp.src([
paths.allSrcJs,
paths.gulpFile,
paths.webpackFile,
])
.pipe(eslint())
.pipe(eslint.format())
.pipe(eslint.failAfterError())
);
WebpackにBabelを使ってES6ファイルを扱う方法を教える必要があります(GulpにES6ファイルの扱いを教えるためにgulp-babel
を使ったのと同様です)。Webpackでは、古い素のJavaScriptではないものを扱う場合、loadersを使います。Webpack用のBabel loaderをインストールしてみましょう。
-
yarn add --dev babel-loader
を実行します -
webpack.config.babel.js
ファイルに以下を書きます:
export default {
output: {
filename: 'client-bundle.js',
},
devtool: 'source-map',
module: {
loaders: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: [/node_modules/],
},
],
},
resolve: {
extensions: ['', '.js', '.jsx'],
},
};
これを解読してみましょう:
このファイルはWebpackが読めるようにexport
する必要があります。output.filename
が生成したいバンドルのファイル名です。devtool: 'source-map'
はブラウザでのデバッグが捗るようにするためのソースマップを有効にします。module.loaders
にはtest
があります。これはbabel-loader
がどんなファイルを扱うかテストするかを正規表現で指定しています。次章では.js
ファイルと(React用の).jsx
ファイルの両方を扱うため、/\.jsx?$/
という正規表現を使っています。node_modules
フォルダはトランスパイルを行わないため、excludeで排除しています。resolve
の部分では、拡張子なしでimport
した場合、どのような種類のファイルをimport
するべきかをWebpackに指定しています。これは例えばimport Foo from './foo'
と書くと、foo
のところはfoo.js
やfoo.jsx
と解釈されるようにするためのものです。
さて、Webpackの設定は終わりましたが、実行するにはもうちょっとかかります。
WebpackをGulpに統合する
Webpackはさまざまなことを行います。プロジェクトがクライアントサイドのものであれば、Gulpをすっかり置き換えてしまうこともできます。一方で、Gulpはもっと汎用的なツールで、linting、テスト、バックエンドタスクなどに向いています。初学者の人にとっては、Webpackの複雑な設定よりもシンプルで理解しやすいでしょう。ここではすでにGulpのセットアップとワークフローを構築済みなので、WebpackをGulpのビルドに統合させるのが理想的でしょう。
それではWebpackを実行するGulpタスクを作りましょう。gulpfile.babel.js
を開きます。
node lib/
を実行するmain
タスクはもう必要ありません。index.html
を開けばアプリが実行されるためです。
-
import { exec } from 'child_process'
を削除します
Gulpプラグイン同様、webpack-stream
パッケージを使えば、GulpにWebpackに統合するのも簡単です。
-
yarn add --dev webpack-stream
を実行し、パッケージをインストールします -
以下の
import
を追加します:
import webpack from 'webpack-stream';
import webpackConfig from './webpack.config.babel';
2行目で設定ファイルを取り込んでいます。
先ほど説明したとおり、次章では.jsx
ファイルを使います(まず使うのはクライアント側でですが、サーバ側でも後ほど使用します)。ちょっと早いですがさっそく設定しておきましょう。
- constの定数宣言を次のようにします:
const paths = {
allSrcJs: 'src/**/*.js?(x)',
serverSrcJs: 'src/server/**/*.js?(x)',
sharedSrcJs: 'src/shared/**/*.js?(x)',
clientEntryPoint: 'src/client/app.js',
gulpFile: 'gulpfile.babel.js',
webpackFile: 'webpack.config.babel.js',
libDir: 'lib',
distDir: 'dist',
};
.js?(x)
は.js
ファイルと.jsx
ファイルにマッチするパターンです。
アプリケーションの別々に分けられた部分についてと、エントリポイントのファイルが定数に追加されました。
-
main
を次のように変更します:
gulp.task('main', ['lint', 'clean'], () =>
gulp.src(paths.clientEntryPoint)
.pipe(webpack(webpackConfig))
.pipe(gulp.dest(paths.distDir))
);
注意: build
タスクは現状、src
以下にある全ての.js
ファイルをES6からES5にトランスパイルします。ここではコードをserver
とshared
、client
に分割しています。これはserver
とshared
のみこのタスクでコンパイルするためのものです(client
はWebpackが面倒を見るためです)。しかしながら、テスティングの章では、Webpackの外でテストするためclient
のコードもGulpでコンパイルする必要があります。そのため、その章にたどり着くまで、不要な重複ビルドが行われることになりますが、今のところはこれでも良いと同意してもらえると思います。実際には、今ここで扱うのはクライアントバンドルのみなので、build
タスクもlib
フォルダもその章までは特に使う必要がないものです。
-
yarn start
を実行すると、Webpackがclient-bundle.js
ファイルを作ります。index.html
をブラウザで開くと"Wah wah, I am Browser Toby"と表示されるはずです。
最後に: lib
フォルダとは異なり、dist/client-bundle.js
ファイルとdist/client-bundle.js.map
ファイルはビルドの度にclean
タスクで消されません。
-
clientBundle: 'dist/client-bundle.js?(.map)'
をpaths
の設定に追加し、clean
タスクを以下のように変更します:
gulp.task('clean', () => del([
paths.libDir,
paths.clientBundle,
]));
-
/dist/client-bundle.js*
を.gitignore
ファイルに追加します。
(原文: 7 - Client App with Webpack)
8 - React
アプリの描画にReactを使ってみましょう。
まず、ReactとReactDOMをインストールします:
-
yarn add react react-dom
を実行します
この2つのパッケージは"devDependencies"
ではなく"dependencies"
に追加されます。ビルドツールと異なり、クライアントバンドルは本番環境用に使われるためです。
src/client/app.js
をsrc/client/app.jsx
にリネームし、ReactとJSXのコードを書いてみましょう:
import 'babel-polyfill';
import React, { PropTypes } from 'react';
import ReactDOM from 'react-dom';
import Dog from '../shared/dog';
const dogBark = new Dog('Browser Toby').bark();
const App = props => (
<div>
The dog says: {props.message}
</div>
);
App.propTypes = {
message: PropTypes.string.isRequired,
};
ReactDOM.render(<App message={dogBark} />, document.querySelector('.app'));
注意: もしReactとそのPropTypesをよく知らないのであれば、まずReactについて学習してからこのチュートリアルに戻ってきてください。後の章ではReactが多用されるため、よく理解しておく必要があります。
Gulpfile内のclientEntryPoint
の値に.jsx
拡張子を追加します:
clientEntryPoint: 'src/client/app.jsx',
ここではJSXの構文を使うため、BabelにJSXを変換するよう伝える必要があります。BabelにJSX構文の処理方法を教えるReact用のBabelプリセットをインストールします: yarn add --dev babel-preset-react
そしてpackage.json
ファイルのbabel
エントリを次のように変更します:
"babel": {
"presets": [
"latest",
"react"
]
},
これでyarn start
実行後、index.html
を開くと、Reactが"The dog says: Wah wah, I am Browser Toby"と出力するのを確認できるはずです。
(原文: 8 - React)
9 - Redux
本章では(おそらくここまでで最も難しい章なのですが)、アプリケーションにReduxを追加し、Reactと連携させます。Reduxはアプリケーションの状態を管理するもので、アプリの状態を表現する素のJavaScriptオブジェクトであるstore、一般的にユーザが起点となるaction、アクションハンドラにも見えるreducerが合わさったものです。reducerはアプリケーションの状態(store)に影響を及ぼし、アプリケーションの状態が変更がされると、アプリケーションに何かが起こります。Reduxの分かりやすい動くデモがここにあります。
Reduxを可能な限りシンプルに扱うにはどうすればよいか説明するため、アプリはメッセージとボタンで構成されるようにします。メッセージは犬が鳴いているかどうか(最初は鳴いていません)を伝え、ボタンを押すと犬が鳴き、メッセージが更新されるようにします。
ここでは、redux
とreact-redux
の2つのパッケージが必要になります。
-
yarn add redux react-redux
を実行します。
はじめに、2つのフォルダ、src/client/actions
とsrc/client/reducers
を作ります。
-
actions
内に、以下のdog-actions.js
ファイルを作ります:
export const MAKE_BARK = 'MAKE_BARK';
export const makeBark = () => ({
type: MAKE_BARK,
payload: true,
});
ここではアクションタイプMAKE_BARK
を定義しています。また、MAKE_BARK
アクションのトリガーとなる関数(これはaction creatorとも言います)makeBark
も定義します。どちらも他のファイルから利用するため、exportされています。このアクションはFlux Standard Actionモデルで実装されています。type
属性とpayload
属性を持っているのはそのためです。
-
reducers
フォルダに以下のようなdog-reducer.js
ファイルを作ります:
import { MAKE_BARK } from '../actions/dog-actions';
const initialState = {
hasBarked: false,
};
const dogReducer = (state = initialState, action) => {
switch (action.type) {
case MAKE_BARK:
return { hasBarked: action.payload };
default:
return state;
}
};
export default dogReducer;
ここではアプリの初期状態として、hasBarked
プロパティがfalse
になっている状態を定義しています。また、どのアクションが実行されたかによって状態を作る関数dogReducer
も定義しています。この関数は状態を変更することがありません。代わりに新しい状態オブジェクトを返します。
-
storeを作るため
app.jsx
を修正します。ファイルの中身全体を以下の内容に置き換えます:
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import dogReducer from './reducers/dog-reducer';
import BarkMessage from './containers/bark-message';
import BarkButton from './containers/bark-button';
const store = createStore(combineReducers({
dog: dogReducer,
}));
ReactDOM.render(
<Provider store={store}>
<div>
<BarkMessage />
<BarkButton />
</div>
</Provider>
, document.querySelector('.app')
);
見ると明らかなように、Reduxの関数createStore
がstoreを作ります。storeオブジェクトはReduxのcombineReducers
関数を使って、reducerを全て(この例では1つだけですが)集めたものです。各reducerはここで名前がつけられます。ここではdog
という名前をつけています。
ここは純粋なReduxの部分です。
それではreact-redux
を使ってReduxとReactを結びつけましょう。react-redux
がReactアプリにstoreを渡せるようにするため、アプリ全体を<Provider>
コンポーネントで包みます。このコンポーネントは1つだけしか子要素を持てません。そのためここでは<div>
要素を作り、この<div>
要素にアプリの2つの重要な要素であるBarkMessage
とBarkButton
を持たせるようにします。
import
セクションにある通り、BarkMessage
とBarkButton
はcontainers
フォルダからimportしたものです。ここはComponents と Containersという2つのコンセプトを紹介するよい機会です。
Component は 賢くない(dumb) Reactコンポーネントです。これはReduxの状態について何も知らないという意味です。Containers は *賢い(smart)なコンポーネントです。これは状態を知っており、他の賢くないコンポーネントに接続(connect)*できるためです。
-
src/client/components
とsrc/client/containers
の2つのフォルダを作ります。 -
components
の中に、以下のファイルを作ります:
button.jsx
import React, { PropTypes } from 'react';
const Button = ({ action, actionLabel }) => <button onClick={action}>{actionLabel}</button>;
Button.propTypes = {
action: PropTypes.func.isRequired,
actionLabel: PropTypes.string.isRequired,
};
export default Button;
そして、message.jsx:
import React, { PropTypes } from 'react';
const Message = ({ message }) => <div>{message}</div>;
Message.propTypes = {
message: PropTypes.string.isRequired,
};
export default Message;
これらは賢くないコンポーネントの例です。ロジックがなく、Reactのprops経由で問い合わせが来たものをそのまま返すだけのものです。button.jsx
と message.jsx
の主な違いは、Button
はそのプロパティとしてアクション(action) を持っていることです。アクションはonClick
イベントに結び付けられています。アプリのコンテキストで、Button
ラベルは変化することがありませんが、Message
コンポーネントはアプリの状態を反映し、状態によって変化します。
繰り返しますが、 コンポーネント はReduxの アクション やアプリの状態 について何も知りません。そのため、適切なactions と dataを2つの賢くないコンポーネントに伝えるための賢いコンテナを作ることになるのです。
-
containers
内に、以下のファイルを作ります:
bark-button.js
import { connect } from 'react-redux';
import Button from '../components/button';
import { makeBark } from '../actions/dog-actions';
const mapDispatchToProps = dispatch => ({
action: () => { dispatch(makeBark()); },
actionLabel: 'Bark',
});
export default connect(null, mapDispatchToProps)(Button);
そして bark-message.js:
import { connect } from 'react-redux';
import Message from '../components/message';
const mapStateToProps = state => ({
message: state.dog.hasBarked ? 'The dog barked' : 'The dog did not bark',
});
export default connect(mapStateToProps)(Message);
BarkButton
はButton
とmakeBark
アクション、そしてReduxのdispatch
メソッドを結びつけます。また、BarkMessage
はアプリケーションの状態とMessage
を結びつけます。状態が変更されると、Message
は自動的に適切なmessage
属性を再描画します。これらはreact-redux
のconnect
関数によって動作します。
-
yarn start
を実行し、index.html
を開きます。そうすると、"The dog did not bark"とボタンが表示されます。ボタンをクリックすると、"The dog barked"というメッセージが表示されるはずです。
(原文: 9 - Redux)
10 - Immutable JSとReduxの改良
Immutable JS
前章とは違い、本章はとても簡単で、ちょっとした変更を行うだけです。
まず、コードベースにImmutable JSを追加します。Immutableはオブジェクトを変更(mutating)することなしに扱うためのライブラリです。例えば以下のようにする代わりに:
const obj = { a: 1 };
obj.a = 2; // Mutates `obj`
こうすることができます:
const obj = Immutable.Map({ a: 1 });
obj.set('a', 2); // Returns a new object without mutating `obj`
このアプローチは関数型プログラミングのパラダイムに則ったもので、Reduxとの相性がとても良くなっています。実際、reducer関数は引数として渡される状態を変更しない、純粋な関数でなければならず、状態オブジェクトを新しく作って返すようになっています。それではImmutableを使い、このやり方を強制してみましょう。
-
yarn add immutable
を実行します。
このコードベースではMap
を使うのですが、ESLintとAirbnbの設定はクラス以外に大文字の名前を使うと警告を出します。package.json
のeslintConfig
に以下を追加します:
"rules": {
"new-cap": [
2,
{
"capIsNewExceptions": [
"Map",
"List"
]
}
]
}
これはMap
とList
(この2つのImmutableなオブジェクトはずっと使うことになります)を例外扱いするようESLintルールを変更するものです。この冗長なJSONフォーマットはYarn/NPMによって自動的に行われるもので、残念ながらコンパクトにはできません。
それはさておき、Immutableに戻りましょう:
dog-reducer.js
を以下のように修正します:
import Immutable from 'immutable';
import { MAKE_BARK } from '../actions/dog-actions';
const initialState = Immutable.Map({
hasBarked: false,
});
const dogReducer = (state = initialState, action) => {
switch (action.type) {
case MAKE_BARK:
return state.set('hasBarked', action.payload);
default:
return state;
}
};
export default dogReducer;
初期状態はImmutableのMapを使って作られます。そして新しい状態は、それ以前の状態を変更することのないset()
を使って作られるようになります。
containers/bark-message.js
のmapStateToProps
関数を、.hasBarked
の代わりに.get('hasBarked')
を使うよう修正します:
const mapStateToProps = state => ({
message: state.dog.get('hasBarked') ? 'The dog barked' : 'The dog did not bark',
});
アプリケーションは以前と同様の振る舞いをするはずです。
注意: BabelがImmutableについて100KB制限を超えていると警告する場合, package.json
のbabel
のところに"compact": false
を追加します。
上記のコード片を見ると分かる通り、状態オブジェクトは素のオブジェクト属性dog
を持っており、イミュータブルではありません。これはこれで構わないのですが、イミュータブルオブジェクトしか扱いたくない場合、ReduxのcombineReducers
関数を置き換えるため、redux-immutable
パッケージをインストールできます。
オプション:
-
yarn add redux-immutable
を実行します。 -
app.jsx
にあるcombineReducers
関数をredux-immutable
からimportしたものに置き換えます。 -
bark-message.js
のstate.dog.get('hasBarked')
をstate.getIn(['dog', 'hasBarked'])
に置き換えます。
Reduxアクション
アプリにアクションを加えていくにつれて、同じボイラープレートを何度も書き加えていくことになります。redux-actions
パッケージを使えば、ボイラープレートのコードを減らしてくれます。dog-actions.js
ファイルをredux-actions
で簡潔に書き換えることができます。
import { createAction } from 'redux-actions';
export const MAKE_BARK = 'MAKE_BARK';
export const makeBark = createAction(MAKE_BARK, () => true);
redux-actions
は先ほど実装したようなアクションであるFlux Standard Actionモデルを実装したもので、このモデルに従っていればredux-actions
はシームレスに導入できます。
-
yarn add redux-actions
を忘れずに実行します。
(原文: 10 - Immutable JS and Redux Improvements)
11 - Mocha、Chai、Sinonによるテスティング
MochaとChai
-
src/test
フォルダを作ります。このフォルダはアプリケーションのフォルダ構造を反映していて、src/test/client
フォルダも同様に作ります(server
フォルダとshared
フォルダも必要なら作っても構わないのですが、ここではテストを書きません)。 -
src/test/client
フォルダ内にstate-test.js
ファイルを作ります。これはReduxアプリケーションのライフサイクルをテストするのに使われます。
メインのテスティングフレームワークには、Mochaを使うことにします。Mochaは使いやすく、機能が豊富で、今のところ最もよく使われるJavaScriptのテスティングフレームワークです。また、フレキシブルでモジュラー化されています。特に、好きなアサーションライブラリを使うことができます。Chaiは、たくさんのpluginを持つ素晴らしいアサーションライブラリで、異なるアサーションスタイルを選ぶことができます。
-
yarn add --dev mocha chai
を実行して、MochaとChaiをインストールします。
state-test.js
を以下のように書きます:
/* eslint-disable import/no-extraneous-dependencies, no-unused-expressions */
import { createStore } from 'redux';
import { combineReducers } from 'redux-immutable';
import { should } from 'chai';
import { describe, it, beforeEach } from 'mocha';
import dogReducer from '../../client/reducers/dog-reducer';
import { makeBark } from '../../client/actions/dog-actions';
should();
let store;
describe('App State', () => {
describe('Dog', () => {
beforeEach(() => {
store = createStore(combineReducers({
dog: dogReducer,
}));
});
describe('makeBark', () => {
it('should make hasBarked go from false to true', () => {
store.getState().getIn(['dog', 'hasBarked']).should.be.false;
store.dispatch(makeBark());
store.getState().getIn(['dog', 'hasBarked']).should.be.true;
});
});
});
});
さて、この全体を解析してみましょう。
まず、chai
のshould
アサーションスタイルをどのようにimportしているのかを注意してみてください。これを使うとmynumber.should.equal(3)
といった、ちょっと巧妙な構文を使ってアサートできるようになります。どんなオブジェクトに対してもshould
を呼び出せるように、何よりも前にshould()
を実行しなければなりません。これらのアサーションには、 mybook.should.be.true
のように*式(expressions)*であるものがあり、ESLintはこのような書き方に警告を出します。そのため、ファイルの先頭にno-unused-expressions
ルールを無効にするESLintのコメントを追加しています。
Mochaのテストは木構造のような階層構造で動作します。ここでは、アプリケーションの状態のdog
属性に影響するmakeBark
関数をテストしたいので、テストの階層はdescribe()
で表現した通り、App State > Dog > makeBark
という形になります。it()
が実際のテスト関数で、beforeEach()
はit()
の各テストの前に実行される関数になります。この例では、各テストが走る前に新しい状態が必要です。そこで、ファイルの先頭で変数store
を宣言し、このファイル内での全てのテストで使えるようにしています。
makeBark
テストは明示的に書かれていますが、it()
内に文字列で与えられている説明によってさらにわかりやすくなっています: ここではmakeBark
の呼び出しによってhasBarked
がfalse
からtrue
に変わるのをテストしています。
さあ、テストを実行してみましょう!
-
gulp-mocha
プラグインを使って、gulpfile.babel.js
に以下のtest
タスクを作ります:
import mocha from 'gulp-mocha';
const paths = {
// [...]
allLibTests: 'lib/test/**/*.js',
};
// [...]
gulp.task('test', ['build'], () =>
gulp.src(paths.allLibTests)
.pipe(mocha())
);
- もちろん、
yarn add --dev gulp-mocha
で実行します。
このように、テストはlib
にトランスパイルされたコードを実行します。これがtest
がbuild
の前提条件になっている理由です。build
もlint
という前提条件を持っており、そして最後に、test
がmain
の前提条件になります。これにより、default
は次のようなタスクの連鎖ができます: lint
> build
> test
> main
。
-
main
の前提条件をtest
に変えます:
gulp.task('main', ['test'], () => /* ... */ );
-
package.json
の"test"
スクリプトを"test": "gulp test"
に変更します。こうするとyarn test
でテストを実行できるようになります。test
は例えばCIサービスのようなツールで自動的に呼ばれる標準のスクリプトでもあるため、必ずテストタスクはここに書くべきです。yarn start
はWebpackのクライアントバンドルを作る前にテストを実行するため、全てのテストにパスすればビルドするだけになります。 -
yarn test
またはyarn start
を実行すると、テスト結果が出力されます。おそらくグリーンになっているはずです。
Sinon
ユニットテストでfakeを使いたくなることがあります。たとえば、deleteDatabases()
という関数の呼び出しを含むdeleteEverything
という関数があったとします。deleteDatabases()
の実行には様々な副作用があるため、テストスイートを走らせる際には実行したくありません。
Sinonは スタブ (とその他もろもろ)を提供するテスティングライブラリで、deleteDatabases
を無効化し実際に呼び出すことなく監視のみ行うようになります。これにより、呼ばれたかどうか、どのような引数で呼ばれたかどうかといったことをテストできるようになります。これはAJAXの呼び出しをフェイクしたり避けたりする場合によく使われます - バックエンド上の副作用を起こしうるような場合です。
ここでは、アプリに対して、src/shared/dog.js
にあるDog
クラスのメソッドbarkInConsole
を追加してみます。
class Dog {
constructor(name) {
this.name = name;
}
bark() {
return `Wah wah, I am ${this.name}`;
}
barkInConsole() {
/* eslint-disable no-console */
console.log(this.bark());
/* eslint-enable no-console */
}
}
export default Dog;
ユニットテストでbarkInConsole
を実行すると、 console.log()
はターミナルへの出力を行います。このようなことは、ユニットテストの文脈における望ましくない副作用であると考えられます。興味があるのはconsole.log()
が 正常に呼び出されるかどうか であり、テストしたいのはどのようなパラメーターが呼び出されるのかといったことです。
- 新しいファイル
src/test/shared/dog-test.js
を作り、次のように書き加えます:
/* eslint-disable import/no-extraneous-dependencies, no-console */
import chai from 'chai';
import { stub } from 'sinon';
import sinonChai from 'sinon-chai';
import { describe, it } from 'mocha';
import Dog from '../../shared/dog';
chai.should();
chai.use(sinonChai);
describe('Shared', () => {
describe('Dog', () => {
describe('barkInConsole', () => {
it('should print a bark string with its name', () => {
stub(console, 'log');
new Dog('Test Toby').barkInConsole();
console.log.should.have.been.calledWith('Wah wah, I am Test Toby');
console.log.restore();
});
});
});
});
ここでは、Sinonのstubsと、その上でChaiのアサーションを使うためのChaiプラグインをimportしています。
-
yarn add --dev sinon sinon-chai
を実行してライブラリをインストールします。
ここでの新しいことは何でしょうか? 何よりもまず、Chaiプラグインを使うためにchai.use(sinonChai)
を呼び出しています。そして、it()
文で全ての魔法が発動しています: stub(console, 'log')
がconsole.log
を無効化し監視します。new Dog('Test Toby').barkInConsole()
が実行されると、新たなconsole.log
が用意されます。そしてconsole.log
をconsole.log.should.have.been.calledWith()
で呼び出してテストし、最後にrestore
でconsole.log
を元に戻しています。
重要な注意: console.log
をスタブするのはおすすめできません。もしテストが失敗すると、console.log.restore()
が呼び出されず、そのためconsole.log
はターミナルで残りのコマンドをテストしている間中ずっと壊れてしまったままだからです! テストが失敗した時のエラーメッセージも出力できないため、何が起こっているかの情報をほとんど残すことができなくなります。これは大変厄介です。この例は単純なアプリでスタブを説明するためだけのものです。
もしこの章の内容がうまくいっていれば、パスするテストが2つ得られるはずです。
(原文: 11 - Testing with Mocha, Chai, and Sinon)
12 - Flow
Flowは静的型チェッカーです。コード内の一貫性のない型を検出し、アノテーションを使って明示的な型宣言を追加することができます。
-
トランスパイルの過程でBabelにFlowアノテーションを理解し削除させるため、
yarn add --dev babel-preset-flow
を実行してBabelのFlowプリセットをインストールします。そして、package.json
のbabel.presets
に"flow"
を追加します。 -
プロジェクトのルートに空の
.flowconfig
ファイルを作ります。 -
yarn add --dev gulp-flowtype
を実行してFlowのGulpプラグインをインストールし、lint
タスクにflow()
を追加します:
import flow from 'gulp-flowtype';
// [...]
gulp.task('lint', () =>
gulp.src([
paths.allSrcJs,
paths.gulpFile,
paths.webpackFile,
])
.pipe(eslint())
.pipe(eslint.format())
.pipe(eslint.failAfterError())
.pipe(flow({ abort: true })) // Add Flow here
);
abort
オプションはFlowが問題を検出した場合にGulpタスクを中断させるものです。
これでFlowが実行できるようになりました。
-
src/shared/dog.js
にFlowアノテーションを以下のように追加します:
// @flow
class Dog {
name: string;
constructor(name: string) {
this.name = name;
}
bark(): string {
return `Wah wah, I am ${this.name}`;
}
barkInConsole() {
/* eslint-disable no-console */
console.log(this.bark());
/* eslint-enable no-console */
}
}
export default Dog;
// @flow
コメントはFlowにこのファイルの型検査をしてほしいことを伝えるものです。それ以外の部分について、Flowアノテーションは、たいてい関数の引数か関数名のあとにコロンがついています。詳細はドキュメントを見てください。
この状態でyarn start
を実行すると、Flowは問題なく動きますが、ESLintはここで使われている標準的ではない文法について注意してくれることでしょう。Babelのパーサは先ほどインストールしたbabel-preset-flow
プラグインのおかげでFlowコンテンツをパースできるようになるため、ESLintがFlowアノテーションを独自に解釈しようとするのではなく、Babelのパーサを使うようになってくれると理想的です。babel-eslint
パッケージを使えばこれが実現できます。やってみましょう。
-
yarn add --dev babel-eslint
を実行します。 -
package.json
のeslintConfig
に次のプロパティを追加します:"parser": "babel-eslint"
yarn start
を実行するとlintと型検査が正しく行われるようになったはずです。
これでESLintとBabelで共通のパーサを実行できるようになったので、eslint-plugin-flowtype
を使ってESLintにFlowのアノテーションをlintさせられるようになりました。
-
yarn add --dev eslint-plugin-flowtype
を実行し、package.json
のeslintConfig.plugins
に"flowtype"
を追加します。また、eslintConfig.extends
の配列の"airbnb"
の次に"plugin:flowtype/recommended"
を追加します。
これで例えばname:string
というアノテーションを書くと、ESLintはコロンの後ろにスペースを入れるのを忘れていると注意してくれるようになります。
注意: package.json
に書いた"parser": "babel-eslint"
プロパティは実際には"plugin:flowtype/recommended"
に含まれるものなのでpackage.json
から削除することができますが、好みで明示的に残しておいても構いません。このチュートリアルは最小限のセットアップを行うものなので、削除しました(訳注:削除してない??)。
-
src
以下のすべての.js
と.jsx
ファイルに対して、// @flow
を追加し、yarn test
またはyarn start
を実行します。そしてFlowが提案した部分全部に対して、型アノテーションを追加します。
直感的ではない例としては、src/client/components/message.jsx
にある以下のものがあります:
const Message = ({ message }: { message: string }) => <div>{message}</div>;
このように、分割代入している場合、オブジェクトリテラル記法のようなものを使って展開されたプロパティにアノテーションをつけます。
他に見かける例としては、src/client/reducers/dog-reducer.js
にあるように、FlowはImmutableがデフォルトのexportをつけないと注意してくるというものがあります。この問題はImmutableの#863で議論されていますが、2つの回避策があります:
import { Map as ImmutableMap } from 'immutable';
// or
import * as Immutable from 'immutable';
Immutableがこの件を公式に解決するまで、Immutableコンポーネントをimportする時はどちらか好きな方を使いましょう。個人的にはimport * as Immutable from 'immutable'
の方が、短い上に修正された際のリファクタリングも不要なので気に入っています。
注意: Flowが型エラーをnode_modules
フォルダで見つけた場合、問題のあるパッケージを無視するため[ignore]
セクションを.flowconfig
に追加します(node_modules
ディレクトリ全体を無視しないようにします)。以下のようになります:
[ignore]
.*/node_modules/gulp-flowtype/.*
この場合、node_modules/gulp-flowtype
ディレクトリにあるAtomのlinter-flow
プラグインに型エラーが検出されました。これは// @flow
がついたファイルを同梱していました。
これでlintされて、型検査もテストも通った万全のコードができました!
(原文: 12 - Flow)