Docker Node Testerを使ってNode.jsをバージョン毎にテストする #葉桜js

昨日は 葉桜js でした。


僕のLT

t_wadaさんが来るということでテストっぽい話をしたくて、Docker Node Testerの話をしました。

Node.js v0.12 に関して

このブログで Node.js v0.12で変わることについて色々と紹介してきました。NANの話もそうですし、traicing apiとかsync child_processもそうです

Node.js v0.12では他にもコアモジュールに色々とバグ修正やエンハンスが加わっています。

コアチームが互換性に配慮しているとはいえ、影響を受けてしまい、自分のモジュールが動かなくなることは考えられます。

そこで Docker Node Tester

Docker を使ってNode.jsのバージョン間でビルドしたイメージを作っておいて簡単にバージョンごとのテストを行うツールです。

How to use

インストールは下記のとおりです。

$ npm install dnt -g

.dntrcっていう下記のようなファイルをプロジェクトのルートに作っておきます。

NODE_VERSIONS="\
  master   \
  v0.11.9  \
  v0.11.8  \
  v0.10.22 \
  v0.10.21 \
  v0.8.26  \
"
TEST_CMD="\
  cd /dnt/ &&                                                    \
  npm install &&                                                 \
  node_modules/.bin/node-gyp --nodedir /usr/src/node/ rebuild && \
  node_modules/.bin/tap test/*-test.js;                          \
"

この状態で sudo setup-dntを実行するとNODE_VERSIONSに記述したNode.jsのイメージを作成してくれます。

最後に sudo dnt を実行すると下記のような結果が出ます。

これで、バージョン間のテストを実現できます。

注意点

まだ Mac の boot2dockerだとメモリ不足の影響でdocker環境構築に時間がかかったり、エラーになることがあります。(※僕はDigital OceanでUbuntuの環境を借りて実施しました。)

また、最新のDocker v0.10に Docker Node Testerが対応できてないので、今pull reqを投げて様子を見ているところです。既にDocker v0.10がインストールされているのであれば、僕のpull req内容を元に試してください

pullreqがマージされたので、最新のdocker node tester v0.3.0 以降では docker v0.10サポートされました!!

#寿司js 、#桜js に行ってきた感想と居心地が良いコミュニティを作ることに関して

teppeisさんのブログエントリを読んで、そういえば全然寿司jsと桜jsについて書いてなかったなと思ったので書く。

寿司js

寿司jsはいつもtwitter上では絡みのあるteppeisさんと一回話してみたかったazu_reさん、imayaさんと飲んでみたいというモチベーションから寿司食べに行こうって誘ったのが始まりでした。結局飛行機の都合でteppeisさんは来れなかったものの、hokacchaさんやyoshikikojiさんやkyo_agoさんが集まってワイワイした飲み会LT大会に。

寿司jsは以下のエントリを参考にしてもらえればいいかと。

寿司jsでしたLT

僕が寿司jsでした話は、チーム開発してて依存ファイルに変更あった時にライブラリインストールし直す必要あるんだけど、みんなどうしてんの?っていう話で、ちょうど事前にブログ書いてたので、それを解決するためのツール、hookinの紹介でした。

git pullでファイルに変更があったら特定のコマンドを実行する。 - from scratch

毎回起動する時にインストールしなおして解決するとか、代替策は出たんだけど、弊社ではgithubにはあるけど、npmにpublishされてないモジュールが多くて、そういうモジュールをpackage.jsonに記述すると、npm が変更があったかどうかを検知できずに毎回installが実行されるから無駄が多いんだという議論になり、この辺、npmが頑張ってくれるといいよね、っていう話をした。

寿司jsの感想


まぁこんな感じだった。どちらかと言うとお互いが共通項目について深く話すというよりもお互いの業界を肴に便利情報や自分の興味を語り合うって感じだった。

LTの内容は全部面白かったんだけど、特に面白かったのは、mozaic.fmでも話してたWebComponentsの話 by hokacchaが面白かった。

x-sushiっていうカスタムタグを作って皿属性を消すと皿を見えなくしたり、うにをマグロに変えたりとデモをしてくれた。モジュラビリティを高める話もあったし、ちょうどmozaic.fmで話してたパッケージマネージャの話もチラホラ出てて面白かった。

桜js

花見しながらハッカソンしたいよね、っていうノリからkyo_agoさんが主体となって呼びかけてくれた飲み会LT大会です。結局天候の都合によって花見ハッカソンは叶わなかったんだけど、居酒屋でLT大会になった。

寿司jsの時にtkihiraさんに会いたいって言っていた人が多数いたのでtkihiraさんを入れ、飛び入りでJxckさんとotiai10さんを交えて開催した。

桜jsでしたLT

僕が桜jsでした話はNode.js v0.12で変わることの話だった。tracing apiやchild_processのsync系 APIの話、つい最近入ったPromise, WeakMap, Object.observeが使えるようになるっていう話をした。

基本的には前のNode学園で発表した話と当日にあったpull reqの話だった。

これからのNode.jsの話をしよう // Speaker Deck

https://github.com/joyent/node/pull/7394#issuecomment-39276195

tracing apiとかWeakMapとかメモリの解放がやりやすくなっていいよね、っていう話をした。

桜jsの感想


というわけで、teppeisさんが書いてくれている通り、深い議論ができて物凄く面白かった。
みんながしたLTと問題提起が良かったんだと思う。

居心地が良いコミュニティを作ること

桜jsと寿司js、すごく居心地が良かった。コンテキストが大体一緒で、皆一定以上の深い造詣や知識があってそういう人達が集まってワイワイ議論するのって物凄く楽しくて有効で、大きめの勉強会行って話し聞いて帰ってくるよりも得られたものは大きかったと思う。

PUT/DELETEの議論の深化もそうだし、クライアントサイドMVCに対するdisもそう。気を遣う事無く自分の思っていることをぶつけられる場っていうのはやっぱり楽しい。

んで、自分は今年の1月からNode.jsユーザーグループの代表なんですけど、Node.jsってフロントエンドツールとしてもバックエンドサーバとしても使われてて、活躍の舞台が広くて必然的にたくさんの人が集まってくれてて嬉しい限りなんですが、大人数になればなるほどコンテキストが大体一緒で深い知識や造詣を持っている人達とコミュニケーションするのってどんどん難しくなってきてるなと感じています。

しょうがないんですけど、東京Node学園に来てもらって発表だけ聞いて帰った時に得ているものと桜js、寿司jsで深い議論した時に得ているものって違うんですよね、質も量も。

Node学園で発表している内容ってのは、そこまで本質とは関係なくて、今のNode.jsで起きている最新の事をsyncする程度で考えてもらえれば良いかなと思っているんです。本質は最後にある懇親会だと思うんですよ。そこで自分が今抱えている問題や作ろうとしているアプリケーションのことを知見者に話したり、深い造詣を持っている人に聞くっていう、思っていることをぶつけられる場にしたいなと思うんです。

発表やLTはそのためのものであって、コミュニケーションのためのタネです。時間の都合上しょうがない人も多いと思うし、人見知りの人もいるのでしょうがないと思うんですけど、発表だけ聞いて懇親会に出ないというのは自分の思っていることをぶつけられる機会を失っているって言うことなので非常に損だと思うんです。

という訳で、今度Node学園 4/24(木) 19時から渋谷ヒカリエで実施します
コミュニケーションしてみたいけど、詳しい人が誰なのか分からないって言う方や知り合いがいないっていう人は僕が懇親会で話している輪に勝手に加わって、僕に質問や思いの丈をぶつけて下さい。僕より詳しい人がいればその人を紹介しますし、答えられる範囲で話します。

I'm looking forward to talking with you :)

node.jsのnative addonを作るときはNANを使おう。

さて、 Node.js v0.12 で変わることの一つとして、native addonを作る時に後方互換性を壊す変更が加えられています。

これにより、v0.10でnative addonを作っているモジュール達は、ほとんど動かなくなってしまうことが考えられます。

V8側がこの後方互換性を壊す変更をしているため、V8に追従しているNode.js側としてはこのbreaking changesを受けざるを得なかったんだと思います。*1

どれくらい変更されてるのかは node.js の native addon で Hello World モジュールを作る方法が載っているのでそれをまずは参考にします。

// これまでの v0.10ではこう書いてた。
#include <node.h>
#include <v8.h>

using namespace v8;

Handle<Value> Method(const Arguments& args) {
  HandleScope scope;
  return scope.Close(String::New("world"));
}

void init(Handle<Object> exports) {
  exports->Set(String::NewSymbol("hello"),
      FunctionTemplate::New(Method)->GetFunction());
}

NODE_MODULE(hello, init)
// v0.11以降はこう書く

#include <node.h>

using namespace v8;

void Method(const FunctionCallbackInfo<Value>& args) {
  // 現在のisolate(current thread)を取得する。
  Isolate* isolate = Isolate::GetCurrent();
  HandleScope scope(isolate);
  // args.GetReturnValueで値をreturnする。
  // Stringのインスタンス方法も変更されてる。
  args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world"));
}

void init(Handle<Object> exports) {
  // NODE_SET_METHODというFunctionTemplateのヘルパー関数を利用してる。
  // これ自体は v0.8から既にあった様子。
  NODE_SET_METHOD(exports, "hello", Method);
}

NODE_MODULE(addon, init)

とまぁこうなります。

まぁお世辞にも変更が少ないとは言えないような感じです。
このHello Worldのモジュールだけでも、かなり変更されている事がわかります。

これって

V8に追従している以上しょうがないんですけど、ネイティブモジュールの作成者側からするとかなり辛いです。

しかも一度この変更に対処してしまうと、v0.10ではそのモジュールの最新に追従できないという問題も発生します。

というわけで、毎回毎回辛い思いをしているんですが、そんな状況に終止符を打つためのNANという抽象化ラッパーが登場しています。

NAN (Native Abstractions for Node.js) とは

node.js NAN で検索してもNaNの事しかヒットしないのでググラビリティとは何なのか、という名前ですが、提供してくれる機能は強力です。


先ほどの breaking changes に対応してくれるためのモジュールで、NANが提供するラップされた関数を使ってnative add-onを作っておけば、NANがnode.jsのバージョンを見て、v0.8, v0.10, v0.11のいずれかの変更に対応してくれます。先ほどの問題を解決してくれるわけです。

NANというのは、Native Abstractions for Node.jsの略称です。直訳するとNode.jsのためのネイティブ抽象化、という意味ですね。

作者のメッセージを引用します。

Thanks to the crazy changes in V8 (and some in Node core), keeping native addons compiling happily across versions, particularly 0.10 to 0.11/0.12, is a minor nightmare. The goal of this project is to store all logic necessary to develop native Node.js addons without having to inspect NODE_MODULE_VERSION and get yourself into a macro-tangle.

This project also contains some helper utilities that make addon development a bit more pleasant.

V8 (と Node coreのいくつかのモジュール) の crazy な変更のおかげで、native addonがバージョン間を跨いでコンパイルするのは困難になってしまったよ。 特に 0.10-0.11/0.12のマイナーチェンジは悪夢だね。
このプロジェクトのゴールはNode.jsのバージョン変更を調査しなくても、native addon を開発するために必要なロジック、ライブラリを提供することだ。

このプロジェクトには、バージョン間の差異を吸収するだけじゃなく、addon 開発をより楽しくするためのヘルパーユーティリティも含むよ。

ということです、NANをベースに作っていきましょう。

NAN 対応する

とりあえず、HelloWorldをNAN対応しましょう。

まずはインストールから

$ npm install nan -S

installしたらbinding.gypのtarget property以下に下記の文字列を記述します。

      "include_dirs" : [
        "<!(node -e \"require('nan')\")"
      ]


終わったら、以下のようにnativeモジュールをNAN対応していきます。

addon.cc

#include <node.h>
// nan.hを読み込む

#include <nan.h>

using namespace v8;

// v8に公開したいMETHODの定義時にNAN_METHODを使う
NAN_METHOD(Method) {
  // NanScopeで始める、これはHandleScopeをラップする奴。
  NanScope();
  // NanReturnValueで値を返す、return のラッパー
  // ちなみに文字列もNanSymbolで囲む
  NanReturnValue(NanSymbol("world"));
}

void init(Handle<Object> exports) {
  // exports->Setで定義する方向に固定。
  // その代わりNanSymbolを使う
  exports->Set(NanSymbol("hello"),
      FunctionTemplate::New(Method)->GetFunction());
}

NODE_MODULE(addon, init)


ここまででaddon.ccモジュールのNAN対応が実施されました。

NAN対応済みのサンプルプロジェクトは以下になります。

yosuke-furukawa/NANSample · GitHub

これを使って実際にv0.8 - v0.11で使えるのかtravisで確認しました。

f:id:yosuke_furukawa:20140407204934p:plain

結果はこんな感じで全部のバージョンでビルドができて、テストが通る事を確認できました。

Hello World以上の事を知りたい場合は

NANプロジェクトにあるexamplesが参考になります。円周率を求めるためのモジュールですが、syncのAPIとasyncのAPIがそれぞれ用意されてて非常に参考になります。

NAN example

後はsindresorhusさんが、WindowsOSXの時の差を吸収するためのモジュールの書き方を実践してくれているのでそれも参考になるかもしれません。

sindresorhus/fullname-native · GitHub

2014/04/08 追記 NAN対応を実践してみた。

実践してみました。ハマりどころを紹介しておきます。

NAN対応でハマった所:

通常のコードをNANでラップするのはそこまで難しくなかった。
一応、雰囲気としては、HandleScope scope;から始まるメソッドの始まりはNanScope();にして、returnで返す値はNanReturnXXXXを使う、

jsに公開するメソッド Handle Hoge(const Arguments& args) 等はNAN_METHOD(Hoge)に変更するだけ。

という感じにやっていけば機械的に変更できる。
PersistentからLocalにする時に一瞬ハマったけど、公式ドキュメントにあるNanPersistentToLocalを使えば大丈夫。

大変だったのは、、、

  • v8::StringでMayContainNonAsciiとWriteAsciiの2つのメソッドが消えていること
  • ArrayBufferクラスのインスタンスの取り扱いが変わっていること

の2点。

MayContainNonAsciiとWriteAsciiの2つのメソッドが消えている

MayContainNonAsciiとWriteAsciiはバグってた様子。共通で使っているHasOnlyAsciiCharsっていうメソッドが正しい値を返さないことがあるってことで、APIから削除されました。

んで、新しくIsOneByteっていうメソッドが追加されているのでv0.11以降はそれを使いましょう。v0.10ではまだこのメソッドが追加されていないので、そこは諦めてこうやって書きましょう。

#if (NODE_MODULE_VERSION > 0x000B)
        // node.js v0.11+
        bool hasMultiByte = !str->IsOneByte();
#else
        // node.js v0.10
        bool hasMultiByte = str->MayContainNonAscii();
#endif
        if (hasMultiByte) {

WriteAsciiはテストコード読むとWriteUtf8を使うようになったので、そちらを使いましょう。

ArrayBufferの取り扱いが変わった件

Bufferから値を読み込む時に値が入っているかどうかを確認するために使う、HasIndexedPropertiesInExternalArrayDataがtrueを返さなくなった。

詳しくは、このgist見てもらうといいんですけど、ArrayBufferを渡すのではなく、TypedArrayそのものを渡すと動いたので、そういう動作に変更されたようです。

というところでかなりハマったんですが、なんとか抜け出せました。:)

一旦ここまで

本当は、NANの紹介だけじゃなくてどんな時にどのラッパーを使うかとか、API翻訳とか、あまりにもブログのエントリが長くなりそうなので、紹介まで。

気が向いたらもう少し長く紹介します。

*1:Node.jsが安全性を担保する上で要求した変更も多いようなので、breaking changesに関してはV8だけを攻めるのは筋違いですね。 see: https://groups.google.com/forum/#!msg/v8-users/6kSAbnUb-rQ/e-GI0b1ThA4J

testling-ciとtravis-ciでクライアントサイドもサーバサイドもテストを実行する

まえがき

NHK番組表APIjavascript版を作りました。

NHKの番組表APIが発表されてて、皆思い思いに好きな言語で実装されていくのを見てました。

Golang : https://github.com/mattn/go-nhk

Scala : https://github.com/seratch/nhk4s

Perl : https://github.com/moznion/WWW-NHKProgram-API

Python : https://github.com/drillbits/nhk-api

Ruby : https://github.com/mitukiii/nhk_program-for-ruby

Emacs : https://github.com/gongo/emacs-nhk-program

Titanium : https://github.com/h5y1m141/TiNHKProgram

それのNode.js版ってよく見ると無いなと思ったんで作ったんですが、これは考えてみればbrowserifyでクライアントサイドでも使えるライブラリとして公開するチャンスだと思ってクライアントサイドjavascriptとしてもサーバーサイドのjavascriptとしても動作するハイブリッドのNHK番組表クライアントを作ってみました。

Node.js : https://github.com/yosuke-furukawa/nhk_api.js

npm : https://www.npmjs.org/package/nhk_api

bower : http://bower.io/search/?q=nhk_api

Getting Started

node.jsの場合
$ npm install nhk_api
browserから使う場合
$ bower install nhk_api

installしたらbowerをscriptで読めるようにする。

<!-- in browser -->
<script src="components/nhk_api/client/nhk_api.js"></script>

使い方

var NHK = require("nhk_api");

// KEYを設定してください。
var nhk = new NHK("YOUR_API_KEY");
var callback = function(err, result) {
  console.log(JSON.stringify(result));
};

//  nhk list api
nhk.list.get("130", "g1", callback);
nhk.list.get("東京", "NHK総合1", callback);
nhk.list.get("東京", "NHK総合1", "tomorrow", callback);
// nhk genre api
nhk.genre.get("130", "g1", callback);
nhk.genre.get("東京", "NHK総合1", callback);
nhk.genre.get("東京", "NHK総合1", "tomorrow", callback);
// nhk info api
nhk.info.get("130", "g1", "123456789", callback);
// nhk now on air api
nhk.now.get("130", "g1", callback);

本題

さて、これは単に作ったというだけで本題はここからなんですが、Node.js版、というかサーバーサイドはtravisとかあって簡単にバージョンを跨いだテストが出来ますよね。

クライアントサイドの場合はどうでしょう。ブラウザ間のバージョンを跨いだテストを実施したいところです。せっかくbrowserifyを使ってサーバーサイドとクライアントサイド両方共同じ使い方で使えているので、testling-ciというサイトを使ってブラウザ間のテストを実行しましょう。

testling-ciでテストを実行すると以下の様なバッジが生成されて見れるようになります。

どのブラウザでサポートされているのか一目瞭然で分かりやすいですね。

ちょっと前にsubstackが公開していた記事があって、それを見るとやり方が書いてあります。ちょっと長いんですが最後までお付き合いをお願いします。

簡単にやり方を紹介していきます。

テストツールのインストール

以下のツールをインストールしておきます。

// phantomjs は必須、そうでなくても何らかのヘッドレスブラウザがないとtestlingが動かない
$ brew install phantomjs
$ npm install browserify tape testling -g

browserifyはサーバーサイドのコードをクライアントでも使えるようにしてくれるツールですね。

tapeはtapが喋れるテストライブラリです、mochaでもいいんですが、substackが最近issue全部クローズした!!って騒いでたので使ってみました。

testlingはtestling-ciの動作確認ができるCLIツールです。インストールが終わったら準備完了。

まずはtapeを使ってテストを書いて、nodeのテストを実行します。

こんな感じのファイルをtestフォルダ以下に用意します。

var NHK = require('../index');
// tapeをrequire
var test = require('tape');

// 時間用ライブラリを利用
var moment = require('moment-timezone');

// this apikey for test
var apikey = process.env.NHK_API_KEY || "123456789";

// genreのurlがあっているかどうかテスト
test(' genre url ', function (t) {
  var nhk = new NHK(apikey);
  var url = nhk.genre.createUrl("130", "g1", "0000");
  var date = moment().tz("Asia/Tokyo").format("YYYY-MM-DD");
  // nhkのgenre apiのurlと合致することを期待
  var expected = "http://api.nhk.or.jp/v1/pg/genre/130/g1/0000/" + date + ".json?key=" + apikey;

  // t.equalで比較、第一引数が actual、第二引数が expected、第三引数がコメント
  t.equal(url, expected, "url is same");
  // t.endでテストが終了される。
  // t.endを呼ぶのは必須、非同期テストの場合はこっちのほうが嬉しい。
  t.end();

  //ちなむとt.planっていうメソッドがあって、それをcallすると指定回数のassetチェックが終わると自動でendされる仕組みになる。
  // t.plan(1);
});

test(' genre url to specify keyword ', function (t) {
  var nhk = new NHK(apikey);
  // 日本語でもgenre生成されるんですよ
  var url = nhk.genre.createUrl("東京", "NHK総合1", "0000");
  var date = moment().tz("Asia/Tokyo").format("YYYY-MM-DD");
  var expected = "http://api.nhk.or.jp/v1/pg/genre/130/g1/0000/" + date + ".json?key=" + apikey;
  t.equal(url, expected, "url is same");
  t.end();
});

テストの実行は簡単で

$ tape test/genre.js

で実行できます。

TAP version 13
# genre url
ok 1 url is same
# genre url to specify keyword
ok 2 url is same
# genre url today
ok 3 url is today
# genre url tomorrow
ok 4 url is tomorrow
# get genre url
ok 5 error is not found
ok 6 msg is truthy
ok 7 msg.list is truthy
ok 8 msg.list.g1 is truthy

1..8
# tests 8
# pass 8

# ok

node-tapをインストールしているならtapコマンドが使えるようになるので、以下の様な感じになります。

$ tap test/genre.js

ok test/genre.js ........................................ 9/9
total ................................................... 9/9

ok 

と、ここまでは普通のNode.jsのテストですね。

testlingを使ってbrowserのテストも実行する。

testlingとbrowserifyを使ってbrowser側のテストも実行しましょう。実行はすごく簡単。

$ browserify test/genre.js | testling

TAP version 13
#  genre url
ok 1 url is same
#  genre url to specify keyword
ok 2 url is same
#  genre url today
ok 3 url is today
#  genre url tomorrow
ok 4 url is tomorrow
#  get genre url
ok 5 error is not found
ok 6 msg is truthy
ok 7 msg.list is truthy
ok 8 msg.list.g1 is truthy

1..8
# tests 8
# pass  8

# ok

ってやるだけですね、これはすごく単純なことしかしてなくて、

  1. browserifyでtestコードをブラウザでも実行できるようにする
  2. testlingからphantomjsを呼び出してそのコードを実行する

ということをやっているだけです。

※phantomjsを入れる前にtestlingを既にインストールしていると、phantomjsを入れてもtestlingがphantomjsを認識してくれない事があります、この場合は ~/.config/browser-launcher/config.json を一旦消してから再実行してみましょう。 

coverageを取る場合

substackが作っているcoverifyっていうツールを使います。

$ browserify -t coverify test/*.js | testling | coverify

...

# /Users/yosuke/Program/nhk-api/lib/base.js: line 53, column 9-28

          console.error(data);
          ^^^^^^^^^^^^^^^^^^^^

# /Users/yosuke/Program/nhk-api/lib/base.js: line 54, column 9-25

          console.error(e);
          ^^^^^^^^^^^^^^^^^

# /Users/yosuke/Program/nhk-api/lib/base.js: line 55, column 9-14

          cb(e);
          ^^^^^^

# /Users/yosuke/Program/nhk-api/lib/base.js: line 59, column 5-10

      cb(e);
      ^^^^^^

# coverage: 860/864 (99.53 %)

コレを使うと通っていないコードとテストのcoverageを取ってくれます。この場合は99.53%カバレッジですね。

tapeとbrowserifyとtestlingでサーバーサイドもクライアントサイドも同じテストコードでテストできるようになりました。ついでにcoverify使うとカバレッジも計測できます。

CIツールで自動テスト

サーバサイドのCIツールはtravisがあるのでそれを使いましょう。以下のサイトが参考になります。

http://d.hatena.ne.jp/hokaccha/20111110/1320910718

クライアントサイドはCIツールとして、testling-ciを使います、これを使うための前準備として、package.jsonに以下のように記述します。

{
  "name": "nhk_api",
  "version": "0.1.1",
  "description": "Node.js client for NHK API",
  "main": "index.js",
  "scripts": {
    "test": "tape test/*.js",
    "build": "browserify -g uglifyify -r ./index.js:nhk_api -o client/nhk_api.js"
  },
  "keywords": [
    "nhk",
    "api"
  ],
  "author": "yosuke furukawa",
  "license": "MIT",
  "dependencies": {},
  "devDependencies": {
    "tape": "~2.10.2",
    "uglifyify": "~1.2.3",
    "browserify": "~3.32.0",
    "moment-timezone": "0.0.3"
  },
  "testling": {
    "files": "test/*.js",
    "browsers": [
      "ie/6..latest",
      "chrome/22..latest",
      "firefox/16..latest",
      "safari/latest",
      "opera/11.0..latest",
      "iphone/6",
      "ipad/6",
      "android-browser/latest"
    ]
  }
}

こんな感じで、testlingプロパティを設定してください。ここにbrowserのどのバージョンをtestling-ciでテストするかを定義します。

設定したら、githubのwebhookページから、http://git.testling.comへのhookを設定してください。

git pushを実行したら、以下のページで実行が確認できます。

https://ci.testling.com/$YOUR_USERNAME_HERE/$YOUR_REPOSITORY_NAME

実際の状況はこちら:https://ci.testling.com/yosuke-furukawa/nhk_api.js

バッジを見る

バッジはこんな感じで手に入ります。

https://ci.testling.com/$YOUR_USERNAME_HERE/$YOUR_REPOSITORY_NAME.png

実際のコード


markdownに貼るならこちら

[![browser support](https://ci.testling.com/$YOUR_USERNAME_HERE/$YOUR_REPOSITORY_NAME.png)
](https://ci.testling.com/$YOUR_USERNAME_HERE/$YOUR_REPOSITORY_NAME)

まとめ

  • NHK番組表APIのサーバサイドでもクライアントサイドでも使えるハイブリッド版を作りました
  • browserifyとtestlingとtapeでクライアントサイドもサーバサイドも同じコードでテストが実行できます
  • testling-ciを使うと各ブラウザ間でテストが実行され、CIが使えます。

browserifyを使うことでサーバサイドもクライアントサイドもほぼ同じコードが使えるようになりました。そのクライアントサイドのテストをするならbrowserifyとtestling-ciの組み合わせは非常に相性が良いです。使っていきましょう。

atomのpackageの作り方

先日、atomというgithub製のIDEが公開されて話題になってます。

f:id:yosuke_furukawa:20140301184804p:plain

これ、広める戦略がうまくて、昔のgmailと同じく、inviteを受けた人が3人だけinvite ticketを持ってて、その人からまた3人inviteできるって仕組みになってます。こうすることでSNSでのinvite ticket要求が盛んになり、流行ってるように見えるというのが上手い。

ちなみにDLされるファイルだけ他人に送っても内部的にチェックしててpackage managerとかが使えない仕組みになってるので、inviteを持ってない人はおとなしく誰かから回ってくるのを待ちましょう。

本題

atom自身はSublime Textっぽい外観で、apmっていうパッケージマネージャが付属されてます。

んで、早速apmに自作のpackageを作って公開してみました。

実行している所:
f:id:yosuke_furukawa:20140302172830g:plain

yosuke-furukawa/language-jsx · GitHub

language-jsx

このlanguage-jsxでは、JSX(DeNA製)のsyntaxハイライト機能、jsxのsnippet機能とPackages > JSX > run から 開いてるjsxファイルを実行できるようにしました。

せっかくなので、作り方と公開方法を説明していきます。

まずはpackageの構造を理解しよう

my-package/
  grammars/       - 文法を定義して、シンタックスハイライトする
  keymaps/           - keybindを定義する
  lib/               - いわゆるライブラリ、大体ココに機能を定義する
  menus/             - メニューの表示を定義する
  spec/                - test用フォルダ
  snippets/          - スニペット、短縮キーワードを登録可能
  stylesheets/      - cssでページのスタイル決めるところ
  package.json      - そのpackageの定義を記述する、npmと同じような書き方

こんな感じのフォルダ構造になります。
npmを作ったことがある人なら分かるかと思いますが、package.jsonというpackageの定義を記述するファイルが必要です。

このフォルダ群の雛形は Packages > Package Generator > Create Atom Package を実行すると簡単なフォルダ群が作成されます。

適当な名前をつけて実行すると、~/.atom/packagesに以下のようなフォルダ群を作ってくれるので楽。

my-package/
  keymaps/
  lib/
  menus/
  spec/
  stylesheets/
  .gitignore
  LICENSE.md
  README.md
  package.json

なので、ここから作業を始めて行くといいと思います。

まずは package.json を確認

package.json

{
  "name": "my-package",
  "main": "./lib/my-package",
  "version": "0.0.0",
  "private": true,
  "description": "A short description of your package",
  "activationEvents": ["my-package:toggle"],
  "repository": "https://github.com/atom/my-package",
  "license": "MIT",
  "engines": {
    "atom": ">0.50.0"
  },
  "dependencies": {
  }
}

多分こんな感じの外観になっていると思います。

package.jsonの中身を少し説明しておきます。

この中で重要なのは

  • main (指定必須) entrypointへのパスを指します、これがないと正常にインストールされないので指定必須です。
  • activationEvents (任意指定) packageがロードされてから有効になるイベントです。遅延ロードする時に使います。
  • dependencies (任意指定) ここにnpmのパッケージを指定することができます、今回作成したlanguage-jsxの場合、jsxの依存があるので、以下のようになっています。
 "dependencies": {
    "jsx": "*"
  }

全体としては以下のようになっています。

{
  "name": "language-jsx",
  "version": "0.1.3",
  "main": "./lib/jsx",
  "description": "JSX language support in Atom",
  "engines": {
    "atom": "*",
    "node": "*"
  },
  "activationEvents": [
    "jsx:run"
  ],
  "dependencies": {
    "jsx": "*"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/yosuke-furukawa/language-jsx.git"
  },
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/yosuke-furukawa/language-jsx/issues"
  }
}

ちなみにsnippetsやstylesheetsのフォルダ名を変えるなら、このpackage.jsonにsnippetsとかstylesheetsとかでキーを指定して、フォルダ名を指定すれば変更できます。

{
  "name": "language-jsx",
  "version": "0.1.3",
  "main": "./lib/jsx",
  "description": "JSX language support in Atom",
  "engines": {
    "atom": "*",
    "node": "*"
  },
  "activationEvents": [
    "jsx:run"
  ],
  "dependencies": {
    "jsx": "*"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/yosuke-furukawa/language-jsx.git"
  },
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/yosuke-furukawa/language-jsx/issues"
  },
  "snippets" : "./abc",
  "stylesheets": "./css"
}

grammarsを定義しよう

TextMateでもあったと思いますが、Grammarを定義しておくことでsyntaxのハイライトができるようになります。

定義例:grammars/jsx.cson

  {
    'match': '(?<!\\.)\\b(boolean|byte|char|class|double|enum|float|function|int|interface|long|short|void)\\b'
    'name': 'storage.type.jsx'
  }
  {
    'match': '(?<!\\.)\\b(const|export|extends|final|implements|native|private|protected|public|static|synchronized|throws|var)\\b'
    'name': 'storage.modifier.jsx'
  }

こうすることで、varとかclassとかそういうキーワードが修飾語としてハイライトされます。

f:id:yosuke_furukawa:20140302213318p:plain

詳しい定義方法はこちら。
TextMate Manual » Language Grammars

keymapsを定義する

キーマップを定義します。こうすることで、ショートカットキーによるコマンドが定義されます。


定義例:keymaps/jsx.cson

'.workspace':
  'ctrl-r': 'jsx:run'

これで、ctrl+r'jsx:run'コマンドを実行するようになります。

ちなみに、'.workspace'はdivタグに定義されているclassで、全体を表すんですが、特定の状態の時にだけ行いたい場合は、そのクラスを指定するといいです。

'.tree-view-scroller':
  'ctrl-V': 'changer:magic'

これでtree-view-scrollerクラスの上でだけctrl-vが有効になります。

menusを定義する

定義すると画面上部のメニュー一覧から実行できるようになります。

定義例:menus/jsx.cson

'menu': [
  {
    'label': 'Packages'
    'submenu': [
      {
        'label': 'JSX'
        'submenu': [
          {
            'label': 'RUN'
            'command': 'jsx:run'
          }
        ]
      }
    ]
  }
]

これで、Pacakges > JSX > RUN を実行すると、jsx:runが実行されます。

snippetsを定義する

snippetsを定義することで、記述している最中にtabを押すと補完してくれる機能が増えます。

定義例:snippets/jsx.cson

'.source.jsx':
  'class':
    'prefix': 'class'
    'body': 'class ${1:class_name} {\n\n}'
  'main':
    'prefix': 'main'
    'body': 'static function main(args : string[]) :void {\n\t${0:// body...}\n}'

これで、classと打ってからtabを打つとclassの定義が補完されます。
また、mainと打ってからtabを打つと、static function main(args: string[]) :void {}が補完されます。

libを定義する

このlibでパッケージの機能を定義します。今回のlanguage-jsxでは、JSXの実行を行う機能を提供します。

jsx_bin_path = "/node_modules/jsx/bin/jsx"
child_process = require 'child_process'

module.exports =

  # jsx:runで呼び出された時にrunメソッドを実行するようにする
  activate: (state) ->
    atom.workspaceView.command "jsx:run", => @run()

  # ATOMのSHELLをNodeコマンドとして実行するための環境変数をONにする
  # thanks @mootoh
  getExecPath: ->
    "ATOM_SHELL_INTERNAL_RUN_AS_NODE=1 '#{process.execPath}'"

  # 外からnodeコマンドの環境変数を渡せるように
  getNodePath: ->
    atom.config.get("language-jsx.nodepath")

  # 実際のmain処理
  run: ->
    # 現在開いているeditorの本体
    editor = atom.workspace.getActiveEditor()
    # language-jsxのパッケージパス
    lang_jsx_path = atom.packages.resolvePackagePath("language-jsx")
    # jsxの実行コマンドまでのパス
    jsx_bin = lang_jsx_path + jsx_bin_path
    # nodeのコマンドパス
    node_path = @getNodePath() || @getExecPath()

    # getUriで現在開いているファイルのパスを取得する
    uri = editor.getUri()
    # node jsx --run file_name.jsx
    command = "#{node_path} #{jsx_bin} --run #{uri}"
    options = {
      "cwd" : lang_jsx_path
    }

    # 子プロセスから外部コマンド実行する
    child_process.exec(command, options, (error, stdout, stderr) ->
        console.error(error) if error
        console.error(stderr) if stderr
        console.log(stdout) if stdout
    )

    # DevToolを開く
    atom.openDevTools()

こんな感じです、atomAPIはココを見るといいですね。

Atom API Documentation

nodeの外部コマンド呼び出しでハマったこと

jsxのコマンドはnode.jsのスクリプトで出来ているので、nodeから実行することが可能です、ただ、Atomそのものも中身はnode.jsなので、node.jsがmacにインストールされていなくてもAtomさえインストールされていれば実行できるようにしたい所です。

ただ、Atomprocess.execPathを取るとnodeを直接使用しているのではなく、 "Atom Helper"と呼ばれるコードを実行していることが分かります。

これから JSXを呼びだそうとしても、atomが起動してしまい、うまくいきません。

そこでどうするかというと、ATOM_SHELL_INTERNAL_RUN_AS_NODEという環境変数があることが中身のbinaryファイルから分かるので、その環境変数を無理矢理いじるとatomではなく、atomが内部で使っているnodeをnodeとしてそのまま実行することが可能です。

バイナリを解析してくれたのは @mootoh さん、感謝です。

atomのpackageを公開する。

ここだけはコマンドで実行します。

$ cd my-package
$ apm publish

で公開されます。んで、一度公開した後に修正したいときなどは

$ apm publish patch

ってやると勝手にpackage.jsonのversionのpatchバージョンをincrementしてくれます。
minorバージョンアップとmajorバージョンアップもあるので、その辺の詳しい紹介はapm helpを実行して下さい。

$ apm help

まとめ

jsxのatomパッケージを作って公開しました、あとpackageの作り方を紹介しました。

もっと詳細が知りたい場合は公式ドキュメントを閲覧して下さい。

とりあえず作ってみたい場合は2つ目のtutorialから始めるのがオススメです。


Happy atom life!!!

東京Node学園 11時限目を開催しました。

東京Node学園の11時限目を開催しました。

新代表に就任してから最初のNode学園ということで結構ハラハラ・ドキドキしてたんですが、
スタッフの皆さんと手伝ってくれた方々のおかげで滞り無く進行することができました。

ありがとうございました。

トゥギャってくれた方がいて、大変ありがたかったです。

東京Node学園 11時限目 - Togetterまとめ


イベントの様子はそこ見ても分かるかもしれません。

さてさて、東京Node学園のレポート書いていきます。

「これからのNodeの話をしよう」 by @yosuke_furukawa


内容としては、Node.jsのnews、Node v0.12で変わること、最近のライブラリ、東京Node学園の今後を話しました。

Node v0.12のtracing apiやexecSyncのデモ、koaPrimusに関して、「知らなかった」とか、「楽しそう!」みたいな反応がツイートから見られたので良かったです。

※ Primusをずっと "プリマス" って読んでたんですが、正しい発音は "プライマス" ですね。


あと、最後の襲名披露口上は超滑りましたが、まぁまぁ楽しんでもらえたようなので、代表としてエンタメ面での仕事ができて良かったかなと思います。

んで、もっとNode学園にどんな興味があるのか教えて欲しいので、gitterとgithubを作って公開しました。

https://gitter.im/yosuke-furukawa/TokyoNodeFestival2014

https://github.com/yosuke-furukawa/TokyoNodeFestival2014

ぜひ意見をお寄せ下さい。

「browserifyことはじめ」 by @hitsujiwool

hitsujiwoolさんからのbrowserifyの話。

最近browserifyかなり人気があって、azu_reさんの解説記事改めて入門記事が出るほど。

実際発表もかなり分かりやすく解説されてて、すごく面白かった。

transformすごいなーと思ったし、周りでどんなbrowserifyライブラリが作られてるのか知れてよかった。

browserifyもっと使っていきたいですね。

「NodeでDeep Learning」by @Lewuathe

Nodeで機械学習をやるというすごくエッジの効いた話。

残念ながらパフォーマンスに難あり、という話でしたが、Node.jsで行列演算とかかなり非同期処理に気をつけないと遅いので大変かもしれません。

Pythonだと行列計算が速い、っていう所には若干ライブラリの差異があるんじゃないかと思いますが、いずれにせよ、機械学習ツールにもNode.jsを使うという新たな使い方の道を示してて面白かった。

「非同期プログラミング養成ギブスとしてのnode.js」by @niryuu

callbackでの非同期プログラミングから、Promiseの話、asyncフロー、yield/generatorの話まで非同期プログラミングの今どきのやり方を説明しつつ、結局どのやり方でも見通しはよくなるものの、例外処理や適切な関数分割は必要で、ちゃんとやらないとどれ使ってもダメ、逆に言えばちゃんと使えば非同期プログラミングの本質が分かる、という話だったかと思います。

これはもうまさにその通りで、どれ使っても非同期プログラミングはこれまでと違うパラダイムなので、同期型のプログラミングと同じ書き方はできないし、それに関してはいろんなツールを知って、適切に使うべき。

callback地獄を▶と表現したのは面白かった。逆くの字の意味だったのねw

「Node.jsなら日曜プログラマーでも簡単に通信対戦ゲームが作れる」 by 竹内さん

謎のテンションですごく面白かったww

socket.ioとnode.jsでHSPのゲームプログラミングのように簡単にゲームが作れるから日曜プログラマ向きで楽しいっていう話。一番会場を爆笑させてたんじゃないかなと思います。

じゃんけんゲームを使ってsocket.ioの基礎的な話とその時どういうことをやってるかを説明しつつ、基本的にはnode.jsでゲームプログラミングが楽しいって話だった。

プレゼンターとしての自虐ネタと姿勢は非常に会場を楽しませてましたw

まとめ

東京Node学園の11時限目を開催して、browserify含めて色んな話を聞けました。

準備や進行に不手際があったかもしれませんが、大きな障害もなく無事終えることができて良かったです。

また4月に実施するのでその時にあいましょう。

あと、毎回niftyさんの会場を借りてたんですが、毎回niftyだとniftyの方々に負担がかかるので、次はDeNAとかの会場借りるのも検討します。

ちなみにスタッフも募集中です、一緒に新しいNode学園を作っていきましょう!!! :)

Maintainable Gruntfile.js

さてさて、前回の続きです。

オレはgruntのエコシステムに乗って楽をしたい、でもGruntfile.jsが長くなりすぎて辛い、grunt taskが時間がかかりすぎて辛い、という話は話で分かります。また、それに対する色んな解決策もあります。

最近出た、HTML5Rocksで紹介されてたやり方もあるし、いくつか先人の知恵もあるので、解決していきましょう。

Gruntfile.jsが長くなりすぎて辛い時

https://github.com/firstandthird/load-grunt-configを使いましょう。

いろんなtipsを見てきましたが、このライブラリが一番分かりやすく、かつGruntfile.jsをメンテナブルに保つことができます。

load-grunt-configには3つの機能があります。

  1. grunt pluginの自動ロード機能
  2. grunt configのファイル分割機能
  3. grunt register task の外出し機能
grunt pluginの自動ロード機能

Gruntfile.jsを書く時、以下の様な感じでgrunt pluginsを記述すると思います。

  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-qunit');
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-contrib-concat');

npmモジュールをロードしてgruntのタスクを読み込んでくれるために必要な箇所なんですが、ここはgrunt-load-configを使うとバッサリと不要になります。

その代わり、load-grunt-configだけをロードしておく必要があります。

つまり

Before

module.exports = function(grunt) {
 
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      options: {
        separator: ';'
      },
      dist: {
        src: ['src/**/*.js'],
        dest: 'dist/<%= pkg.name %>.js'
      }
    },
    uglify: {
      options: {
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n'
      },
      dist: {
        files: {
          'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
        }
      }
    },
    qunit: {
      files: ['test/**/*.html']
    },
    jshint: {
      files: ['gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
      options: {
        // options here to override JSHint defaults
        globals: {
          jQuery: true,
          console: true,
          module: true,
          document: true
        }
      }
    },
    watch: {
      files: ['<%= jshint.files %>'],
      tasks: ['jshint', 'qunit']
    }
  });
 
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-qunit');
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-contrib-concat');
 
  grunt.registerTask('test', ['jshint', 'qunit']);
 
  grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']);
 
};


AFTER

module.exports = function(grunt) {
  // load all grunt tasks!
  require('load-grunt-config')(grunt);

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      options: {
        separator: ';'
      },
      dist: {
        src: ['src/**/*.js'],
        dest: 'dist/<%= pkg.name %>.js'
      }
    },
    uglify: {
      options: {
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n'
      },
      dist: {
        files: {
          'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
        }
      }
    },
    qunit: {
      files: ['test/**/*.html']
    },
    jshint: {
      files: ['gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
      options: {
        // options here to override JSHint defaults
        globals: {
          jQuery: true,
          console: true,
          module: true,
          document: true
        }
      }
    },
    watch: {
      files: ['<%= jshint.files %>'],
      tasks: ['jshint', 'qunit']
    }
  });
 
  grunt.registerTask('test', ['jshint', 'qunit']);
 
  grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']);
 
};

grunt libraryの読み込みが不要になってスッキリしましたね。
ここまでなら load-grunt-tasks でも同じことができます。

Gruntfile.jsのタスクを記述する際に load-grunt-tasks プラグインが地味に便利 | 5 LOG
プラグイン毎にgrunt.loadNpmTasks()を追加する必要が無くなるload-grunt-tasksを紹介するよ - Qiita

でもまだまだ長いですよね...

grunt configのファイル分割機能

これ使えばgrunt.initConfigを別ファイルに分割できます。
grunt.initConfigで記述していた箇所を削除して、grunt/xxx.jsに移動させます、つまり、

BEFORE

module.exports = function(grunt) {
  // load all grunt tasks!
  require('load-grunt-config')(grunt);

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      options: {
        separator: ';'
      },
      dist: {
        src: ['src/**/*.js'],
        dest: 'dist/<%= pkg.name %>.js'
      }
    },
    uglify: {
      options: {
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n'
      },
      dist: {
        files: {
          'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
        }
      }
    },
    qunit: {
      files: ['test/**/*.html']
    },
    jshint: {
      files: ['gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
      options: {
        // options here to override JSHint defaults
        globals: {
          jQuery: true,
          console: true,
          module: true,
          document: true
        }
      }
    },
    watch: {
      files: ['<%= jshint.files %>'],
      tasks: ['jshint', 'qunit']
    }
  });
 
  grunt.registerTask('test', ['jshint', 'qunit']);
 
  grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']);
 
};

AFTER

Gruntfile.jsは以下のようになります。

module.exports = function(grunt) {
  // load all grunt tasks!
  require('load-grunt-config')(grunt);

  grunt.registerTask('test', ['jshint', 'qunit']);
 
  grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']);
 
};

その代わり、gruntフォルダ以下に下記のような設定ファイルを記述します。

grunt/uglify.js

module.exports = {
  options: {
    banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n'
  },
  dist: {
    files: {
      'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
    }
  }
};

grunt/concat.js

module.exports = {
  options: {
    separator: ';'
  },
  dist: {
    src: ['src/**/*.js'],
    dest: 'dist/<%= pkg.name %>.js'
  }
}

grunt/qunit.js

module.exports = {
      files: ['test/**/*.html']
};


こんな感じで。

そうするとgruntフォルダ以下にこんな感じで並びます。

grunt
├── uglify.js
├── concat.js
├── qunit.js
・・・

見通しが良くなりましたね。

grunt register task の外出し機能

外出し機能はここまでやらなくてもいい気がしますが、一応あります。
これを使うとGruntfile.jsは3行になります。

BEFORE

module.exports = function(grunt) {
  // load all grunt tasks!
  require('load-grunt-config')(grunt);

  grunt.registerTask('test', ['jshint', 'qunit']);
 
  grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']);
 
};

AFTER

module.exports = function(grunt) {
  // load all grunt tasks!
  require('load-grunt-config')(grunt);
};

んで、grunt-registerTaskもgruntフォルダ以下にaliases.jsとして登録します。


grunt/aliases.js

module.exports = {
  test : ['jshint', 'qunit'],
  default : ['jshint', 'qunit', 'concat', 'uglify']
};

※もしも中でどうしてもgruntを使いたい場合(gruntのオプションで読み込むタスクが違うなど)

grunt/aliases.js

var grunt = require('grunt');

module.exports = {
  default: function(target) {
    if (target === 'force') {
      return grunt.task.run(['concat', 'uglify']);
    }
    return grunt.task.run(['jshint', 'qunit', 'concat', 'uglify']);
  }
};


これで、Gruntfile.jsが短くなってメンテナンスしやすくなりましたね。

Grunt タスクが遅くて辛い時

まずは時間を測りましょう。どんなときも計測が重要です。
その上でチューニング手法を学びましょう。

時間を計測する

time-gruntを使いましょう。

f:id:yosuke_furukawa:20140222024838p:plain

こんな感じに時間を計測してくれるプラグインです。

module.exports = function(grunt) {
  // show elapsed time at the end
  require('time-grunt')(grunt);
  // load all grunt tasks!
  require('load-grunt-config')(grunt);
};

こんな感じで指定します。

これだけで時間がかかっているタスクがわかります。

意味のないタスクを実行しない

さて、時間がかかっていることが分かったらタスクをダイエットさせていきましょう。

まず、gruntで何度もタスクを実行することってあると思います。その際、サブタスクの実行結果に変化がない奴は一度やれば十分です。
例えば、gruntでimageのminimizeとjavascriptのtestを実施するタスクがある場合、javascriptのファイルに変更があればtestの結果は変わりますが、imageの方は変更がないので実施する必要はありません。

こんな感じで前回と実行元のファイルが変わってないのであれば、実行せずにキャッシュを返すというタスクがgrunt-newer です。


使い方は超簡単で、タスクの先頭に"newer"を付けるだけで実現できます。

grunt.initConfig({
    jshint: {
      options: {
        jshintrc: '.jshintrc'
      },
      all: {
        src: 'src/**/*.js'
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-newer');

  // newerを付けるだけ
  grunt.registerTask('lint', ['newer:jshint:all']);
時間がかかるタスクを並列実行したい時

grunt-concurrentgrunt-parallelizeの二通りの方法があります。

grunt-concurrentはサブタスクを子プロセスを使って並列実行するタスクです。

grunt-parallelizeは一つのタスクで対象のファイルを分割して、子プロセスを使って並列実行するタスクです。

grunt-concurrentを使う

画像の減色処理とcssやjsの圧縮といろいろ時間がかかるタスクを普通にgruntで実行すると直列で実行されるので、時間がかかります。

時間がかかる処理があるのであれば、それを実行している間に別なタスクを実行させて並列化しましょう。

grunt.initConfig({
    concurrent: {
        target1: ['coffee', 'sass'],
        target2: ['jshint', 'mocha']
    }
});

grunt.loadNpmTasks('grunt-concurrent');
grunt.registerTask('default', ['concurrent:target1', 'concurrent:target2']);

こんな感じで使います。こうすると、target1で指定したcoffee, sassタスクとtarget2で指定したjshint, mochaタスクを並行実行できます。


grunt-parallelize

一個のタスクを並行実行したいときに使います。
jshintを使いたいけど、srcファイルが多すぎて時間がかかる、full-test流したいけど、testファイルが多すぎるとかですね。

grunt.initConfig({
  jshint: {
    all: {
      src: './**/*.js'
    }
  },
  parallelize: {
    jshint: {
      // Run jshint:all task with 4 child processes in parallel.
      all: 4
    },
  },
});


grunt.registerTask('default', ['parallelize:jshint:all']);

こうするとjshint:allのsrcのファイル群を2つに分割して並列実行してくれます。(teppeis++)


今回使ったプラグインの使い方はココにまとめました。

https://github.com/yosuke-furukawa/modern-grunt-sample