愛と勇気と缶ビール

ふしぎとぼくらはなにをしたらよいか

Gruntをようやく使ってみた

Gruntってアレですね、JSでホゲホゲっと設定ファイルを書いてJSとかCSSのファイルをminify/unifyとかするヤツです。

最近よく聞くので、機会があったら使ってみようと思っていたのですが、最近とあるウェッブページをPlay!Frameworkで作ってみたりしていたので、それのJS/CSSのビルドに使ってみました。

もろもろやりたいことを実現したらこんな感じに。本当はもうちょっと綺麗に書けるのではないかと思います。

https://gist.github.com/3738650

僕のやりたかったことは、

  • JS/CSSのunify
  • JS/CSSのminify
  • JS/CSSgzip (for Play!)
  • timestampをつけたファイル名の生成
  • 作成されたfileどものclean
  • conf, templateの文字列置換。@@timestampをtimestampに置換するなど。 (for Play!)

だったのですが、pluginっぽいものを導入したら全部いけました。おおむねのサイトで必要になるJS/CSSのpreprocessingは大体いけそうな感じですね。いい感じですね。

timestampをつけたり何だりしているのは、該当ウェッブページでは「max-ageに一年後を指定してずーっとブラウザキャッシュにのせてもらって、更新があった場合はファイル名を変える」というよくあるキャッシュ戦略をとっているからですね。このブラウザキャッシュに頼ったキャッシュ戦略はスマートフォンではそこまでいけてない(主にiOSのブラウザキャッシュがオンメモリ限定だったり、デスクトップブラウザよりブラウザキャッシュ全体のサイズが小さいため比較的早く追い出しが発生することから)と思うのですが、JSをlocalStorage/WebSQLにキャッシュする x3.js (x3-js - JavaScript rapidly reloader - Google Project Hosting) を使うなどしてもうちょっとマシになるよう努力しています。x3.js、素敵です。


ちょっと脱線しましたが、Grunt自体はよいツールです。何がよいのかというと、

という所ですね。こういうツールが特定のフレームワーク/言語前提で書かれていると、「えーそれじゃ俺ら使えないじゃーん」「えー他の言語でサーバサイド書きたい場合に移行できないじゃーん」問題が発生するのですがGruntの場合はNode.jsというJavaScriptで小粒なツールを作るための環境にのっかっているので誰でも気楽に使えます。JSやらCSSのファイルをゴニョゴニョするためだけに他のプログラミング言語に登場してもらうのは忍びない、という奥ゆかしい事情もありますね。

とはいえ、v0.4になったらまた微妙にインタフェースが変わるとか、現存しているpluginのインタフェースに今ひとつ統一感がないとか「うーむ…数ヶ月後には俺の書いたgrunt.jsは動かないんじゃないだろうか...」という気分になってしまう部分も多いので、その辺は今後に期待したいところです。

canvasのgetImageData(), toDataURL()などにおけるCORSについて

僕のcanvas力はいまだ中学生男子並みなので、そもそもcanvasの一部のAPIにSame Origin Policy的なものが適用されることすら今日まで知らなかったのですが、どうやら

違う生成元がsrcに指定されているimgをputImageData -> その描画領域をgetImageDataしようとするとDOM Exception

という感じに、普通に怒られちゃうみたいですね。知らんかった。

http://www.w3.org/TR/cors/#use-cases

↑のあたりに書いてあるのをみると、taintedなcanvas、というのですかね。getImageDataで同じ生成元から読み込んだimgの描画部分を指定しても怒られないけど、違う生成元の画像を描画した部分に少しでもかかっていると怒られるので、canvasってピクセルごとに "taintedか、そうでないか" を管理してるのかなー、と。ブラウザの中の人もなかなかタイヘンですね。


で、これって何か回避策ないのかなー、と探していたらどうやらCORSの流儀にのっとってimgを読みこめば出来るらしい。要は、

1. img要素にcrossorigin属性をつける (https://developer.mozilla.org/ja/docs/CORS_Enabled_Image)
2. 画像をサーブする側でしかるべき Access-Control-Allow-Origin header をつける

という両方の条件を満たせば、特に怒られることなく異なる生成元のimgをcanvasに読み込んだ上で安心してputImageDataやらtoDataURLができます。もちろんCORS周りのことをちゃんと理解してくれるブラウザでないといけませんが。
XHR Level2とかを知っている人が見ると「後者だけでいけるんじゃね?」という気になりますが、どうやら両方の条件を満たさないとダメなようです。(少なくともChromeでは)

最近Amazon S3がCORSに対応したらしいですが、このへんを組み合わせると「(nginxとかでdomain合わせるためのproxyをかますことなく) ピャーっとS3の画像を読み込んで、canvas上にloadしてチョメチョメして、ピャーっとXHR Level2でその画像をpostしてピャーっとupdateする」というようなことがJSレベルで完結してしまうのかもしれませんね。S3使ったことないのでこの辺は僕の妄想ですが、まあRESTfulっていうんだから多分それぞれのobjectに対するCRUD操作は提供されているんでしょう。多分。


あまりにcanvas力がないので何か作りたいなーと思っていたりします。「裸に見える画像ジェネレーター(服の布のある部分を隠すと裸に見えるアレ)」とか出来たら素敵なのですが、ああいうのって「アイコラ」「アイコラ」言われてうしろ指をさされそうやなあ、と思って、躊躇しております。

Maintainable JavaScriptにみる、コンテキストとアプリケーションロジックの分離

個人的なこと

読書はいわゆる自己投資?にあたるものなのでケチるもんじゃないよなあ、とは思いつつも可能なら安い値段でより大きなリターンを得たいよねー、ということで最近はOreillyの半額セールに目を光らせるようになりました。英語は「拾い読み」がし辛いという欠点があるのですが、まぁ、安いし、全ての本にちゃんと訳が出るわけでもないので、ええかなぁと。

そんなわけで "Maintainable JavaScript" という本を読んでいたのですが、その中のEvent Handlingについての章が「おお、これこれ」という感じだったのでちょっと紹介。


Maintainable Event Handling

jQuery覚えたぜ!って感じの人がとりあえずコードを書くと、だいたいこんな感じになりますね。ちなみに、これは別にjQueryがどうとかいう話ではなくて、本質的には生DOMでも他のライブラリでも同じことです。

$(function() {
    $("#foo").on("click", function(e) {
        // アプリケーションロジック1
        // ... そこまで長くはない ...
    });

    $("#bar").on("click", function(e) {
        // アプリケーションロジック2
        // ... まあまあ長い ...
    });

    $("#baz").on("click", function(e) {
        // アプリケーションロジック3
        // ... そこまで長くはない ...
    });
})();

要は、特定のElementにイベントハンドラをくっつけて、イベントハンドラの中にアプリケーションロジックをダラダラっと書くわけですね。
ちなみに、「これがダメ!」って言いたいわけではないです。コードの規模に対して適切な抽象化のレベルってあると思うので、静的なWebページに飾り付けをするだけならこういう書き方でもそこまで泣きをみることはないと思います。小規模なコードしか書く予定がないのに変にクラスベースなオブジェクト指向を頑張ってみたりとか、明確な意図なしに「今はClient Side MVCが流行りなんだぜい!」とかやってしまうと逆に後で自分をうらむことになるでしょう。

で、"Maintainable JavaScript"の著者であるNicholas Zakas先生は「こういうイベントハンドラの中にアプリケーションロジックの書いてあるコードはいくない!」と言います。なぜかといえば、

  • 実装したアプリケーションロジックを、イベントハンドラ意外の場所から呼び出したくなるのはままあることだ
  • 例えば、ユーザのアクション起因じゃなくてViewをプログラムから更新したい時とか...(あるある)

なので、イベントハンドラとアプリケーションロジックは分離しておくと、未来の自分に感謝されます。

$(function() {
    var Application = {
        handleClick: function(e) {
            this.showPopup(e);
        },
        showPopup: function(e) {
            // アプリケーションロジック
        }
    };
    $("#foo").on("click", function(e) {
        Application.handleClick(e);
    });
})();

Zakas先生は「これでもまだいかん!」と言います。なぜなら、上の例のshowPopupという関数がevent objectを引数に取ってしまっているので、このままでは結局他の所から呼べないからですね。
まだ、「ユーザのクリック」という呼び出しコンテキストとアプリケーションロジックが結合してしまっています。

なので、以下のように変更します。

$(function() {
    var Application = {
        handleClick: function(e) {
            this.showPopup(e.clientX, e.clientY); // jQueryのEvent Objectってこいつら生えてたっけ?まあいいや
        },
        showPopup: function(x, y) {
            // アプリケーションロジック
        }
    };
    $("#foo").on("click", function(e) {
        Application.handleClick(e);
    });
})();

こうすると、ユーザのクリック以外のタイミングでshowPopupを、任意の座標x, yを引数として呼び出せるようになりますね。
また、showPopupのテストも書きやすくなると思います。「アプリケーションロジックには直接Eventを渡さず、必要なデータのみを渡す」ということですね。

ところで、いちいちイベントの種類ごとにhandleなんとかという関数を用意するのもアレなので、handleEventでまとめてしまうのも場合によってはアリかもしれません。

$(function() {
    var Application = {
        handleEvent: function(e) {
            switch (e.type) {
                case "click": this.showPopup(e.clientX, e.clientY); break;
                case "scroll": ...
                case "drag": ...
            }
        },
        showPopup: function(x, y) {
            // アプリケーションロジック
        }
    };
    $("#foo").on("click", Application);
})();

jQueryが正しく扱ってくれるかどうかまでは知りませんが、handleEventという関数が生えたオブジェクトをイベントハンドラとして指定できるのはDOM Level2のれっきとした仕様です。(http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventListener)

ここまで来れば、さらに再利用性を高めるための抽象化を行うまでは後一歩です。

(function() {
    function ModalController(el) {
        this.elem = el;
        el.on("click", this);
    }
    ModalController.prototype.handleEvent = function(e) {
        switch (e.type) {
            case "click": this.showPopup(e.clientX, e.clientY); break;
            case "scroll": ...
            case "drag": ...
        }
    };
    ModalController.prototype.showPopup = function(x, y) {
        // アプリケーションロジック
    };
})();
$(function() {
    var controller = new ModalController($("#foo"));
})();

が、現実にこんなことをやり始めた場合は、何らかの既成のフレームワークに乗っかることを考え始めた方がいいと思います。

呼び出しコンテキストとアプリケーションロジックの分離、について

上記の話のキモは、「ユーザのクリックが行われ、それに伴うEvent Objectがある」というコンテキストと、実際のアプリケーションロジックを分離することで再利用性を高めている部分にあります。

これってどこかで聞いた話ですね。Webアプリケーションフレームワークの多くが採用している、MVCパターンに則ってアプリを作る場合でも「ControllerにおけるHTTP Requestにひもづくコンテキスト(例えばContext Objectであったり、HTTP Requestを表現するObjectであったり、Formを表現するObjectであったり)をビジネスロジックのレイヤに持ち込まない、伝播しない」という書き方を、まともな人ならすると思います。そうしておかないと、同じビジネスロジックをbatchやworkerなどの他の場所から再利用できなくなってしまいますからね。MVCというフレームワーク/語彙を使わなくても、この辺の原則はまぁ変わらないんじゃないかなーと思うのですが、先生どうなんでしょう。

iOSAndroidのネイティブアプリでも、やはりユーザのアクションに紐付いたコンテキストと、アプリケーションロジックは分離するとよさそうですね。JavaScriptでのEvent Handlingに限らず、アプリケーションロジックは複数の場所/コンテキストから呼び出される前提で書くものだぜ!という考え方はコードの再利用性を高めてくれるのではないかなーと思います。

ES5 features on iOS/Android's default browser

iOS, Androidのめぼしいバージョンのデフォルトブラウザについて、ECMAScript 5 compatibility table を使ってES5の対応度合いを調べました。既にありそうだなーと思いつつパッと見当たらなかったので。
どれもエミュレータで調べたものですし、特にAndroidについてはメーカーが手を入れてヘンテコリンなことになっている可能性も結構あるので、目安程度に。

全てのiOSデバイスが6.0以上、Androidデバイスが4.1以上になればstrict mode含めてやりたい放題(かも?)、ということが分かりますね。いつになることやら。

feature/os version ios-4.3.2 ios-5.0 ios-5.1 ios-6.0 android-1.6 android-2.1 android-2.3.3 android-3.0 android-4.0.2 android-4.1.1
Object.create yes yes yes yes no no yes yes yes yes
Object.defineProperty yes yes yes yes no no yes yes yes yes
Object.defineProperties yes yes yes yes no no yes yes yes yes
Object.getPrototypeOf yes yes yes yes no yes yes yes yes yes
Object.keys yes yes yes yes no no yes yes yes yes
Object.seal no yes yes yes no no no no yes yes
Object.freeze no yes yes yes no no no no yes yes
Object.preventExtensions no yes yes yes no no no no yes yes
Object.isSealed no yes yes yes no no no no yes yes
Object.isFrozen no yes yes yes no no no no yes yes
Object.isExtensible no yes yes yes no no no no yes yes
Object.getOwnPropertyDescriptor yes yes yes yes no no yes yes yes yes
Object.getOwnPropertyNames yes yes yes yes no no yes yes yes yes
Date.prototype.toISOString yes yes yes yes no yes yes yes yes yes
Date.now yes yes yes yes yes yes yes yes yes yes
Array.isArray yes yes yes yes no no yes yes yes yes
JSON yes yes yes yes no yes yes yes yes yes
Function.prototype.bind no no no yes no no no no yes yes
String.prototype.trim yes yes yes yes no no yes yes yes yes
Array.prototype.indexOf yes yes yes yes yes yes yes yes yes yes
Array.prototype.lastIndexOf yes yes yes yes yes yes yes yes yes yes
Array.prototype.every yes yes yes yes yes yes yes yes yes yes
Array.prototype.some yes yes yes yes yes yes yes yes yes yes
Array.prototype.forEach yes yes yes yes yes yes yes yes yes yes
Array.prototype.map yes yes yes yes yes yes yes yes yes yes
Array.prototype.filter yes yes yes yes yes yes yes yes yes yes
Array.prototype.reduce yes yes yes yes no yes yes yes yes yes
Array.prototype.reduceRight yes yes yes yes no yes yes yes yes yes
Getter in property initializer yes yes yes yes yes yes yes yes yes yes
Setter in property initializer yes yes yes yes yes yes yes yes yes yes
Property access on strings yes yes yes yes yes yes yes yes yes yes
Reserved words as property names no yes yes yes no no no yes yes yes
Zero-width chars in identifiers no no no yes no no no no no yes
Strict mode no yes yes yes no no no yes no yes

DOMContentLoadedに引っ掛けて初期化を行う外部スクリプトを、DOMが出来てからアレアレする

だいたい以下のような感じでうまくいくんじゃないかと思うけど、ちゃんとした検証はしてないので。WebKitでしかみてませんし。

引数のtargetはwindowやdocument, scriptはsrcが設定されたscript elementだと思いねえ。

(※イベントハンドラ内でevt.targetとかを参照するようなヤツはこれだとダメです)

function hijackDOMLoaded(target, script) {
    var orig = target.addEventListener, callbacks = [];
    target.addEventListener = function(type, cb, useCapture) {
        if ( type === "DOMContentLoaded" ) {
            callbacks.push(cb);
        }
        else {
            orig.call(target, type, cb, useCapture);
        }
    };

    script.onload = function() {
        target.addEventListener = orig;
        callbacks.forEach(function(cb) { cb(); });
    };
    document.body.appendChild(script);
}