Hatena::ブログ(Diary)

文殊堂 このページをアンテナに追加 RSSフィード Twitter

2012-11-28

r.jsでビルドする際に設定として使うbuild.jsを動的にする。さらに部品化する。

r.js

RequireJS用のmoduleをビルドする際にはr.jsを使います。

build.js

この時にビルド設定の記述に使用するのがbuild.jsこんな感じで書きます。

簡単な例だとこんなの

({
  baseUrl:'./src',
  dir : './build',
  paths:{
    jquery:"../lib/jquery-1.8.2",
    text:'../lib/plugins/text',
    underscore:'../lib/underscore-1.3.3'
  }
})

動的なbuild.js

build.jsJSONではなくてJSです。

JSなので処理を書くことが出来ます。

例えば関数その場実行を含む下記のbuild.jsは、上記のものと同じ設定を表します。

({
  baseUrl:'./src',
  dir : './build',
  paths:(function () {
    var paths = {
      jquery:"../lib/jquery-1.8.2",
      text:'../lib/plugins/text',
      underscore:'../lib/underscore-1.3.3'
    };
    return paths;
  })()
})

ちなみに全体を関数その場実行で包むことは出来ません。

以下のコードはbuild.jsとしては使えません。

(function () {
  return ({
    baseUrl:'./src',
    dir : './build',
    paths:{
      jquery:"../lib/jquery-1.8.2",
      text:'../lib/plugins/text',
      underscore:'../lib/underscore-1.3.3'
    }
  });
})();

さて、現状ではbuild.jsの中に処理を書けるようになったものの、何を以て設定を変更するかが分からないので、動的というほどではなくてあまり嬉しくありません。

r.jsNode.jsで実行されているので、process.argvコマンドライン引数を参照することも出来ます。

r.js -o build.js optimize='none' os='ios5'
({
  baseUrl:'./src',
  dir : './build',
  paths:(function () {
    var paths = {
      jquery:"../lib/jquery-1.8.2",
      text:'../lib/plugins/text',
      underscore:'../lib/underscore-1.3.3'
    };
    var os = (process.argv.filter(function (arg) {
      return arg.match(/^os=/);
    })[0] || '').replace(/^os=/,'');
    if (os === 'ios5') {
      // 何らかの処理
    }
    return paths;
  })()
})

これでコマンドラインオプションによってビルド時の設定を細かく変えることが出来るようになりました。

build.jsNode.js module化

繰り返しますが全体を関数その場実行で包むことは出来ません。

objectのvalueしか関数その場実行で包めません。

つまり、処理を関数として共通化したくても、それを置いておく場所がありません。

例えば各項目でos optionによる振り分けを行いたいなら、各項目のvalueの関数その場実行の中に以下を入れる必要があります。

    var os = (process.argv.filter(function (arg) {
      return arg.match(/^os=/);
    })[0] || '').replace(/^os=/,'');

これではちょっとやりたいことが増えればすぐに肥大化してしまいます。

さて、繰り返しますがr.jsNode.jsで実行されています。共通化したい処理はNode.jsのmoduleにしてしまいましょう。

以下のような内容のoptions.jsを作ります。

var os = (process.argv.filter(function (arg) {
  return arg.match(/^os=/);
})[0] || '').replace(/^os=/,'');
module.exports = function options(){
  return {
    os:os
  }
};

これを使う際の注意点は二つあります。

  • Node.jsのmodule読み込み関数requireはr.jsではrequire.jsのそれで上書きされています。
    • nodeRequireが元々のrequire関数なのでこれを使います。
  • Node.jsで実行されているのはr.jsなので、build.jsからの相対pathでmoduleを読み込もうとしても無駄です。
    • process.env.PWDを使ってr.jsを実行したときのカレントディレクトリ(build.jsはここにある前提)を参照しましょう。

こんな感じでもりもりbuild.jsを動的にしていくことが出来ます。

複数環境向けビルドをしたいとか行った場合に捗りますね。

({
  baseUrl:'./src',
  dir : './build',
  paths:(function () {
    var paths = {
      jquery:"../lib/jquery-1.8.2",
      text:'../lib/plugins/text',
      underscore:'../lib/underscore-1.3.3'
    };
    var options = nodeRequire(process.env.PWD+'/build-tools/options')();
    var os = options['os'];
    if (os === 'ios5') {
      // 何らかの処理
    }
    return paths;
  })()
})

2012-10-24

Require JSで設定をmodule外に出して、それをビルド時に同梱する

モジュール別の設定を読み込む

RequireJSではモジュールの名前として"module"が予約されていて、モジュール内でmoduleモジュールを取得するとそのモジュールの情報を参照することが出来ます。

define('foo',['module'], function (module) {
});

RequireJS API

config methodで設定できる項目の中のconfigにモジュール名をkey何らかのオブジェクトをvalueという辞書を設定しておくと、

moduleモジュールのconfig methodでそのオブジェクトを取得することが出来ます。

今回はmodule moduleとかconfig configとか見てて混乱するのが多いですね。

こんな設定をすると

requirejs.config({
    config: {
        'baz': {
            color: 'blue'
        }
    }
});

こんなふうに取れます。

define('baz',['module'], function (module) {
    //Will be the value 'blue'
    var color = module.config().color;
});

こういった設定をr.jsでのビルド時に同梱してrequirejs.config/require.configでの設定と一緒に使われるデフォルト値のような形にすることは出来ないようです。

設定込みのビルドというのは出来ないわけです。

ビルド時に設定を同梱する

同梱出来ない設定をどうやって同梱するか?

設定を行うJavaScriptのコードを同梱してしまえば良いのです。

RequireJS2.1からはビルド後のmodule全体を囲むwrapの設定で複数ファイルを指定することが出来るようになっています。

r.js/build/example.build.js at master ? jrburke/r.js ? GitHub

wrap.startFileの2つ目以降に設定を行うJavaScriptのファイルを入れれば同梱出来ます。

環境別ビルド等でどんどん活用していきましょう。

2012-09-19

CommonJS AMDとDeferred

Writing Modular JavaScript With AMD, CommonJS & ES Harmony のModules With Deferred Dependenciesが便利なので活用してる。

初期化処理が非同期処理でrequireしてきても即使えるとは限らない場合に使う。

モジュール側ではモジュールそのものではなくてpromiseを返しておいて、モジュールの実体が完成したらresolveで渡す。

使う側はrequireしてきたpromiseのthenメソッドのcallbackでモジュールの実体を受け取って使う。

// 何らかの非同期処理を経て初期化されるモジュール
define('someModule',['jquery'],function($){
  var dfd = $.Deferred();
  setTimeout(function(){
    // モジュールとして実際使いたいobject
    var module = {a:1};
    dfd.resolve(module);
  },500);
  return dfd.promise();
});
// モジュールを使う側
require(['someModule'],function(someModule){
  someModule.then(function(module){
    console.log(module); // {a:1}
  });
});

こういったモジュール複数に依存している場合は

require(['someModule1','someModule2','someModule3'],function(someModule1,someModule2,someModule3){
  $.when(someModule1,someModule2,someModule3).then(function(module1,module2,module3){
    console.log(module1,module2,module3);
  });
});

ね、簡単でしょう?

2012-09-07

JavaScriptでコマンドを作って実行する

前置き

Firebug1.10にhelpコマンドなるものが入ってました。

consoleでhelpって入力して実行するとFirebugで使える関数等が表示されます。

help();じゃないんですよ。

関数じゃなくてコマンド。

どうやって実現しているんだろうかって気になったのでエスパーして似たようなのを作ってみました。

Firebugのソースは読んでないけど多分おんなじようなことをしているはず。

コード

以下、with文の中でcommandって書いて実行するとcommand is executed.って出力されます。

var obj={};
Object.defineProperty(obj,'command',{
  get : function() {
    console.log('command is executed.');
  }
});
with (obj) {
  command // command is executed.
}

解説

Object.definePropertyでgetアクセサのあるpropertyを定義すれば、getアクセサの関数関数メソッド呼び出しではなくproperty参照で実行できます。

さらにwith文でそのpropertyを持ったobjectを指定することで、.や[]なしでproperty参照できるので、コマンドのように実行することが出来るようになります。

Firebugでは少なくとも以前は入力した内容をwith文の中でevalすることでFirebug専用の関数等を使えるようにしていたので、helpコマンドもおそらく同じです。

2012-09-04

JavaScriptでのbuilt-in/DOM objectのprototype拡張

@rosylillyが気にしていた

のでまとめた。

built-in/DOM objectのprototype拡張による弊害

追加したプロパティ/メソッドがfor inで列挙される
var obj = {a:1};
for (var i in obj) {
  console.log(i);
}

こうするとaだけ出るはずが、

Object.prototype.b=function(){};

こうした後だとa,bが出てしまうって奴ですね。

そのまま代入しないでObject.defineProperty/definePropertiesでenumerable:falseのプロパティとして定義すれば列挙されなくなるので特に問題ありません。

今回挙げるprototype拡張の弊害の内唯一これだけはECMAScript5時代になって解消されました。唯一これだけは。

built-in/DOM objectの新しい仕様で追加されたプロパティ/メソッドバッティング

JSどうこうというかどの言語のmonkey patchでも起こりうる問題。

よく見かけた例

JSON.stringify({
  b:{
    toJSON:function(){return 1;}
  }
}); // '{"b":1}'

ECMAScript6でも便利メソッドは追加されるので今後も似たような状況にはなりうるので、prototype拡張として便利メソッドを実装するのはお勧めしません。

代案としてはやはりラッパーオブジェクト系のライブラリ関数群を提供するライブラリが良いと思います。


ラッパーオブジェクト系のライブラリ

正直Underscore.jsもそろそろラッパーオブジェクト/メソッドチェーンのレイヤと便利メソッドを生やすレイヤを分けてUnderscore.js的なライブラリを作る土壌になったほうが良いのでは?などと。

同じようにbuilt-in/DOM objectのprototype拡張をしている他ライブラリとのバッティング

これもJSどうこうというかどの言語のmonkey patchでも起こりうる問題。

バッティングが起こらないように気を付けろというのは対策になりません。

手動でscriptタグを並べていた時代ならいざしらず今はCommonJS AMD再帰的に依存関係を辿る真っ当なモジュールシステムがある時代なので、依存関係グラフの途中のどこかとどこかのモジュールバッティングしているとか目も当てられません。

積極的なmonkey patchが忌避されるのはJavaScriptに限った話ではないということを忘れるべきではありません。

monkey patchでのバッティングの対策は以下のようなものだと思います

JavaScriptでは上記3対策のうち事実上prefixを付けるくらいしか有効ではないので、便利メソッドを無邪気に生やすのは考えものです。

Re:prototypeを拡張することで得られるもの。prototype拡張指向へのスイッチ - latest log

prototypeを拡張することで得られるもの。prototype拡張指向へのスイッチ - latest log

ついでにこのエントリに言及しておくと、このエントリでやってるのは典型的な藁人形メソッドです。


prototype拡張を使わない冗長なコードとprototype拡張を使った自作ライブラリを使った簡潔なコードを比較していますが、冗長なのはprototype拡張を使っていないからではありません。

冗長な使い捨てコードと簡潔なAPIライブラリを比較しているだけです。


Number#to(prototype拡張あり)とUnderscore.js+ECMAScript5標準(prototype拡張なし)を比較してみましょう。

prototype拡張によって越えた壁はどこかにありますか?

function isX5(n) { // Multiples of 5 filter
  return n % 5 === 0;
}

1..to(100);       // -> [1, 2 .. 99, 100]
100..to(1);       // -> [100, 99 .. 2, 1]
1..to(100, 2);    // -> [1, 3 .. 97, 99] (skip 2)
1..to(100, isX5); // -> [5, 10, .. 95, 100] (filter)
function isX5(n) { // Multiples of 5 filter
  return n % 5 === 0;
}

_.range(1,100+1); // [1, 2 .. 99, 100]
_.range(1,100+1).reverse(); // [100, 99 .. 2, 1]
_.range(1,100+1,2); // [1, 3 .. 97, 99] (skip 2)
_.range(1,100+1).filter(isX5); // [5, 10, .. 95, 100] (filter)

built-in/DOM objectのprototype拡張をしてよさそうな箇所

最後にprototype拡張を活用できる・できそうな箇所についても言及しておきます。

shim/pollyfill

標準仕様に入ったAPIを未実装の環境に提供するのは何も問題ありません。バッティングして問題が発生するなら相手のほうが悪いです。どんどんやればいいし、そういうライブラリを提供している人はたくさんいます。

internalプロパティ/メソッド

APIとして公開するものでなくライブラリの中でのみ使うプロパティ/メソッドをまず他とバッティングしないような長い名前で追加するのは問題ありません。もちろんenumerableはfalseにする。例えばmonjudoh/BeautifulProperties.js ? GitHubでObject.prototypeに対してEvents.onしたら'BeautifulProperties::internalObjectKey'というkeyプロパティが追加されるがこれはバッティングしないでしょう。

小規模なWebWorker内

この場合DOM objectのprototype拡張はそもそもありません。Worker間・Worker/window間でbuilt-in objectのprototypeは共有されないので、Worker内での拡張が他に影響することはありません。個々のWorkerのscriptのサイズをprototype拡張の影響を充分に追いきれる規模に止めれば良いでしょう。