2012年6月20日水曜日

jQuery.fn.loadのcallbackで受け取るjQuery.Event.targetはIEでDOM要素をとれない時がある

タイトルで大体言いきりました。

写真のスライドショーを作成していて、画像タグはJavaScript側で作成してDOMに追加するようにしていました。 何故こうしたかというと、環境によって画像の読み込みが終わらないうちにスライドショーの自動再生が始まってしまうのを防ぐためです。

具体的には以下のようなコードで画像の読み込み完了を待ってDOMに追加しています。

$('').attr(src: '/images/hoge.jpg?' + new Date().getTime()).load (e) =>
  $img = $ e.target
  $('#container').append $img

'?' + new Date().getTime() の部分ですが、コレをしておかないとIEがonloadイベントをサボるんですよね。。
必ずイベントを発火させるようにしています。

でIMG要素を $ e.target でjQuery化してappendという流れですが、実はこれだとIEでIMG要素がとれていないんです!
気づかずにそのまま処理を進めると画面真っ白になってお馴染みのわかりづらいエラーメッセージにかなり困惑しました。

IE8で以下の様に検証してみました。

A) e.targetでIMG要素が取得できない

$('').attr(src: '/images/hoge.jpg?' + new Date().getTime()).load (e) =>
  console.log e.target.nodeName # => #document

B) e.targetでIMG要素がとれる

$img = $ 'img.sample'
$img.attr(src: $img.attr('src') + '?' + new Date().getTime()).load (e) =>
  console.log e.target.nodeName # => IMG

違いはJSで要素を作成するか、htmlに記述されている要素を取得するかですね。

まぁぶっちゃけ loadのcallback内でthisが対象のIMG要素なのでコレ使えばいいんですよ。
でもCoffeeScriptつかってclass化したオブジェクトの中だと @(this) は常にクラスそのものとして使いたいので、 関数のthisにバインドしてくれる => を使いたいんですよね。

というわけで jQuery.Event オブジェクトの出番なわけです。
Aの方法だとIEで期待通りでないことがわかったので調べたところ、jQuery.Event.currentTarget というのがありました。
console.log e.currentTarget.nodeName でちゃんとIMGが返ってきたよ〜!

今後調べたいこと

  • $('<img />') と書いた時にブラウザ別に何が行われているのか
  • jQuery.Eventのコンストラクタを読む

追記:

  • jQuery APIの.load()を見たらDeprecatedのタグが付いてたのでbindでも試してみたけど、結果は一緒
2012年6月4日月曜日

herokuで(mbstring有効な)phpがすぐ動いた

herokuでphpが動くのは前々から知っていたのですが、mbstringが有効なphpがいとも簡単に動くという情報を見つけたので試してみました。

$ mkdir heroku-php
$ git init
$ vim index.php
<?php
phpinfo();

$ git add .
$ git commit -m "initial commit"
$ heroku create --buildpack https://github.com/winglian/heroku-buildpack-php -s cedar
$ git push heroku master

こんだけ!?全然難しくないですね。
heroku openしてみたらちゃんとphpinfoが表示されました。

でも気になったのがindex.phpの設置場所。
基本phpはindexに相当するものを設置しなければいけないと思うのですが、これってpublicディレクトリにしても動くのだろうか…?

$ mkdir public
$ git mv index.php public/
$ git commit -am "moved index.php"
$ git push heroku master

-----> Heroku receiving push
-----> Fetching custom buildpack... done
 !     Heroku push rejected, no Cedar-supported app detected

ガーン!…そもそもアプリのルートにindex.phpが無いと受け付けてくれないのか。
ということは環境変数DOCUMENT_ROOTとか使えないのかな?

$ heroku config:add DOCUMENT_ROOT=/app/www/public
Adding config vars and restarting app... done, v5
  DOCUMENT_ROOT => /app/www/public

まぁ無理でした。環境変数はセットできても何の意味も無いですね。ググっても情報みつからないので多分このオプションは存在しないのでしょう。
アプリそのものが公開ディレクトリなのはそれはそれでphpらしいですよね。

しかしphp速い感じする。ただのphpinfoというのもあるだろうけど、アプリの更新しても再起動が無いからか。

参考:HerokuのBuildpackを利用してmbstringが有効なPHPサーバを立ててみた - hnwの日記

2012年6月1日金曜日

CoffeeScriptでJavaScriptもクラス化しよう。テストを書きやすくメンテしやすく

CoffeeScriptを知ったのはRailsに組み込まれたからですが、Rails3.1で採用されてすぐに飛びついたわけではなく、しばらく敬遠してた時期もありました。しかし使い始めてからはもう手放せなくなりました。
そして次第にCoffeeScriptのclass構文を使えばよりよいJavaScriptの開発ができるのではないかと思うようになりました。
今回は最近CoffeeScriptを使ってどのように開発しているかを紹介したいと思います。

CoffeeScriptでのclass定義はこうやります

class Person
  constructor: (@my_name) ->

  greeting: ->
    alert "Hello! my name is #{@my_name}."

window.Person = window.Person || Person

コンパイルすると以下のようなJavaScriptになります

(function() {
  var Person;

  Person = (function() {

    Person.name = 'Person';

    function Person(my_name) {
      this.myname = my_name;
    }

    Person.prototype.greeting = function() {
      return alert("Hello! my name is " + this.my_name);
    };

    return Person;

  })();

  window.Person = window.Person || Person;
}).call(this);

※nameプロパティを使うと上書きしてしまうのでmynameとしました

例えばこのようなコードを person というファイル名で保存し、インスタンス生成は main という別ファイルで実行します。

main.coffee
$ ->
  kozo = new window.Person 'kozo'
  kozo.greeting()
main.js
(function() {
  $(function() {
    var kozo;
    kozo = new window.Person('kozo');
    return kozo.greeting();
  });
}).call(this);

こうすることでまず管理がしやすくなりますし、テストも気持ちよく書けると思います。

テストフレームワークはよく jasmine をつかっています。その前は QUnit だったんですが jasmine の方がドキュメントが探しやすいのとツールが整っている印象ですし、なによりコードが書きやすく後からでも見やすいですね。人によるかも知れませんが。
僕の場合CoffeeScript を使うときはほぼ Rails か sinatra 上ですので jasmine-headless-webkit というgemをつかってます。これを使うとCUIで実行結果が得られます。(jasmine-railsはこれに依存しています)

jasmineの諸々の設定は省略しますがテストコードはこんな感じにしています。

person_spec.coffee
describe "Person", ->

  beforeEach ->
    @person = new window.Person 'sample'

  afterEach ->
    delete @person

  describe "propaties", ->
    it "have 'my_name' string", ->
      expect(typeof @person.my_name).toBe "string"

  describe "greeting method", ->
    beforeEach ->
      spyOn window, "alert"
      @person.greeting()

    it "alert greeting text", ->
      expect(window.alert).toHaveBeenCalledWith "Hello! my name is sample."

DOMが絡むテストは jasmine-fixture というのをhelperとして読み込んで使っています。jqueryに依存しているのですが、むしろ使わない事のほうが少ないので気にしないです。affix という便利メソッドが使えるようになって、jqueryオブジェクトのprototypeにも追加されるので気に入ってます。

以上のような形で開発を進めてるのですが、クラスファイルと実行ファイルを分けてなるべくDOMの情報とかは実行ファイルに書き込むようにするといいと思います。
サイトのトップページにスライドショー的なものを設置するなら例えば以下の様な感じです。

main.coffee
$ ->
  slideshow = new window.Slideshow {
    container_id: '#hoge-slideshow'
    controller_id: '#hoge-controller'
    duration: 800
    wait: 3000
  }
  slideshow.start()

要はリテラル値をまとめて書いておくということです。

ただこのように実行ファイルに分けていてもファイル名が script とかにする人がいたりして…それはそれでわからなくはないのですが、mainの方が無難じゃないですかねぇ。

あぁマシン持ち寄ってこうゆう話ししながら酒を呑む仲間がほしいなぁ…