ミルク色の記録

やったこと、やってみたこと

Y8 2017 spring in Shibuyaに参加してきた

y8-2017-spring.hachiojipm.org

去年に引き続き、今年も参加してきた。

f:id:ushiboy:20170528191115j:plain:w480

そして今年は、2nd season(懇親会)の方で猫型さん(@neko_gata_s さん)と「春だ!webフロントエンド開発を語り尽くすぞ!!」というタイトルで漫談してきた。

トークを聴いて

苦労話とか、知らなかった話とか、ためになる話があってなるほどなーという感じで良かった。

  • hyperapp – 1kbのビューライブラリ
    • ソース読んで見たらすごいコンパクトでびっくりした
  • ScalaでウェブAPIを書いている人が設計や実装やその他について話そうか
    • 猫型さんがScala.jsでドメイン全部書きたいって言ってた理由がなんとなくわかった
  • V8 for フロントエンドデベロッパ
    • JavaScriptを書くときにV8の気持ちを考えるのに参考になった

辺りが特に印象に残った感じ。

漫談したこと

勉強会とかで発表した経験が浅い自分にとって、漫談とはいえ大勢の前でやるのは難易度高すぎて、 最初に猫型さんに誘われた時は、正直「いやいやいやいや…」って感じだったけど、 やっぱり何かしら挑戦しなければと常々思っていたので、良い経験になった。

緊張しまくってたけど、夜の部で懇親会中ということもあり、始まってしまえばそれなりに気楽になった。 だんだんフリーダムになって事前に打ち合わせしていた内容の半分くらい喋らなかったのはご愛嬌。

漫談中のアドリブもそうだけど、漫談後に色々な方にせっかく話しかけて頂いたのにイマイチうまく話すことができなくて、 「なんでもっとうまく話せなかったんだろう…」と一夜明けてからずーっと悶絶しているけど、 「そういうのを次に活かしてサイクル回すと良いよ」というアドバイスを頂いたので、頑張りたい。頑張ろう。

猫型さんは「ニュービーなので…」などと言いながらフロントエンドのツールやライブラリをちゃんと使いこなしつつ、 複雑なSingle Page Applicationの設計をものすごく研究している人なので、 「純粋にWebブラウザでどこまでのアプリケーションが作れるのか」が大好きな自分にとって、 すごく勉強になるし、漫談の打ち合わせで話しているだけでも本当に楽しかった。

自分の関心のある話を誰かと濃く話せるというのはすごい楽しいんだなーと思った。 なので、もっとそういう話をできる人とお近づきになりたい(とういか師匠が欲しい)。

まとめ

主催の@uzullaさん、運営スタッフ、発表者、参加者の皆様ありがとうございました。 今年も楽しい体験をさせて頂きました。

一緒に漫談やってくれた猫型さん、見守ってくれた@hayajoさんありがとうございました。 数年前のあの日、hayajoさんに誘っていただいて行った居酒屋での出会いは自分に確かな影響を与えています。

【NDSiN#12×Niigata.js×C4N】JavaScript 2017春に参加した

nds-meetup.connpass.com

久しぶりのNiigata.js絡みのイベントに参加して、「SPAのルーティング」の話をしてきた。

www.slideshare.net

SPAのルーティングに関してはあまり話に出てるの見たことがなくて(この前のWEB+DB PRESS 97号で初めて見たくらい)、みんな当たり前のようにやってる事なのかなーと思っていたけど、自分がSPAなアプリケーションとか、SPA用のルーティングライブラリ作った時に結構やらかしたり学んだりしたことがあったので題材にしてみた。

ホントは更にサーバサイドレンダリングと絡めて、ページロード時のサーバサイドのルーティングや初期化データ取得とのViewの生成、ページロード直後のフロントエンドの動きあたりのとこまで踏み込めると、また深みがあるのでそこまで行きたかったんだけど、まだ自分も模索中みたいなところが多すぎて無理だった(その辺りの良いやり方知りたい…)。

他の発表者さんの発表を聴いてて、みんな落ち着いて話してるなーという感じで、自分もっと場数踏まないとだなーと思った。

2016年にフロントエンド開発に使った環境

今年も終わるのでメモしておく。

去年との大きな違いはwebpackの設定を個別ファイルにするのをやめたくらい。 webpackの設定はgulpfile.js内で関数用意して、引数で対象の環境(development, production, testing)を切り替えるようにした。

こんな感じで使う。

他にはbabelのバージョンが上がったことに伴うあたりが変わったくらいで、そんなに大きな変化はなかったかな…と。

以下、package.jsonとか。

2016 development tool

SVGのtext要素でUnicodeのHTML文字参照を使う

意外なやり方だったのでメモしておく。

方法

次のようなsvg#screen1にHTML文字参照を含むコンテンツを持つtext要素を追加したい場合、

text要素をcreateElementNSで生成したらinnerHTMLでコンテンツをセットする。

これで"test@foo"が表示される。

見つけた経緯

レイアウトの調整のために、直接タグを書いていたときには問題なく表示できていた。

<svg width="400" height="20">
  <text x="0" y="16" fill="black">test&#x0040;foo</text>
</svg>

動的に描画するためにtextContentやcreateTextNodeを使ったら、"&"が"&amp;"になるので、"test&#x0040;foo"と表示されてしまった。

直接タグを書いていた時にはできていたので何かあるはずと思って、text要素のインスタンスのプロパティを直接見たらinnerHTMLがあった(SVGの要素なのに)。

気になること

W3Cとかでこのあたりの資料探してみたけどいまいち見つからない...

React.jsの場合

上記のことを踏まえて、dangerouslySetInnerHTMLを使えば実現できる。

確認したブラウザとバージョン

次のブラウザで表示できるのを確認した。

ブラウザ バージョン
Firefox 47.0
Google Chrome 52.0.2743.82
Safari 9.1.2

ソースコード

こちら: SVG Text · GitHub

YAP(achimon)C::Asia Hachioji 2016 mid in Shinagawaに行ってきた

すっかり時間が開いてしまったけど、7月の2日、3日にYAP(achimon)C::Asia Hachioji 2016 mid in Shinagawaへ行ってきたので残しておく。

感想

ベストトーク賞の上位に上がっていたトークはすべて会場で聴いていてどれもこれも良かったのだけど、特に@kamadangoさんの「2人で楽しくサービスやアプリを作る話」がすごく良かった。

そもそもトークの応募リストに上がったのを見た時から、ポッドキャストのdandy.fmでデザイン思考の話聴いてたこともあり、これは絶対聴きに行きたいやつだ...と思ってたので、当日会場で聴いて、デザイン思考のフローに沿ったポイントとか、「ものづくり」する上での大事なこと、ストレスにしないための工夫・気づかいなどなど、予想通り大満足だった。あと、始まる前の突発的な@uzullaさんと@yusukebeさんの平和なエゴの話も良かった。

自分がやりたいのはこういう「ものづくり」なんだよなーと改めて思って、それが自分はできているか?と考えると、遠い目になる...という感じではあったのだけど、個人的に方向性を探るきっかけみたいにはなったかも。

今回、「遠方だから絶対決まってないと死ぬ」枠で2日間申し込んだのだけど、実は申し込む前に「遠方扱いで申し込んでいいものか?」とか思ってちょっとためらった。申し込んで、後日にconnpass見なおしたら「※ 新潟は遠方です。」と追記されてて笑ったし、些細なことかもしれないけれども、嬉しかった。

楽しい2日間だった。

Niigata.js #1に参加して発表した話

(いろいろあって忘れてた)一ヶ月前の話だけど、記録のために書いておく。

3行でふりかえり

4月23日にNiigata.jsの第1回目で「Web Worker使って◯◯する」という話をしてきた。

話した内容は、Web Workerの基礎に見せかけたWeb Worker使ってFluxする話。

去年のYAPCの前夜祭で聴いた「質問があると嬉しいんですよ」と言う話がなんとなく解った。

Niigata.jsについて

新潟でJavaScriptにフォーカスした濃ーーいお話ができる場所が欲しかったので、すごく嬉しい。

ぜひ2回目もおねがいします...

Web WorkerでFlux

FluxとかReduxやっていて、ステートって単純にJSONとかでいいんだなーと思っていたら、以前、Backbone.js使っていた時に「ModelのレイヤーをWorkerの中に入れてプレゼンテーションレイヤーと分離してみたいなー、でもうまくできないなー」と思っていたやつが今度はできそうだと気づいたのがきっかけ。

資料は下。

www.slideshare.net

Universal(Isomorphic)な辺りを考えるとWorkerを直接使わずに、もうひとつ抽象化しないと行けないような気がするけど、どんなもんか。

発表してみて

他の参加者さんから質問とか反応をもらえて、なるほど。と思ったので、また、なにかネタができたら...。

2015年が終わるのでフロントエンド開発に使ってるツールまわりをざっと書き出してみた

題名の通り、ざっと書き出し。

このあたりのことを相談された時に、手元のリポジトリを漁って「こんな感じでやってます」とやってたけど、 とりあえずこのURL渡して参考にしてもらうみたいなことができれば...

ご注意

  • Xubuntu 14.04でやってるのでWindowsOSXは未確認。
  • こじんまりした規模の開発で使っている構成なので、規模によっては向いていないかも。
  • サーバとはJSONとかのAPIでお話するだけの、SPAなフロントエンド用(ルーティングはハッシュチェンジ系向け)。

おしながき

共通用

nodeの管理

とりあえずnodeがないと始まらないのでnodeから。

nodeの管理はnodebrewを使っている。 変なハマり方をしないように、v4.2.x系を使っている。

JavaScriptのLint

eslintを使っている。 Vimプラグインsyntasticを使って、ファイルの保存時にチェックしてる。

インストール

$ npm install -g eslint

次のようにVimに設定して有効にする。

let g:syntastic_javascript_checkers=['eslint']

プロジェクト用

プロジェクトディレクトリの下準備

npm initでpackage.jsonを作り、giboを使ってnode用の.gitignore設定を作っている。

$ mkdir project
$ cd project
$ npm init
$ gibo Node | tee .gitignore

スクランナー

スクランナーはgulpの3.9系を使っている。

gulpコマンドはプロジェクト配下にインストールしたものをnpmのコマンド経由で使う。 後々追加することになるgulpのプラグインはgulp-load-pluginsでまとめてロードできるようにしている。

HTMLやCSSJavaScriptなどのファイルはappディレクトリ配下に置いて行き、 各種ツールを通してビルド済みディレクトリ(dist)に配備されるようにする。

ビルド済みディレクトリはpackage.jsonに設定として書いておき、gulpfile.jsなどから参照して使っている。

ディレクトリ構成は次のような感じ。

.
├── app
├── dist
├── gulpfile.js
├── node_modules
└── package.json

インストール

$ npm install --save-dev gulp gulp-load-plugins del

開発時に使う共通的なgulpのタスクは次の通り。

タスク名 用途
clean ビルド済みディレクトリを削除する
dev 開発環境を起動する
default デフォルト(cleanしてdevを実行する)

gulpfile.js

var path = require('path');
var pkg = require('./package.json');
var distDir = path.join(__dirname, pkg.dist);
var gulp = require('gulp');
var $ = require('gulp-load-plugins')();
var del = require('del');

gulp.task('clean', del.bind(null, [distDir]));

gulp.task('dev', [], () => {

});

gulp.task('default', ['clean'], () => {
  gulp.start('dev');
});

package.jsonにgulpコマンドとビルド済みディレクトリの設定を次のように追加している。

package.json

-- 中略 --
  "dist": "dist",
  "scripts": {
    "gulp": "gulp"
  },
-- 中略 --

ビルド済みディレクトリはgitignore対象。

.gitignore

dist
-- 中略 --

これでgulpはこんな感じに使う。

$ npm run gulp

HTMLと開発用Webサーバ

開発時にブラウザで確認するためのWebサーバはconnectを使っている。

Webサーバはビルド済みディレクトリをドキュメントルートとして動作させる。 gulpのwatchでファイル監視して、変更があったら必要なビルドをしてLiveReloadでブラウザを更新させる。

BrowserSyncをしばらく使っていたけど、 connectで十分な機能しか使ってなかったのでconnectに戻した。

インストール

$ npm install --save-dev connect connect-livereload serve-static gulp-livereload

HTMLファイルをappディレクトリに追加する。

app/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Hello World!</title>
</head>
<body>
  <h1>Hello World!</h1>
</body>
</html>

HTMLと開発用Webサーバのためのgulpタスクは次の通り。

タスク名 用途
html HTMLファイルをビルド済みディレクトリへ配備する
serve 開発用Webサーバを起動する
dev 開発環境を起動し、HTMLファイルを監視する

gulpfile.js

-- 中略 --
var connect = require('connect');
var connectLivereload = require('connect-livereload');
var serveStatic = require('serve-static');
-- 中略 --
gulp.task('html', () => {
  return gulp.src('app/**/*.html')
  .pipe(gulp.dest(distDir))
  .pipe($.livereload());
});

gulp.task('serve', () => {
  $.livereload.listen();
  connect()
  .use(connectLivereload())
  .use(serveStatic(distDir))
  .listen(3100);
});

gulp.task('dev', ['html', 'serve'], () => {
  gulp.watch('app/**/*.html', ['html']);
});
-- 中略 --

これでgulpを起動して、開発用Webサーバを立ち上げてブラウザで確認できる。

$ npm run gulp

CSSや画像ファイルなどのスタイルまわり

CSSBootstrapをベースにlessで書いて、 gulp-lessでビルドしている。

インストール

$ npm install --save bootstrap
$ npm install --save-dev gulp-less

アプリ用にapp.lessファイルを作り、importでbootstrapのlessを読ませて必要なスタイルを書いていく。

Reactみたいなコンポーネント作って行く系のを使ってると、 コンポーネントごとにlessファイル作って管理とかしたほうがいいのかなーと思うけど、 Bootstrapに毛の生えた程度にしかスタイル追加してないのでapp.lessだけで今のところは済んでいる。

アイコン増やしたいみたいなときだけfont awesomeとか足してる。

app/styles/app.less

@import '../../node_modules/bootstrap/less/bootstrap.less';

body {
  background-color: #efffef;
}

HTMLにCSSの読み込みを追加。

app/index.html

-- 中略 --
<head>
  <meta charset="UTF-8">
  <title>Hello World!</title>
  <link rel="stylesheet" href="./styles/app.css" />
</head>
-- 中略 --

スタイル周りのgulpタスクは次の通り。

タスク名 用途
styles:dev 開発環境用スタイルビルド(less:dev、fonts、imagesを実行する)
less:dev lessファイルをビルドする
fonts フォントリソースをビルド済みディレクトリへ配備する
images 画像リソースをビルド済みディレクトリへ配備する
dev 開発環境を起動し、HTMLファイル、lessファイルを監視する

gulpfile.js

-- 中略 --
gulp.task('styles:dev', ['less:dev', 'fonts', 'images']);

gulp.task('less:dev', () => {
  return gulp.src('app/styles/**/*.less')
  .pipe($.less({
    paths: [
      'node_modules/bootstrap/less'
    ]
  }))
  .pipe(gulp.dest(path.join(distDir, 'styles')))
  .pipe($.livereload());
});

gulp.task('fonts', () => {
  return gulp.src([
    'node_modules/bootstrap/fonts/*'
  ])
  .pipe(gulp.dest(path.join(distDir, 'fonts')));
});

gulp.task('images', () => {
  return gulp.src([
    'app/images/**/*'
  ])
  .pipe(gulp.dest(path.join(distDir, 'images')));
});
-- 中略 --
gulp.task('dev', ['html', 'styles:dev', 'serve'], () => {
  gulp.watch('app/**/*.html', ['html']);
  gulp.watch('app/styles/**/*.less', ['less:dev']);
});
-- 中略 --

JavaScriptまわり(モジュールバンドラ)

Babelを使ってES6で書いたコードをES5にトランスパイルしている。 モジュールバンドラにはwebpackを使っている。

webpackは多機能だけど、あくまでJavaScriptのモジュールバンドラとして使っているので、 必要に応じてbrowserifyに差し替えたりしている。

インストール

$ npm install --save-dev webpack babel-loader babel-preset-es2015 gulp-util

Babelの設定は.babelrcに書く。

.babelrc

{
  "presets": ["es2015"]
}

開発環境用のwebpack設定を作る。

エントリポイントのapp.jsを読み込んで、Babelを通してソースマップ付きでビルド済みディレクトリに配備している。

webpack.config.js

var path = require('path');
var pkg = require('./package.json');
var distDir = path.join(__dirname, pkg.dist);

module.exports = {
  entry: {
    app: './app/scripts/app.js'
  },
  output: {
    path: path.join(distDir, 'scripts'),
    filename: '[name].js'
  },
  devtool: 'inline-source-map',
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      }
    ]
  }
};

JavaScript用のgulpタスクは次の通り。

タスク名 用途
bundle:dev JavaScriptの開発用ビルドを行う
dev 開発環境を起動し、HTMLファイル、lessファイル、JavaScriptファイルを監視する

gulpfile.js

-- 中略 --
var webpack = require('webpack');
var bundler = webpack(require('./webpack.config.js'));
-- 中略 --
gulp.task('bundle:dev', cb => {
  bundler.run((err, stats) => {
    if (err) {
      throw new $.util.PluginError('webpack:build', err);
    }
    $.util.log('[webpack:build]', stats.toString({
      colors: true,
      chunkModules: false
    }));
    cb();
    $.livereload.reload();
  });
});
-- 中略 --
gulp.task('dev', ['html', 'styles:dev', 'bundle:dev', 'serve'], () => {
  gulp.watch('app/**/*.html', ['html']);
  gulp.watch('app/styles/**/*.less', ['less:dev']);
  gulp.watch('app/scripts/**/*.js', ['bundle:dev']);
});
-- 中略 --

JavaScriptソースコードはapp/scripts配下に置いて行く。

app/scripts/Hoge.js

export default class Hoge {

  constructor(name) {
    this._name = name;
  }

  greet() {
    return `Hello! ${this._name}`;
  }
}

app.jsをエントリポイントとして、HTMLファイルに読み込ませる。

app/scripts/app.js

import Hoge from './Hoge';

const hoge = new Hoge('test');

console.log(hoge.greet());

app/index.html

-- 中略 --
<body>
  <h1>Hello World!</h1>
  <script src="./scripts/app.js"></script>
</body>
</html>

プロダクション用のビルド

開発用のビルド設定とは別にプロダクション用ビルド設定(ソースマップなし、ミニファイ化)も用意している。 ビルド済みリソースはzipで固めて、直接フロントエンドの開発をしないメンバーに配れるようにする。

インストール

$ npm install --save-dev gulp-zip gulp-minify-css

gulpのプロダクション用ビルドタスクは次の通り。

タスク名 用途
styles:prod プロダクション環境用のstyleビルド(less:prod、fonts、imagesを実行する)
less:prod lessファイルのプロダクション用ビルドを行う
bundle:prod JavaScriptのプロダクション用ビルドを行う
prod プロダクションビルドを起動する
build プロダクションビルド(cleanしてprodする)

less:devとless:prodの共通部分をless関数として切り出して、それぞれのタスクに利用する。 webpackのプロダクションビルド用設定を追加してbundle:prodではそちらを使うようにする。

gulpfile.js

-- 中略 --
function less() {
  return gulp.src('app/styles/**/*.less')
  .pipe($.less({
    paths: [
      'node_modules/bootstrap/less'
    ]
  }));
}
-- 中略 --
gulp.task('styles:prod', ['less:prod', 'fonts', 'images']);

gulp.task('less:dev', () => {
  return less()
  .pipe(gulp.dest(path.join(distDir, 'styles')))
  .pipe($.livereload());
});

gulp.task('less:prod', () => {
  return less()
  .pipe($.minifyCss())
  .pipe(gulp.dest(path.join(distDir, 'styles')))
});
-- 中略 --
gulp.task('bundle:prod', cb => {
  webpack(require('./webpack.production.config.js'), cb);
});
-- 中略 --
gulp.task('prod', ['html', 'styles:prod', 'bundle:prod'], () => {
  return gulp.src(path.join(distDir, '**/*'))
  .pipe($.zip(pkg.name + '.zip'))
  .pipe(gulp.dest('build'));
});

gulp.task('build', ['clean'], () => {
  gulp.start('prod');
});
-- 中略 --

webpackの設定を開発用とプロダクション用で分離する。両方の共通部分をcommon設定ファイルとして取り出す。

webpack.common.js

var path = require('path');
var pkg = require('./package.json');
var distDir = path.join(__dirname, pkg.dist);

module.exports = function() {
  return {
    entry: {
      app: './app/scripts/app.js'
    },
    output: {
      path: path.join(distDir, 'scripts'),
      filename: '[name].js'
    },
    module: {
      loaders: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          loader: 'babel'
        }
      ]
    }
  };
};

開発用のwebpack設定は、共通設定を読み込んでソースマップを追加した設定にする。

webpack.config.js

var config = require('./webpack.common.js')();
config.devtool = 'inline-source-map';

module.exports = config;

プロダクション用のwebpack設定は、共通設定を読み込んでUglifyプラグインを追加した設定にする。

webpack.production.config.js

var config = require('./webpack.common.js')();
var webpack = require('webpack');
config.plugins = [
  new webpack.optimize.UglifyJsPlugin({
    compress: {
      warnings: false
    }
  })
];

module.exports = config;

buildディレクトリはgitignore対象。

.gitignore

dist
build
-- 中略 --

ビルド用コマンドをpackage.jsonに追加する。

package.json

-- 中略 --
  "scripts": {
    "gulp": "gulp",
    "build": "gulp build"
  },
-- 中略 --

プロダクション用ビルドはコマンドを叩いて行う。

$ npm run build

テスト環境

テストはmochapower-assertで書いている。 テストコードもES6で書きたいのでespower-babelを使っている。

インストール

$ npm install --save-dev mocha power-assert espower-babel

mochaの設定を入れておく。

test/mocha.opts

--compilers js:espower-babel/guess

テストコードはこんな感じ。

test/Hoge-test.js

import Hoge from '../app/scripts/Hoge';
import assert from 'power-assert';

describe('Hoge', () => {

  const name = 'Test';
  let hoge;
  beforeEach(() => {
    hoge = new Hoge(name);
  });

  describe('#greet', () => {
    it('returns greet message', () => {
      assert(hoge.greet() === `Hello! ${name}`);
    });
  });

});

テストを叩くためのnpmコマンドをpackage.jsonに追加する。

package.json

-- 中略 --
  "scripts": {
    "gulp": "gulp",
    "build": "gulp build",
    "mocha": "mocha",
    "test": "mocha test/*-test.js"
  },
-- 中略 --

テスト叩くときはこんな感じで一気に。Vimで編集のファイルとか個別に叩くときはnpm run mocha で。

$ npm test

APIサーバとつなぎながら開発

AjaxでバックエンドのAPIを使う場合、可能であればバックエンドのサーバを起動しておいて、 http-proxy-middlewareを使ってAPIへのリクエストだけリバースプロキシしている。

インストール

$ npm install --save-dev http-proxy-middleware

この場合、gulpのserveタスクに次のような修正を加える。 下の設定例だと/apiなパスへのリクエストはlocalhostの3000ポートの方へプロキシされる。

gulefile.js

-- 中略 --
var proxyMiddleware = require('http-proxy-middleware');
-- 中略 --
gulp.task('serve', () => {
  var port = process.env.API_PORT || 3000;

  $.livereload.listen();
  connect()
  .use(connectLivereload())
  .use(serveStatic(distDir))
  .use(proxyMiddleware([
    '/api'
  ], {
    target: 'http://localhost:' + port,
    changeOrigin: true
  }))
  .listen(3100);
});
-- 中略 --

fetchを使ってAPIを叩くサンプルが次のような感じ。

app/scripts/Hoge.js

export default class Hoge {

  constructor(name) {
    this._name = name;
  }

  greet() {
    return `Hello! ${this._name}`;
  }

  fetchGreet() {
    return fetch('/api/greeting')
    .then(res => res.json())
    .then(json => {
      return `${json.message} ${this._name}`;
    });
  }
}

app/scripts/app.js

import Hoge from './Hoge';

const hoge = new Hoge('test');

console.log(hoge.greet());

hoge.fetchGreet()
.then(message => {
  console.log(message);
});

余談:バックエンドサーバのAPIをモック

バックエンドサーバのAPI仕様がJSON Hyper-Schemaで定義されている場合 (下のfixture/api.jsonみたいな感じ)、雑にモックサーバを起動して代替にしている。

fixture/api.json

{
  "$schema": "http://interagent.github.io/interagent-hyper-schema",
  "type": [
    "object"
  ],
  "definitions": {
    "greeting": {
      "$schema": "http://json-schema.org/draft-04/hyper-schema",
      "title": "Greeting",
      "description": "ご挨拶API",
      "stability": "prototype",
      "strictProperties": true,
      "type": [
        "object"
      ],
      "definitions": {
        "message": {
          "description": "ご挨拶メッセージ",
          "example": "こんにちは",
          "type": [
            "string"
          ]
        }
      },
      "links": [
        {
          "description": "ご挨拶メッセージを取得する",
          "href": "/api/greeting",
          "method": "GET",
          "rel": "self",
          "title": "Info"
        }
      ],
      "properties": {
        "message": {
          "$ref": "#/definitions/greeting/definitions/message"
        }
      }
    }
  },
  "properties": {
    "greeting": {
      "$ref": "#/definitions/greeting"
    }
  }
}

インストール

$ npm install --save-dev json-schema-mockserve

この場合、gulpのserveタスクを次のように修正する。 環境変数MOCKに値が設定された場合、モックサーバを起動するようにしている。

gulpfile.js

-- 中略 --
var MockServe = require('json-schema-mockserve').MockServe;
-- 中略 --
gulp.task('serve', () => {
  var port = process.env.API_PORT || 3000;

  if (process.env.MOCK) {
    new MockServe({
      port: port,
      path: path.join(__dirname, 'fixture', 'api.json')
    }).start();
  }

  $.livereload.listen();
  connect()
  .use(connectLivereload())
  .use(serveStatic(distDir))
  .use(proxyMiddleware([
    '/api'
  ], {
    target: 'http://localhost:' + port,
    changeOrigin: true
  }))
  .listen(3100);
});
-- 中略 --

モックサーバで起動するコマンドをpackage.jsonに追加する。

package.json

-- 中略 --
  "scripts": {
    "gulp": "gulp",
    "gulp:mock": "MOCK=ON gulp",
    "build": "gulp build",
    "mocha": "mocha",
    "test": "mocha test/*-test.js"
  },
-- 中略 --

コマンドから起動する。

$ npm run gulp:mock

fetchをモックしてテスト

Ajaxなことは、Promiseベースのインターフェイスになっていることから、 XMLHttpRequestよりもfetchを使うようになった。 fetchを使うAPIをテストする場合、fetch-mockを使っている。

インストール

$ npm install --save-dev fetch-mock

テストコードは次のような感じに。 fetchMockのmockでモック設定して、afterEachでrestoreしてモック解除している。

test/Hoge-test.js

import Hoge from '../app/scripts/Hoge';
import assert from 'power-assert';
import fetchMock from 'fetch-mock';

describe('Hoge', () => {

  const name = 'Test';
  let hoge;
  beforeEach(() => {
    hoge = new Hoge(name);
  });
  afterEach(() => {
    fetchMock.restore();
  });

  describe('#greet', () => {
    it('returns greet message', () => {
      assert(hoge.greet() === `Hello! ${name}`);
    });
  });

  describe('#fetchGreet', () => {
    beforeEach(() => {
      fetchMock.mock('/api/greeting', 'GET', {
        status: 200,
        body: '{"message":"Hello Hello Hello..."}'
      });
    });
    it('returns greet message promise', () => {
      return hoge.fetchGreet()
      .then(message => {
        assert(message === `Hello Hello Hello... ${name}`);
      });
    });
  });

});

ブラウザでテスト

nodeの環境だけでなくブラウザ環境でもテストする場合はtestemを使っている。

webpackでJavaScriptコードをブラウザ用にビルドしてtestemに読ませてる。 このとき、power-assertとwebpackでjsonの読み込みを可能にしておかないと、 ビルド後のコードが動かないのでjson-loaderを追加しておく。

インストール

$ npm install --save-dev testem glob json-loader

テスト用のwebpack設定を追加する。 testディレクトリ配下のテスト用コードをglobでごそっと集めて変換する。

webpack.test.config.js

var path = require('path');
var glob = require('glob');

module.exports = {
  entry: {
    test: glob.sync(path.join(__dirname, 'test/**/*-test.js'))
  },
  output: {
    path: path.join(__dirname, '.powered-assert'),
    filename: '[name].js'
  },
  devtool: 'inline-source-map',
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      },
      {
        test: /\.json$/,
        loader: 'json'
      }
    ]
  }
};

testemの設定をtestem.jsonに行う。 実行前にwebpackでテスト用ビルドを行い、実行後にテスト用ビルドディレクトリ(.powered-assert)ごと破棄する。

testem.json

{
  "framework": "mocha",
  "before_tests": "webpack --config webpack.test.config.js",
  "on_exit": "rm -rf .powered-assert/",
  "src_files": [
    ".powered-assert/**/*.js"
  ]
}

テスト用ビルドディレクトリもgitignore対象にする。

.gitignore

dist
build
.powered-assert/
-- 中略 --

testemの実行をコマンドとして追加しておく。

package.json

-- 中略 --
  "scripts": {
    "gulp": "gulp",
    "gulp:mock": "MOCK=ON gulp",
    "build": "gulp build",
    "mocha": "mocha",
    "testem": "testem",
    "test": "mocha test/*-test.js"
  },
-- 中略 --

testemを起動する場合は次のようにして行う。起動したらブラウザでアクセスする。

$ npm run testem

一気に行う場合はciオプションで実行する。

$ npm run testem ci

まとめ

一通りやったリポジトリこちら

必要なものを必要なときに必要なだけ取り入れつつ、全体的に薄めに取り替えが効くようにシンプルな構成で...というのが今のところの自分の方針。