LoginSignup
1893
2018

More than 5 years have passed since last update.

ゼロから始めるJavaScript生活

Last updated at Posted at 2016-11-01

(訳者注: これは、JavaScript Stack from Scratchを翻訳し、まとめて読めるように1ファイルにしたものです。元の翻訳と各種ファイルについては、日本語訳forkリポジトリを参照してください。また、原文が活発に更新されているため、訳文も追従して更新されます。ご了承ください。)

ゼロから始めるJavaScript生活

YarnReactGulpReduxESLintWebpackMochaChaiFlow

モダン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 @verekiaverekia.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 addyarn add --devなどのコマンドを、npm install --savenpm install --save-devなどと読み替えてください。

  • インストール手順に従ってYarnをインストールします。npm install -g yarnsudo 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がdependenciescolorを自動的に追加したのがわかります。

そして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.jsonbabelよりもそちらが優先されます。ルートフォルダはそのうちどんどん膨れ上がっていくため、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つのタスク: buildcleanmainwatch、そしてdefaultを定義します。

  • buildsrc以下のソースファイルを変換しlibに保存するため、Babelが呼ばれるところです。
  • cleanbuildを行う前に、libフォルダに自動生成されるファイル全てを削除するタスクです。このタスクは、src内のファイルをリネームや削除したり、ビルドに失敗したことに気づかない場合でもlibフォルダをsrcフォルダに確実に同期させるため、生成されたファイルを削除するのに便利です。Gulpストリームと連携してファイルを削除するために、ここではdelパッケージを使っています(これはGulpでのファイルの削除方法として推奨されている方法です)。
  • mainは前章でnode .と入力し実行させているのとほぼ同じもので、lib/index.jsを実行することだけが異なっています。index.jsはNodeが探しにいく標準のファイル名なので、単にnode libとだけ書いています(DRYを守るためにlibDir変数を使っています)。タスク内のrequire('child_process').execexecの部分はシェルコマンドを実行するNodeの標準関数です。stdoutconsole.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コードはクラスやconstletbark()で使われているような(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 = Dogexport 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.jsgulpfile.babel.jsにリネームすれば、BabelはimportされたモジュールをGulpに渡す役割を担ってくれます。

  • gulpfile.jsgulpfile.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フォルダには、次のサブフォルダを作ります: serversharedclient。そしてカレントにあるindex.jsserverフォルダに、dog.jssharedフォルダに移動します。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.jsoneslintConfigに以下を追加します:

"env": {
  "browser": true
}

こうすると、windowdocumentといったブラウザでは必ずアクセスできる変数を使っていても、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.jsfoo.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にトランスパイルします。ここではコードをserversharedclientに分割しています。これはserversharedのみこのタスクでコンパイルするためのものです(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.jssrc/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を可能な限りシンプルに扱うにはどうすればよいか説明するため、アプリはメッセージとボタンで構成されるようにします。メッセージは犬が鳴いているかどうか(最初は鳴いていません)を伝え、ボタンを押すと犬が鳴き、メッセージが更新されるようにします。

ここでは、reduxreact-reduxの2つのパッケージが必要になります。

  • yarn add redux react-reduxを実行します。

はじめに、2つのフォルダ、src/client/actionssrc/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つの重要な要素であるBarkMessageBarkButtonを持たせるようにします。

importセクションにある通り、BarkMessageBarkButtoncontainersフォルダからimportしたものです。ここはComponentsContainersという2つのコンセプトを紹介するよい機会です。

Component賢くない(dumb) Reactコンポーネントです。これはReduxの状態について何も知らないという意味です。Containers賢い(smart)なコンポーネントです。これは状態を知っており、他の賢くないコンポーネントに接続(connect)できるためです。

  • src/client/componentssrc/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.jsxmessage.jsx の主な違いは、Buttonはそのプロパティとしてアクション(action) を持っていることです。アクションはonClickイベントに結び付けられています。アプリのコンテキストで、Buttonラベルは変化することがありませんが、Messageコンポーネントはアプリの状態を反映し、状態によって変化します。

繰り返しますが、 コンポーネント はReduxの アクション やアプリの状態 について何も知りません。そのため、適切なactionsdataを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);

BarkButtonButtonmakeBarkアクション、そしてReduxのdispatchメソッドを結びつけます。また、BarkMessageはアプリケーションの状態とMessageを結びつけます。状態が変更されると、Messageは自動的に適切なmessage属性を再描画します。これらはreact-reduxconnect関数によって動作します。

  • 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.jsoneslintConfigに以下を追加します:

"rules": {
  "new-cap": [
    2,
    {
      "capIsNewExceptions": [
        "Map",
        "List"
      ]
    }
  ]
}

これはMapList(この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.jsmapStateToProps関数を、.hasBarkedの代わりに.get('hasBarked')を使うよう修正します:

const mapStateToProps = state => ({
  message: state.dog.get('hasBarked') ? 'The dog barked' : 'The dog did not bark',
});

アプリケーションは以前と同様の振る舞いをするはずです。

注意: BabelがImmutableについて100KB制限を超えていると警告する場合, package.jsonbabelのところに"compact": falseを追加します。

上記のコード片を見ると分かる通り、状態オブジェクトは素のオブジェクト属性dogを持っており、イミュータブルではありません。これはこれで構わないのですが、イミュータブルオブジェクトしか扱いたくない場合、ReduxのcombineReducers関数を置き換えるため、redux-immutableパッケージをインストールできます。

オプション:

  • yarn add redux-immutableを実行します。
  • app.jsxにあるcombineReducers関数をredux-immutableからimportしたものに置き換えます。
  • bark-message.jsstate.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;
      });
    });
  });
});

さて、この全体を解析してみましょう。

まず、chaishouldアサーションスタイルをどのように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の呼び出しによってhasBarkedfalseから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にトランスパイルされたコードを実行します。これがtestbuildの前提条件になっている理由です。buildlintという前提条件を持っており、そして最後に、testmainの前提条件になります。これにより、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.logconsole.log.should.have.been.calledWith()で呼び出してテストし、最後にrestoreconsole.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.jsonbabel.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.jsoneslintConfigに次のプロパティを追加します: "parser": "babel-eslint"

yarn startを実行するとlintと型検査が正しく行われるようになったはずです。

これでESLintとBabelで共通のパーサを実行できるようになったので、eslint-plugin-flowtypeを使ってESLintにFlowのアノテーションをlintさせられるようになりました。

  • yarn add --dev eslint-plugin-flowtypeを実行し、package.jsoneslintConfig.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)

1893
2018
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1893
2018