java-ja.js #2 RequireJS実践編

java-ja.js #2 : ATND

自己紹介

文殊堂といいます。

BePROUDという会社のリーダーです

↑はロケタッチの話です(ロケタッチ知らない人は4SQでMayorになったみたいな話だと思っといてください)。

弊社では今(2010年12月現在)人材募集中らしいですよ。


今の仕事はUIがリッチな業務システムを作るとかそんな感じで、
そこでRequireJSを使っています。

RequireJSって何?

公式サイト
RequireJS
スライド
jQueryRequireJS.pdf
日本語記事だとこの辺?
http://zudolab.net/blog/?p=451


要はJavaScriptの依存性解決をしてくれるライブラリです。
以前RequireJS moduleについてって記事書きました。

あらためてRequireJS moduleについて

まず最初に、一般的な言語と同じような形で再利用可能なコードを書く場合、
モジュールの頭で他モジュール読み込み(importとかincludeとかuseとかそういうの)をすると、
再帰的に読み込んでいくというのをやりたい場合、
ライブラリの名前に反してrequire関数ではなくdefine関数を使います。
これによって定義されるのがmoduleです。
require関数はmain関数に相当するものを書くためのものだと覚えておきましょう。

// hoge.js
define(function(){
  console.log('hoge');
});
// fuga.js
define(['hoge'],function(){
  console.log('fuga');
});
// piyo.js
define(['fuga'],function(){
  console.log('piyo');
});
// main.js
require(['piyo']);

これを読み込むとhoge,fuga,piyoの順に出力される。
defineでなくrequireだと逆の順番になる。

様々なdefine

define([
  'dependent-module1'
  ,'dependent-module2'
],function(module1,module2){
  /* 何らかの処理 */
  return {
    bar : 6
    ,baz : 7
  }
});

基本的な書き方だと第一引数に依存しているmodule名の配列を渡し、
第二引数の関数内に該当moduleの初期化処理を書いていき、
returnした値がmoduleとして使われます。

依存module名の省略

依存しているmoduleがない場合は第一引数に空配列を取ることも、
以下のようにいきなりcallbackを書いてしまうことも出来ます。

define(function(){
  /* 何らかの処理 */
  return {
    bar : 6
    ,baz : 7
  }
});
callbackの省略

特に前処理も必要ないなら、define関数に直接オブジェクトを渡して、
moduleを定義してしまうことも出来ます。

define({
    bar : 6
    ,baz : 7
  });

ただし、オブジェクトを渡したつもりで関数を渡していたりすると、
その関数の戻り値がmoduleとして定義されてしまうので気をつけましょう。
私は一度UnderscoreJSをmodule化するときに以下のようにしてしまいハマりました。

define(_);
module名の明示的な設定

通常、moduleが定義されたJavaScriptファイルのpathから拡張子を除いた物が、
module名になりますがdefine関数の第一引数に文字列を指定することで、
明示的にmodule名を設定することが出来ます。
例えば、jQuery同梱版のRequireJSには以下のようなコードがあります。

	define('jquery',[], function() { return jQuery });

この場合、module名からファイルpathをたどって自動的に読んでくれるということはないので、
RequireJSからではなくscriptタグから必ず読み込むmoduleについてのみやるようにしましょう。

moduleの用途

関数

関数を返すmoduleを作っておけば、好きなところでその関数を使うことが出来ます。

define('log',function(){
  return function(str){
    console.log(str);
  };
});
require(['log'],function(log){
  log('hogehoge'); // consoleにhogehogeと出力される
});
class

コンストラクタを返すmoduleを作っておけば、好きなところでそのclassを使うことが出来ます。

define('bucho',function(){
  function Bucho(){
  }
  Bucho.prototype.show = function(){
    console.log('chinko');
  };
  return Bucho;
});
require(['bucho'],function(Bucho){
  var bucho = new Bucho();
  bucho.show(); // consoleにchinkoと出力される
});
副作用

なにも返さないmoduleにも有用なものがたくさんあります。

  • jQuery plugin
    • jQuery pluginをmodule化すると何も返しませんが、requireやdefineで依存moduleに指定している箇所では読み込まれていることの保証ができます
  • live event等の設定
    • live eventの設定をしているコードをmodule化すると、moduleを読むことで特定のclassが振られた要素に振る舞いを付加できるようになります。
      • 今私がやってる仕事では、ある画面では一覧表示している情報それぞれについてのCRUDそれぞれのダイアログをクリックで表示するが、ある画面では詳細ダイアログしか出さないといった事をmoduleの読み込みで切り替えています
  • モンキーパッチ
    • moduleで定義されたclassのprototypeをcallback内で書き換えます。
      • 混乱するので多用はしないように
ただの連想配列

例えばプログラム中で使うアイコンのURLを名前とマッピングしたものなどが有用です。

設定

RequireJS本体読み込み前にrequireという名前のグローバル変数があると、
RequireJSはそれを設定として使用してくれます。
共通的な設定を書くファイルと環境ごとの違いを上書きするファイルを分けておくとよいでしょう。

<script src="config-require.js?1291034301807"></script>
<script src="config-require-override.js"></script>
<script src="allplugins-require.js?2010091401"></script>
baseUrl

通常RequireJSのmodule名はpathから拡張子を除いたものになりますが、
そのpathの基底階層になるのがbaseUrlです。
私は以下のようにconfig-require.jsに記述して、必ずconfig-require.jsがある階層が基底になるようにしています。

  var scripts = document.getElementsByTagName('script');
  var currentScript = scripts[scripts.length - 1];
  require['baseUrl'] = currentScript.src.replace(/\/[^/]*$/, '/');
paths

通常RequireJSのmodule名はpathから拡張子を除いたものになりますが、
その一部または全部を置き換えることができます。
主に外部ライブラリのpathを短縮する場合などに使います。
以下のようにすれば、jQueryUIの各pluginは'jqueryui/jquery.ui.dialog'といった名前で記述でき、
jquery.notify pluginは'jquery.notify'という名前で記述できます。

require['paths'] = {
  jqueryui : 'require-js-modules/jquery-ui-1.8.6'
  ,'jquery.notify' : 'require-js-modules/monjudoh-jquery-notify-f589e66/jquery.notify'
}


また、config-require-override.jsの中で、
XHRでAPIを叩く自作moduleを、
同じインターフェイスでTimerで非同期にしたstub moduleと置き換えることで、
サーバサイド実装と独立して開発をすすめるなどしました。

require.paths['app/api/hoge-api'] = 'app/api/hoge-api-timer';
urlArgs

scriptタグでJavaScriptを読み込む時にクエリパラメータにデプロイ日時等を含めることで、
キャッシュされた古いJavaScriptを読み込まないようにするというのはよくやると思います。
↓こんな感じです。

  <script src="{{ MEDIA_URL }}js/config-require.js?{{ media_update }}"></script>

RequireJSで動的にJavaScriptを読み込む場合それが効かないのではないかと心配になるかもしれません。


これはurlArgsで解決できます。
RequireJSはmodule読み込みの際に、
require['urlArgs']に設定されている文字列をクエリパラメータとして付加します。
config-require.jsに以下のように記述し、
config-require.jsをscriptタグで読み込むときに適当なクエリパラメータを
付ける仕組みを用意しておけば(フレームワークにありますよね?)、
全部勝手に引き継いでくれます。

  var scripts = document.getElementsByTagName('script');
  var currentScript = scripts[scripts.length - 1];
  var urlArgs = (currentScript.src.match(/(?:\?)(.+)$/) || [])[1];
  if (urlArgs) {
    require['urlArgs'] = urlArgs;
  }
deps

共通で読み込まれるライブラリ(RequireJSのmoduleでなくてもいいです)を設定します。

(function() {
  var global = this;
  require['deps'] = [];
  require['deps'].push('es5-shim' /* http://github.com/kriskowal/es5-shim/raw/master/es5-shim.js */);
  if( !global.JSON ){
    require['deps'].push('json');
  }
  require['deps'].push('fix-require-js-bug');
})();

とりあえずこの辺を読み込んでECMAScript5のメソッドが使えるようにしています。

外部ライブラリのmodule化

これについては私の知る限り現在きれいなやり方がありません。
副作用が期待され、依存対象が必ず読み込まれているようなライブラリ(jQuery plugin等に多い)は別にそのままで問題ありません。
値を返す必要があったり、依存関係の連鎖があったりする場合は、
格好悪いですが、ライブラリのコードの全体をdefineで括ってます。

define(['jqueryui/jquery.ui.widget','jqueryui/jquery.ui.position'],function(){
/* jQuery UI Dialogのコード本体 */
});

text plugin

require.js with pluginsを使う場合様々なpluginを使うことができます。
text pluginはmodule名の前にtext!と付けるとファイルの中身を文字列として取得することができます。


部分HTMLを外部ファイルに記述しておき、それを使ってダイアログを表示する
等といった使い方をしました。

define(['text!html/hoge.html'],function(hoge) {
  $('.show-dialog').live('click',function(ev){
    ev.preventDefault();
    if ( !$.isReady ) {
      return;
    }
    $(html).dialog();
  })
});

パフォーマンスとか

非同期読み込みはHTML5のscriptタグasync属性で実現している模様ですが、
そもそもasync属性に対応しているブラウザはまだ少ないです。
参考:defer/asyncチェックツールを作りブラウザ対応状況を確認してみた | ゆっくりと…


build toolはあるようですがまだ試していません。

循環依存

なるとハマるので、依存moduleの存在チェックレベルのassertionは書いておきましょう。
RequireJS内でassertionを書きたい場合はこれを使うとよいでしょう。
debug-assert-RequireJS-moduleなるものを作りますた - 文殊堂

まとめ

そんなものはない