しばらく見ない間にES6開発ツールセットが良くなってた

WEBのフロントエンドをES6でreact + redux + react-router + Material-UI利用して書いてますが、このビルド環境をそれまでのGulp + Browserifyからnpm + webpackに置き換えました。しばらく見ない間にツール毎の進化があって組み合わせがシンプル化できるようになってきています。WEBフロントの開発ツールエコシステムについてES6キャズムを超えたんじゃないですかね?ライブラリやツール自体もES6で書かれているもの多いですし、雑多に広がる感じから安定成長になってきた印象を受けます。

package.json のscriptsとdevDependenciesの抜粋については以下の通り。eslintはairbnbの最新バージョンで動く組み合わせを探ったら3.xバージョンになりました。4.xだとルールに競合が出てましたが、そのうち成熟するでしょう。それ以外は今日時点での最新バージョンでそろってます。npmは5.3.0、nodeは8.4.0にバージョンアップしました。

  "scripts": {
    "build": "rimraf ./static/js/*; webpack",
    "test": "jest"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-eslint": "^7.2.3",
    "babel-jest": "^20.0.3",
    "babel-loader": "^7.1.1",
    "babel-plugin-transform-decorators-legacy": "^1.3.4",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-1": "^6.24.1",
    "eslint": "^3.19.0",
    "eslint-config-airbnb": "^15.1.0",
    "eslint-loader": "^1.9.0",
    "eslint-plugin-import": "^2.7.0",
    "eslint-plugin-jsx-a11y": "^5.1.1",
    "eslint-plugin-react": "^7.2.1",
    "jest": "^20.0.4",
    "rimraf": "^2.6.1",
    "webpack": "^3.5.5"
  },

npmの実行スクリプトで、buildとtestを行いますがそれぞれwebpackとjestを呼ぶだけ。Webpackでバンドル、eslintでAirbnbルールのチェック、Jestでテストをします。それぞれトランスパイルが必要なのでBabelを設定しています。

Babel

設定をなるべく一箇所にまとめたいのでBabelの設定もpackage.jsonに。package.jsonの該当箇所を抜粋。

  "babel": {
    "presets": [
      "es2015",
      "react",
      "stage-1"
    ],
    "plugins": [
      "transform-decorators-legacy"
    ]
  },

プリセットのes2015とreactは普通。stage-1は展開演算子「…」を、transform-decorators-legacyはJS-classに前置する@デコレータ(Javaアノテーションみたいなやつで、redux-formの利用で出てくる)を解釈するために入れています。package.jsonにきちんと書いておけばその他のツールにBabelの設定は必要なく、設定を使いまわしてくれます。

eslint

eslintの設定抜粋は以下のとおり。package.jsonより。

  "eslintConfig": {
    "extends": "airbnb",
    "env": {
      "browser": true,
      "jest": true
    },
    "parser": "babel-eslint",
    "rules": {
      "indent": ["warn", 4, {"SwitchCase": 1}],
      "max-len": ["warn", 120],
      "react/jsx-indent": ["warn", 4],
      "react/jsx-indent-props": ["warn", 4]
    }
  },

AirbnbのルールカスタマイズはWebStormのエディタの初期値にあわせてのインデント幅調整だけです。前はいろいろいじらないと無理があったのですが、最近は私の手が慣れたのか、Airbnbルールが現実的になってきたのか、ストレスなくコード書けるようになりました。envエントリーで、jestをtrueにしてるのは、グローバルスコープのJestが提供するメソッドをインポートなく用いた際にLintで引っ掛けないために必要です。サーバサイドはGoで書いているのでnodeはfalseでOK。

Jest

facebook.github.io

Jasmineを用いてましたが、なんかWebStormがJestにがっちり対応していたので乗り換え。2年ほど前はそもそもJestを動くところまで設定するのに試行錯誤があったりして未成熟感丸出しだったところが、いまはこなれて安定感満点。同期テストはもちろん問題なく、非同期テストはAxiosでGo製APIサーバとの通信コードぐらいしか私のプロジェクトではやるところないのだけどJasmineですでに不便なかったのと同様にまったく問題ない。

Jestの設定もpackage.jsonに行います。

  "jest": {
    "transform": {
      "\\.(js|jsx)$": "babel-jest"
    }
  },

なんか新旧いろんなドキュメントにいろんなやり方が書いてあるのですが、今の20.0バージョンではこれが最もシンプルなんじゃないかな。AirbnbでJSXは.jsxに書かないと怒られるのでトランスパイル対象を.jsおよび.jsxに正規表現でマッチさせます。ちょっと違和感あるのは正規表現エスケープであるバックスラッシュをエスケープするためにもういっちょバックスラッシュを重ねてるところ。JSプロパティ名にコンパイル前の正規表現文字列を書くなんて、こんなパターン初めて見た。値には橋渡しするbabel-jestを記述します。さて環境の動作確認として動作するコードのテスト形式な実行をしてみましょう。

import { Map } from 'immutable';

describe('An usage of Immutable.js', () => {
    test('Set and Merge values', () => {
        const state1 = Map({ one: 0, two: 0 });
        const state2 = state1.set('one', 1).merge({ two: 2, three: 3 });
        expect(state1.get('one')).toBe(0);
        expect(state1.get('two')).toBe(0);
        expect(state2.get('one')).toBe(1);
        expect(state2.get('two')).toBe(2);
        expect(state2.toJS()).toEqual({ one: 1, two: 2, three: 3 });
    });
});

test('Toggle bool value', () => {
    const state1 = Map({ value: false });
    const state2 = state1.update('value', value => !value);
    expect(state1.get('value')).toBeFalsy();
    expect(state2.get('value')).toBeTruthy();
});

importがきっちり効くからプロジェクト内のソースをどこからでも引っ張ってきて、かつ、ES6でテスト書けるし。Jasmine以前より様々なテストツールがそうだったからdescribeでまとめてるけど、まとめないでいきなりtest関数書いてもOK。デフォルトで、テストファイルは「__tests__」という名前のフォルダに置くか、/\.(test|spec)\.jsx?$/なファイル名をきっかけとして、検索し全部実行してくれます。

f:id:masataka_k:20170816071340p:plain

WebStormでUI対応しているし。しかしデバッガーで止まらない。なんかまだ足りないのかな?単体テストをデバッガーで止められると細かくややこしいところを調査・修正するのに助かるのになあ。

webpack

webpackの設定は、package.jsonにいっしょに書けない。ほとんどJSON返すだけなので書けてもいいとおもうんだけどな。

/* webpack.config.babel.js */
import path from 'path';

export default {
    entry: './js/App.jsx',
    output: {
        path: path.resolve('static/js'),
        filename: 'bundle.js',
    },
    devtool: 'source-map',
    module: {
        rules: [{
            test: /\.(js|jsx)$/,
            exclude: /node_modules/,
            use: ['babel-loader', 'eslint-loader'],
        }],
    },
    resolve: {
        extensions: ['.js', '.jsx'],
    },
};

特に難しいことはしてません。唯一のハマりポイントはresolveエントリを追加して、わざわざ.jsと.jsx拡張子に反応するよう書かないといけないこと。webpack3からの変更?2以前の解説ドキュメントでは必要とされていない。babelとeslintの設定はpackage.jsonをちゃんと参照してくれています。

direnv

プロジェクトルートにある、.envrcの内容は以下の通り。

export PWD="`pwd`"
export GOPATH=$PWD/vendor:$PWD
export PATH=$PWD/node_modules/.bin:$PWD/bin:$PATH

direnvをインストールしてあるので、コンソールを開いてすぐにJSツールのパスを通すことが楽チン。これでWebStormのTerminalからいきなり、jestとかwebpackとか打ち込んでもツールが起動する。npm install -gでグローバルに環境構築をしなくても、これで同じ効果が得られる。同居するAPIサーバコードのためにGOPATHも設定しています。

まだやってないこと

  • webpack-dev-serverを走らせてホットデプロイな環境にしていないので、ブラウザのキャッシュが効きまくってコードの変更が反映されない。
  • Immutable.jsをテストコードでつかってるのは、このあとReduxのreducerをImmutable.jsを用いて「…」展開演算子をなくすように書き直そうと思ってるため。まだやってない。
  • Jestを用いる肝心要はReactの描画について単体テストすること。それはまだやってない。
  • アプリ書いてるうちにcssが必要無くなったので、webpackでsassコンパイル&埋め込みをまだしていない。グリッドレイアウトを行うのにBootstrapのsassライブラリを持ってくる段になったらやる。

沢山やってないことがあるけど、前のGulp + Browserifyの環境よりも格段に構成や設定がシンプルになりました。