Mercurialで別オリジンのリポジトリ間の同期を取る運用の仕方について

前説

DjangoでWebアプリを開発していて自分はJavaScript部分を担当しています。
で、サーバ側を動かさないと開発できないのは辛いので、
サーバサイドで生成されたHTMLとJSONwgetして、js/dummy/とかに配置し、
各種URLの参照を書き換えてローカルでJavaScriptだけもりもり実装できるようにしました。


サーバサイドの実装に先行してJavaScriptの実装を進めていたため、
プロジェクト全体(django-apps)と独立してバージョン管理したかったため、
static配下だけが入ったdjango-staticというリポジトリを作成し、
そちらで開発を進めていきました。

今回の話は、プロジェクト全体(django-apps)とJavaScript&CSSだけ(django-static)の
二つのリポジトリの同期をとって運用していく話です。

準備

hg clone django-apps
hg pull django-static

とやると
「abort: repository is unrelated」と出てpullに失敗してしまいます。

hg pull -f django-static

とやればpull出来るので、2つのオリジンを持つリポジトリが出来上がります。
それぞれのdefaultのheadにdjango-apps-head,django-static-headというlocal tagを打って、
今後は随時移動していくことにしましょう。

同期をとる方法

merge

django-apps由来のheadとdjango-static由来のheadでmergeすることはできます。
これで成果の共有はできますが、もう二度と分離することができません。

revert

同期したいのは特定のディレクトリ(今回はstatic)だけなのでrevertを使うのもありです。

hg update django-apps-head
hg revert static/ -r django-static-head
hg commit -m "message"

とやればdjango-static-headの成果を持ってくることができます。
ただし、細かい履歴が消えてしまう、django-apps側に変更があっても上書きされてしまうなどの難点があります。

transplant

hg transplantは任意のchangesetを任意の位置に移植できます。
なのでサブセットの方から更新分のchangesetsを移植するのは簡単です。

hg pull django-static
hg update django-apps-head
hg transplant pullしてきた中で一番古いrevision番号:tip

とかやれば、簡単に同期できます。


後はdjango-appsにそのままpushしてもよし、
人に見せる用に適度にcommit圧縮してからpushするもよしです。
重要なのはstatic配下が同じ状態のそれぞれのrevisionがどこか記録しておくことです。
django-apps-head,django-static-headを適切に移動しておきましょう。


django-apps→django-staticの方向の同期はやや難しいです。
というのも、static配下の変更を含むchangesetがstatic配下以外への変更を含まないとは限らないからです。
Djangoの場合だとtemplates配下とセットで変更されていることが多いです。


例えばstatic/js/hoge.jsとtemplates/index.htmlが変更されているとして、
そのchangesetをdjango-staticの方にtransplantすると、
static/js/hoge.jsの差分は適用できますが、templates/index.htmlの差分を適用できずに、
transplantは中断してしまいます。
templates/index.htmlの適用できなかった差分はtemplates/index.html.rejとして保存されます。


本来のtransplantのフローでは適用できなかった差分を手動で適用し、
rejファイルを削除し、

hg transplant --continue

とやってtransplantを完了させます。


今回はstatic配下の変更以外は全く要らないので、こんな感じでいいでしょう。

hg transplant static配下の変更を含むrevision
find . -name "*.rej" | grep -v -P '\./static' | xargs rm 
hg transplant --continue

対象になるchangesetsを全部移植し終えたらlocal tagを移動した上で、
static配下が同一であるか確認しましょう。
ファイル内容が同一なだけなので、hg statusではダメです。

hg diff static/ -r django-apps-head:django-static-head

実際の運用

  1. django-apps(共同で使っているリポジトリ)から同期用リポジトリにpull
  2. django-static-headへtransplant
  3. django-static(個人で開発時に使っているリポジトリ)へpush
  4. django-staticでrebase
  5. django-staticから同期用リポジトリにpull
  6. django-apps-headへtransplant
    • (必要なら)transplantした分をcommit圧縮
  7. django-appsにpush

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なるものを作りますた - 文殊堂

まとめ

そんなものはない

IEではRequireJS+jQueryで$(document).ready()で設定したcallbackが実行されないことがある&その対応

とりあえずRequireJS0.14.5+jQuery1.4.3,RequireJS0.15.0+jQuery1.4.4で発生。
IEは6~8っぽい。

なんで気づいたかというと

  1. IEでjQueryUI等の動作がおかしくなった
  2. CSS Box Modelがサポートされている状態なのに$.boxModel(=$.support.boxModel)がtrueでない
    • falseですらなくundefinedだった
  3. $.boxModelの検出はjQuery(function(){})内で行われていた。
    • $(document).ready()と同じ

検証

対象:RequireJS0.15.0+jQuery1.4.4
環境:IE7(+CompanionJS)
今、仕事で作ってるアプリで使っているallplugins-require.jsとjquery-1.4.4.jsに
以下のようにconsole.logを追加した。
後、仕事で作っているコードの中のrequire.ready callbackの頭に、
console.log('require.ready');というlog出力を追加した。


jQueryについてはjQuery.readyの中でlog出力し、
後はtimerやevent経由でjQuery.readyを呼んでいる箇所について、
どこで設定されたものが呼ばれたか分かるようにlog出力した。


$(document).ready()で設定されたcallbackは、
jQuery.readyによって実行されるが、準備が出来ていないと判断される場合は実行されない。
jQuery.readyWaitが0でjQuery.isReadyがtrueの場合が準備できている状態である。
そして、RequireJSは内部でjQuery.readyWaitを増減するので、
その箇所でlog出力をするようにした。

diff -r 4ba54b5cad32 static/js/jquery-1.4.4.js
--- a/static/js/jquery-1.4.4.js	Wed Nov 17 15:35:04 2010 +0900
+++ b/static/js/jquery-1.4.4.js	Wed Nov 17 20:29:59 2010 +0900
@@ -413,6 +413,7 @@
 	
 	// Handle when the DOM is ready
 	ready: function( wait ) {
+    console.log('jQuery.ready:'+jQuery.readyWait+','+jQuery.isReady);
 		// A third-party is pushing the ready event forwards
 		if ( wait === true ) {
 			jQuery.readyWait--;
@@ -454,7 +455,7 @@
 			}
 		}
 	},
-	
+
 	bindReady: function() {
 		if ( readyBound ) {
 			return;
@@ -466,7 +467,10 @@
 		// browser event has already occurred.
 		if ( document.readyState === "complete" ) {
 			// Handle it asynchronously to allow scripts the opportunity to delay ready
-			return setTimeout( jQuery.ready, 1 );
+			return setTimeout( function(){
+        console.log('jQuery.ready document.readyState === "complete"');
+        jQuery.ready();
+      }, 1 );
 		}
 
 		// Mozilla, Opera and webkit nightlies currently support this event
@@ -475,7 +479,10 @@
 			document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
 			
 			// A fallback to window.onload, that will always work
-			window.addEventListener( "load", jQuery.ready, false );
+			window.addEventListener( "load", function(){
+        console.log('jQuery.ready window.onload');
+        jQuery.ready();
+      }, false );
 
 		// If IE event model is used
 		} else if ( document.attachEvent ) {
@@ -484,7 +491,10 @@
 			document.attachEvent("onreadystatechange", DOMContentLoaded);
 			
 			// A fallback to window.onload, that will always work
-			window.attachEvent( "onload", jQuery.ready );
+			window.attachEvent( "onload", function(){
+        console.log('jQuery.ready() window.onload');
+        jQuery.ready();
+      } );
 
 			// If IE and not a frame
 			// continually check to see if the document is ready
@@ -901,6 +911,7 @@
 	}
 
 	// and execute any waiting functions
+  console.log('jQuery.ready() doScrollCheck');
 	jQuery.ready();
 }
diff -r 4ba54b5cad32 static/js/allplugins-require.js
--- a/static/js/allplugins-require.js	Wed Nov 17 15:35:04 2010 +0900
+++ b/static/js/allplugins-require.js	Wed Nov 17 20:29:34 2010 +0900
@@ -704,6 +704,7 @@
                 if (context.scriptCount) {
                     $.readyWait += 1;
                     context.jQueryIncremented = true;
+                    console.log('jQueryCheck :' + $.readyWait + ',' + $.isReady); 
                 }
             }
         }
@@ -953,6 +954,7 @@
                 if (context.jQuery && !context.jQueryIncremented) {
                     context.jQuery.readyWait += 1;
                     context.jQueryIncremented = true;
+                    console.log('req.load :' + $.readyWait + ',' + $.isReady);
                 }
             }
         }
@@ -1679,6 +1681,7 @@
                     if (context.jQueryIncremented) {
                         context.jQuery.readyWait -= 1;
                         context.jQueryIncremented = false;
+                        console.log('req.callReady :' + $.readyWait + ',' + $.isReady);
                     }
                 }
             }

結果

Firefox3.6+Firebug1.5の場合
jQueryCheck :2,false
jQuery.ready:2,false
require.ready
req.callReady :0,true
jQuery.ready window.onload
jQuery.ready:0,true
  1. jQueryCheckが呼ばれてreadyStateがインクリメントされる
  2. (中略)
  3. require.ready callbackを呼び出し終えてreq.callReadyがデクリメントされ0になる
  4. window.onloadのタイミングでjQuery.readyが呼ばれる
    • $(document).ready()で設定したcallbackが実行される
IE7+CompanionJSの場合
Console [332]=     
 jQueryCheck :2,false
 
Console [333]=     
 jQuery.ready:2,false
 
Console [334]=     
 jQuery.ready() window.onload
 
Console [335]=     
 jQuery.ready:1,true
 
Console [336]=     
 require.ready
 
Console [337]=     
 req.callReady :0,true
 
  1. jQueryCheckが呼ばれてreadyStateがインクリメントされる
  2. (中略)
  3. window.onloadのタイミングでjQuery.readyが呼ばれる
    • readyState≠0なので、$(document).ready()で設定したcallbackが実行されない
  4. require.ready callbackを呼び出し終えてreq.callReadyがデクリメントされ0になる

jQueryによってjQuery.readyが呼ばれる最後のタイミングはwindow.onloadなので、
$(document).ready()で設定したcallbackは何時まで経っても実行されない。

フィードバックとか

仕事のコード抜きで再現させられたらしようかと。

とりあえずの対策

require.ready callbackとreq.callReadyの実行は同一のrun loopなので、
以下のような内容のJavaScriptをrequireしてやれば良い。

(function () {
  var windowLoaded = false;
  $(window).bind('load',function(){
    windowLoaded = true;
  });
  require.ready(function() {
    if ($.readyWait !== 0 && windowLoaded) {
      setTimeout(function() {
        $.ready();
      });
    }
  });
})();
Console [360]=     
 jQueryCheck :2,false
 
Console [361]=     
 jQuery.ready:2,false
 
Console [362]=     
 jQuery.ready() window.onload
 
Console [363]=     
 jQuery.ready:1,true
 
Console [364]=     
 require.ready
 
Console [365]=     
 req.callReady :0,true
 
Console [366]=     
 jQuery.ready:0,true
 

RequireJS moduleについて

RequireJSって何?

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


要はJavaScriptの依存性解決をしてくれるライブラリで、こんな感じで使えます。

require(
  [
    'lib/a'
    ,'lib/b'
    ,'lib/c'
  ],
  function(){
    // lib/a.js,lib/b.js,lib/c.jsが読み込まれていることが保証されているcallback
    require(
    [
      'lib/d' // lib/a.jsに依存しているライブラリ
      ,'lib/f'// lib/b.jsに依存しているライブラリ
    ],
    function(){
      // lib/a.js,lib/b.js,lib/c.js,lib/d.js,lib/e.jsが読み込まれていることが保証されているcallback
    });
  }
);

callbackの中でならライブラリが読み込まれてる前提でアプリケーションコードが書けるし、
callbackの中でrequire関数を呼べば、あるライブラリに依存する別のライブラリに依存する
アプリケーションとかも書けちゃいますよ、という感じ。
と言っても、これはかなり単純な使い方で、アプリケーションコードの規模が
小さい場合でないと実用的ではないと思う。

RequireJS moduleって何?

そこで、moduleですよ。

define({})

例えば、こんな内容のhoge.jsが配置されているとする。
このhoge.jsがhoge module。

define({
  hoge : 5
  ,fuga : 'piyo'
});

requireの第一引数に依存しているmodule名の配列を渡すと、
その順にcallback関数に渡される。

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

moduleはdefine関数にオブジェクトを渡すだけでなく、
オブジェクトを返す関数を渡す形でも定義できる。
これはfoo.js(foo module)

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

先程のhoge moduleとあわせて使うと

require(['hoge','foo'],function(hoge,foo){
  console.log(foo.bar); // 6
  console.log(hoge.fuga); // 'piyo'
});

依存関係の連鎖

RequireJS moduleではdefine関数の第一引数にmodule名の配列を渡すことで、
moduleが依存するmoduleを指定することができる。
hage.js(hage module)

define(['hoge','foo'],function(hoge,foo){
  return {
    bar : foo.bar
    ,baz : foo.baz
    ,hoge : hoge.hoge
    ,fuga : hoge.fuga
  }
});

下記のrequire(['hage'])ではhage moduleしか指定していないが、
依存先のhoge,fooも読み込まれている。
hage moduleを使いたい人はhage moduleが何に依存しているか知っている必要はない。

require(['hage'],function(hage){
  console.log(hage.bar); // 6
  console.log(hage.fuga); // 'piyo'
})

require(['hage'])を呼んだ場合、以下のような順で処理が実行される。

  1. hage.jsが読み込まれる
  2. hoge.jsが読み込まれる(hoge moduleが定義される)
  3. foo.jsが読み込まれる
  4. foo.jsでdefine関数に渡しているcallbackが実行され、戻り値がfoo moduleとして定義される
  5. hage.jsでdefine関数に渡しているcallbackが(hoge,foo moduleを使用して)実行され、戻り値がhage moduleとして定義される
  6. require(['hage'])のcallbackにhage moduleが渡され呼び出される

define関数のcallbackはmoduleの初期化用であって、
そのmoduleが依存moduleとして初めて呼ばれた時だけ実行される。


この依存関係の連鎖は何段階でもできる。

関数・class

今までの例ではmoduleとして連想配列しか定義していなかったが、
JavaScriptで扱えるすべての値がmoduleとして定義できる。
例えばこんなvery-useful-function.jsがあれば、

define(function(){
  function veryUsefulFunction(){
    /*
     * 何かすごい実装
     */
  }
  return veryUsefulFunction;
});

これを使いたい人は、

require(['very-useful-function'],function(veryUsefulFunction){
  /*
   * 何か自前のコード
   */
  veryUsefulFunction();
});

とか

define(['very-useful-function'],function(fun){
  /*
   * 何か自前のコード
   */
  fun();
  return module;
});

とか書いてやればいいし、


こんなvery-useful-class.jsがあれば、

define(function(){
  function VeryUsefulClass(){
  }
  VeryUsefulClass.prototype.benri = function(){
    /*
     * 何かすごい実装
     */
  };
  return VeryUsefulClass;
});

こんな風に書いてやればいい

require(['very-useful-class'],function(Useful){
  var hogehoge = new Useful();
  hogehoge.benri();
});


それぞれRequireJSのmoduleとして関数やclassを提供しているが、
グローバルは全く汚染していない。

まとめ

依存関係の連鎖と関数・classの提供などを組み合わせて、
アプリケーションコードを小分けにしていけば、
JavaScriptアプリケーションのコード規模が大きくなっても
スパゲッティ化を逃れられるのではないかと期待している。
もそっと深いところを把握した上で、java-ja.js#2で発表しようと思う。

debug-assert-RequireJS-moduleなるものを作りますた

GitHub - monjudoh/debug-assert-RequireJS-module: RequireJS module for debugging and assertion.
これは何かと言うとRequireJS moduleについて - 文殊堂で説明したRequireJSのmoduleで、
debug支援用のものです。

debug log出力の基本的な使い方

RequireJSのmoduleなので以下のように読み込んでやります。
callbackにdebug関数が渡されてくるのでこいつで適当にdebug printしましょう。

require(['debug'],function(debug){
  debug('hogehoge');
});
define(['debug'],function(debug){
  debug('fugafuga');
});

consoleがない環境で読み込むとdebugは空関数になるので、
console.debug();をべた書きするより安全です。
console.debugがない場合はconsole.logで出力します。

assert機能

サイボウズで学んだこと - IT戦記
id:amachangのこちらのエントリで書かれていたassertionをやるための機能です。
使い方はdebug関数からさらにassert関数が生えているので、
満たしているべき条件や条件判定結果を返す関数を渡しましょう。

require(['debug'],function(debug){
  document.getElementById('button02').onclick=function(){
    var textValue = document.getElementById('text02').value;
    debug(textValue);
    debug.assert(textValue.length < 4);
  }
});

assertに渡した値がfalsyだとdebuggerでbreakされます。

ステップアウトするよう勧められるのでそのとおりにすると…

原因箇所が表示されます。


元ネタと同じくconsoleにはスタックトレースが吐かれます。

debug用callback

debug関数にcallback関数を渡すと、
その第一,第二引数がconsole,assertになるので、
関数内でガシガシdebug用コードを書けます。


debug関数にcallback関数とthisを渡しておけば、
callback関数内外でthisが同じになるので、
プロダクションコードからdebug用コードに
コピペ出来なくて面倒くさいとかもないでしょう。
また、callback関数内で例外が投げられても外には影響ないので、
安心してdebug用コードを書く事ができます。

require(['debug'],function(debug){
  $('div').live('click',function(ev){
  debug(function(console,assert){
    assert($(this).text() == 'hoge');
    console.debug($(this).html());
  },this);
});
});

まとめ?

仕事でガンガン使って育ててこうと思います。
良かったら使ってみたりforkしていじったりしてみてください。

Dateをプロトタイプ汚染から守るExDate

というJavaScriptのライブラリを作りました。
GitHub - monjudoh/ExDate: JavaScript Date class wrapper for using conbination with Date libraly which leads to prototype pollution.

何をするライブラリか

ExDateというclassを定義しています。
やっていることは、インスタンスを作成すると同じコンストラクタ引数でDate objectを作成し、
内部に抱えて、Date objectのメソッドを呼ばれると処理を丸投げするだけです。

何が嬉しいのか

Datejs(GitHub - datejs/Datejs: A JavaScript Date and Time Library)という大変使い勝手のいいライブラリがあるのですが、
Date,Date.prototypeを自重なく汚染していて、使用がためらわれます。
何がまずいかというと今後JavaScript標準のDateにメソッドが増えてきても、
関係なく上書きしてしまい正しく動作しなくなってしまいますし、
実際ECMAScript5のDate.nowメソッドは上書きされてしまっていました。
プロトタイプ変更の対象をDateからExDateにすれば安全に使用できます。

使い方

  1. ExDateを読み込む
  2. Dateをバックアップした上で、ExDateと差し替える
    • originalDate=Date;Date=ExDate;
  3. Date,Date.prototypeを汚染するライブラリを読み込む
  4. バックアップから元のDateを戻す
    • Date=originalDate;

これをやれば、ExDateで各種ライブラリによるDate拡張の機能を使えるようになります。


Datejsと組み合わせた場合のデモもGithubに上げてあるので、
cloneして試してみてください。

hgsubversionのhg clone --startrev

前置き

hgsubversion自体の導入については↓をどうぞ
hgsubversionの導入 - 文殊堂

ちょっと前までのhgsubversion

ちょっと前までhgsubversionではsvnリポジトリの完全なコピーを作ることしかできませんでした。
例えば10年物のsvnリポジトリをhg cloneすると10年分の履歴全てを取ってきてしまいます。
目的がsvnリポジトリのコンバートであるなら問題ありませんが、
svnリポジトリを正としつつMercurialの分散SCMの機能を活用して開発をいい感じにやりたい
という場合には困ります。
git-svnのように特定リビジョン以降だけ持ってくるとかやりたいです。

startrev option

で、changeset:827547493112から出来るようになりました。
残念ながら基底のURL(/trunk,/tags/,/branches/がある階層)のcloneについては使えませんが、
trunkや各branchのcloneでなら使えます。


例えば http://python-nose.googlecode.com/svn/ のpy3k branchが欲しいとします。

svn co http://python-nose.googlecode.com/svn/branches/py3k python-nose.py3k.svn
cd python-nose.py3k.svn
svn log --stop-on-copy

とかやると根元がr562だと分かるので、

hg clone --startrev 562 http://python-nose.googlecode.com/svn/branches/py3k python-nose.py3k

とかやってやれば、branchの分岐点から先端までcloneしてこれます。
そのご普通にpush/pullも出来ます。