Takazudo hamalog

programming notes. mainly about JavaScript / jQuery. [@Takazudo] [takazudo@gmail.com] Hint: alt + /

cool guy

gruntのサンプルら

2012/03/24 permalink

grunt がバージョン0.3になり、APIが変更になった。ついでによく使うサンプルらをまとめておいたのでちょこちょこ更新するかも。

CoffeeScript、sass、compassとかをwatchしてコンパイルするやつ。

jQuery.fx.offですべてのアニメーションをカット

2012/03/22 permalink

でも昔書いたのだけども、IE8以下ではopacityに対応していないので、jQueryでアニメーションしようとすると、色々なことが面倒。そこで、ばっさりIE8以下対応をさっさとやるのが以下。どうのこうのでブラウザ判別でもして

if (ieLessThan9){
  jQuery.fx.off = true;
}

jQuery.fx.off = true にすれば、すべてのアニメーションが一瞬で終わるようになります。これでfadeした時でも一瞬で変わるから問題なしだぜイェイってねー。

※これはかなりUIに関するJSを書く必要があるケースを想定してます

おいおいおい、おれのクライアントはそんなので納得しないぜ?オイ?
とでもお思いですか?それは状況によりけりですよ。かかるコストとスケジュールと実現したいことで決めればいいんです。

例えば、デザインが透過PNGだらけで、やりたいアニメーションをIE8以下向けに調節するのには相当の時間がかかってしまうことが見込まれる場合、普通に作るのと同じぐらいまでにはならないかもしれないけども、かなりの時間をかけて、ここはIE8以下はアニメーション無しでー、これはIE7以下はアニメーション無しでー…っていうようなコードをたっくさん書かないとダメですよ。そういう時、サービス残業してやるんですか? IE死ねとかツイートしながら。それは違うんじゃないんですかねー。どのブラウザではこういう特長があるからっていうのを始めに説明して、それに対応するには時間がかなり掛かることが見込まれるので云々って言っておき、工数を見積る材料にするべきですよ。その見積りだとIE8以下はアニメーションカットですねとかね。

あとはスケジュールが全然無いときとか。とりあえずIE8以下はアニメーション全てカットで完成させたらどうです?そこからIE8以下を細かく調節していったらいいんじゃないですかね。CSSが使われだした時、CSSを切ってもちゃんと見れるって、みんな重視してたでは無いですか。アニメーションを切ってもコンテンツは見れますよ。まぁ、広く見られるコンテンツであればサイトのアクセスログからブラウザの割合ぐらいは確認したほうがいいとは思いますけどね。

逆に、アニメーションをカットしたほうが良いこともあるんですよ。というのは、IE8以下は性能的にもかなり低いブラウザなので、色々一気にアニメーションさせちゃうと、そもそもサイトが重いーってんで辛いことも多いんです。そんな状態なら、逆にアニメーションを全部させないほうがサクサク閲覧できたりすることも多いですよ。

ということで、ひとつ、jQuery.fx.offでIE8以下アニメーション全カット - ってのを一つのボーダーラインとして考えてみるのはどうでしょう? もちろん開発者一人が勝手にそう思ってやりましたとか、後から言ってもダメですけど当然。

CoffeeScriptをおすすめしたいワケ

2012/03/11 permalink

以下のエントリでも少し書いたけれども、

それ以降、CoffeeScriptでJSを書き続けていて、やっぱCoffeeScriptいいなーと結構思ってる。自分にとってのCoffeeScriptがいいと思う一番の理由は、「オブジェクト指向的なコードが、ストレス無く書けるから」っていう点だと感じてる。例えば、以下のような動作をするUIがあり、

このひとまとめのUIを管理するクラス的な物を作ろうとしたら、例えば以下のようになる。

var Hoge = function(el) {
  this.content = el.find('.content');
  var self = this;
  el.find('button').click(function() {
    self.toggle();
  });
};
Hoge.prototype = {
  toggle: function() {
    this.content.fadeToggle();
    return this;
  }
};

$(function() {
  new Hoge($('#hoge'));
});

ここでは、thisの参照が変わっちゃうのでどうのこうのな問題がある。これを解決するには、とりあえず上記のようにself=thisとかするのが早いけど、同様のことをするのに、色んなやり方がある。

jQuery - proxy

var Hoge = function(el) {
  this.content = el.find('.content');
  el.find('button').click($.proxy(this.toggle, this));
};
Hoge.prototype = {
  toggle: function() {
    this.content.fadeToggle();
    return this;
  }
};

Underscore.js - bind

var Hoge = function(el) {
  this.content = el.find('.content');
  el.find('button').click(_.bind(this.toggle, this));
};
Hoge.prototype = {
  toggle: function() {
    this.content.fadeToggle();
    return this;
  }
};

Underscore.js - bindAll

var Hoge = function(el) {
  this.content = el.find('.content');
  _.bindAll(this);
  el.find('button').click(this.toggle);
};
Hoge.prototype = {
  toggle: function() {
    this.content.fadeToggle();
    return this;
  }
};

これらの $.proxy だの _.bind だの _.bindAll だのを理解するのは、結構JSを書いている人であれば、あーそーいうやつねって分かるもんだけど、オブジェクト指向を勉強したいでーすとか思ってはじめる人にとっては、上記のようなことが一つ、壁のように超えないといけない部分なんじゃないかと、ちょっと思う。

まずそもそも、JavaScriptにはクラスが無いとか言う割にはそれっていわゆるクラスだよねみたいな話を理解しないといけないし、OK、クラスっぽいもの理解したってなっても、そのやり方が何通りもあって、うーむ一体どう書けば一番いいんだろうみたいなのを理解するのにまた壁がある…と思うんだけどもどうだろう? 自分は、そういったことに結構な時間を費やしたと思うんだけども。それで、それらの事情を理解した上で初めて、$.proxy だの _.bind だの _.bindAll だのの解説を見て、これを何に使うか理解できるような感じじゃないですかね? それを通り超えた上で、よしじゃあどう書こうみたいな議論も結構不毛。次に待ってるのは継承がどうだみたいな話。そしてそのやり方も様々…とかとか。

別に、そんな事情はさっぱり知らなくていいから、CoffeeScript使っとけばいいんだよ!とかは全然思わないけど、上記のようなコードは、CoffeeScriptなら、以下のように書ける。

class Hoge
  constructor: (el) ->
    @content = el.find '.content'
    el.find('button').click @toggle
  toggle: =>
    @content.fadeToggle()
    @

toggleメソッドは直接インスタンスを参照するみたいな風に、=> で書けばいいだけ。コレをコンパイルすると、以下みたいになる。

var __bind = function(fn, me){
  return function(){ return fn.apply(me, arguments); };
};
var Hoge = (function() {
  function Hoge(el) {
    this.toggle = __bind(this.toggle, this);
    this.content = el.find('.content');
    el.find('button').click(this.toggle);
  }
  Hoge.prototype.toggle = function() {
    this.content.fadeToggle();
    return this;
  };
  return Hoge;
})();

勝手にコンストラクタの中でbindしてくれるというわけ。CoffeeScriptでクラスを書いている以上は、こういった事情は、「裏でCoffeeScriptがやってくれること」として、封じ込めておくことが出来る。それって、jQueryが裏でブラウザごとの差異を埋めてくれて、単純なAPIを提供してくれているがごとく、より直感的なAPIを使いながらコードを書いていける…って言えるんじゃないかなーと思うんだけどどうだろう。

もちろん、裏でそのようなことが行われるというのを理解していなければ、ドツボにハマることは往々にしてあるでしょうが、イメージとして思い描いているクラスベースな設計というのを端的に表してるのは、CoffeeScriptのコードなんじゃなかろうか。ついでに言っておくと、クラスの継承は class Hoge extends SuperHoge とか書くだけだし、mixinも簡単

()とか{}が省略できたり、PythonやRubyライクにコードを書けるのも、確かに大きな利点だと思う。でも、自分が一番大きいと感じているのは、上記のようなクラスベースの書き方が、サクサク使えるために、自分の書いているコードも、自然とそのように整理できるようになったと感じてるので、そーいうところがCoffeeScriptのいいところなんではないかなーとか思った次第。

例えば、JavaScriptでオブジェクト指向を学ぼうみたいな本があったとする。そういう本を書くとしたら、本のかなりの部分が、この、クラスとはなんぞや - そしてそれをどう書くのじゃみたいな説明に費やされてしまうんじゃないだろうか。そういう場合、CoffeeScriptからオブジェクト指向入門したのならば、考え方を比較的素直に理解できるような気がするんだけども。

あとは、CoffeeScriptを書くということは、最終的にはJSにコンパイルする必要があるということで、これはたしかにコードをいじるハードルを高めてしまう。現時点だと、MacかLinuxじゃないと辛くない?とも思うし…。だけど、ついでといってはなんだけれども、このおかげで、自分の場合、どうせコンパイルするならminifyとかjsファイルらの結合もやってしまい、CSSもsassでやって、一気にビルド環境も揃えてしまおうという考えに至ったりした。(参考:grunt

Backbone.jsでTweetDeckっぽいUIなサンプル

2012/03/11 permalink

Twitter検索を複数のカラムで見れるみたいなそういうやつを、Backbone.jsの勉強のために作ってみた。

Modelに対応するViewを作り、modelの更新などをトリガーにしてViewを更新するみたいな感じにして、ModelはViewのことを知らなくても良くすればいいっぽい。

おまけで、上記アプリは検索したクエリーをlocalStorageに保存していて、ブラウザ閉じても前開いてた奴がすぐ出るようになってる。あと、右上にあるカウンターは3分おきに各検索結果をリロードするもので、クリックするとこのインターバルの感覚を変えられる。

Backbone.jsでUIの状態を管理するみたいなやつ

2012/03/09 permalink

前のやつの続き。UIの状態がコロコロ変わるのはどこで制御すりゃええねんっていうことで、Managerっていうクラスを作ってそいつにやらせることにした例。といっても、Spine.jsのManagerっていうやつの真似をBackbone.jsでやっただけだけど。Spine.jsのドキュメントによれば、これは、State machine とかいうやつらしい。

このサンプルでは、一個クリックすると他のがactiveじゃなくなる。作ったViewのインスタンスを、Managerにぴょこぴょこ追加し、追加されたviewらをまとめてアクティブにしたりしなかったりするようなことをやるだけのクラスを作る。

# ============================================
# Abstract things
# ============================================

class Manager
  _.extend @::, Backbone.Events
  add: (item) ->
    @items ?= []
    @items.push item
  deactivateAll: ->
    _.each @items, (item) -> item.deactivate()
    @trigger 'deactivateall'
  deactivateWithout: (target) ->
    _.each (_.without @items, target), (item) ->
      item.deactivate()
  reset: ->
    @items = []

class ActivatableView extends Backbone.View
  active: false
  activate: =>
    if @active then return
    @active = true
    @$el.addClass('active')
    @trigger 'activate'
    @
  deactivate: =>
    if not @active then return
    @active = false
    @$el.removeClass('active')
    @trigger 'deactivate'
    @

# ============================================
# Models
# ============================================

class Kaomoji extends Backbone.Model

class KaomojiCollection extends Backbone.Collection
  model: Kaomoji
  update: (strArray) ->
    data = _.map strArray, (str) -> { str: str }
    @reset data
    @

Kaomojis = new KaomojiCollection

# ============================================
# Views
# ============================================

class KaomojiView extends ActivatableView
  className: 'kaomoji'
  events:
    'click': 'activate'
  render: ->
    @$el.html @model.get('str')
    @

class KaomojiListView extends Backbone.View
  initialize: ->
    @manager = new Manager
    Kaomojis.bind 'add', @addOne
    Kaomojis.bind 'reset', @refresh
  addOne: (kaomoji) =>
    view = new KaomojiView(model: kaomoji)
    @manager.add view
    view.bind 'activate', => @manager.deactivateWithout view
    @$el.prepend(view.render().el)
    @
  refresh: =>
    @$el.empty()
    @manager.reset()
    Kaomojis.each(@addOne)
    @

# ============================================
# \(^o^)/
# ============================================

$ ->

  data = [
    '\(^o^)/', '(´・_・`)', '∩( ・ω・)∩', ' ( ´⊇`)'
    '\(^o^)/', '(´・_・`)', '∩( ・ω・)∩', ' ( ´⊇`)'
    '\(^o^)/', '(´・_・`)', '∩( ・ω・)∩', ' ( ´⊇`)'
    '\(^o^)/', '(´・_・`)', '∩( ・ω・)∩', ' ( ´⊇`)'
  ]

  listView = new KaomojiListView(el: $('#kaomojilist'))

  Kaomojis.update data

  listView.manager.bind 'deactivateall', ->
    alert 'all items were deactivated!'
  Kaomojis.bind 'reset', ->
    alert 'all items were reset!'

  $('#button1').click -> Kaomojis.reset()
  $('#button2').click -> listView.manager.deactivateAll()
  $('#button3').click -> Kaomojis.add { str: '( ´⊇`)' }

たしかに、こういうのやるとき、Viewが超絶に肥大化するので、どいつがアクティブなのよっていう管理を全部別のクラスにまかせちゃうのはよさげな気がした。こーいうの書くとき、CoffeeScriptはやりやすいなー。

例えばTwitterを作るよーとかだったらこんなイメージ

追記、このManagerとやらを使わんでやると、以下のようになるので、viewらを管理する奴を立ててやるという感じなのかも。

class KaomojiView extends Backbone.View
  active: false
  className: 'kaomoji'
  events:
    'click': 'activate'
  render: ->
    @$el.html @model.get('str')
    @
  activate: =>
    if @active then return
    @active = true
    @$el.addClass('active')
    @trigger 'activate'
    @
  deactivate: =>
    if not @active then return
    @active = false
    @$el.removeClass('active')
    @trigger 'deactivate'
    @

class KaomojiListView extends Backbone.View
  initialize: ->
    Kaomojis.bind 'add', @addOne
    Kaomojis.bind 'reset', @refresh
  addOne: (kaomoji) =>
    item = new KaomojiView(model: kaomoji)
    item.bind 'activate', => @deactivateWithout item
    @$el.prepend(item.render().el)
    @items ?= []
    @items.push item
    @
  deactivateAll: ->
    _.each @items, (item) -> item.deactivate()
    @trigger 'deactivateall'
  deactivateWithout: (target) ->
    _.each (_.without @items, target), (item) ->
      item.deactivate()
  deactivateAll: ->
    _.each @items, (item) -> item.deactivate()
    @trigger 'deactivateall'
  deactivateWithout: (target) ->
    _.each (_.without @items, target), (item) ->
      item.deactivate()
  reset: ->
    @items = []
  refresh: =>
    @$el.empty()
    @reset()
    Kaomojis.each(@addOne)
    @

Backbone.jsでMVCでTwitter検索なやつをCoffeeScriptで

2012/03/07 permalink

最近MVCがどうのとか調べたりしてるもんで、前Spine.jsで作ったやつを、Backbone.jsにして書いてみた。

# Template manager
# //hamalog.tumblr.com/post/13593032409/jquery-tmpldeck
deck = $.TmplDeck 'templates.html'

# ============================================
# API wrapper
# ============================================

api =
  getTweets: (query) ->
    $.Deferred (defer) ->
      $.ajax
        url: 'http://search.twitter.com/search.json'
        dataType: 'jsonp'
        data:
          result_type: 'recent'
          rpp: 10
          page: 1
          q: query
      .pipe (res) ->
        defer.resolve res.results
      , ->
        alert 'error!'

# ============================================
# Models
# ============================================

class Tweet extends Backbone.Model
  defautls:
    'from_user': ''
    'profile_image_url': ''
    'text': ''

class TweetList extends Backbone.Collection
  model: Tweet
  initialize: ->
  update: (query) ->
    api.getTweets(query).done (tweets) =>
      @reset(tweets)
      if not tweets.length then @trigger('noresults')

window.Tweets = new TweetList

# ============================================
# Views
# ============================================

class TweetItemDiv extends Backbone.View
  render: ->
    @$el.html( deck.tmpl 'item', @model.toJSON() )
    @

class TweetsDiv extends Backbone.View
  initialize: ->
    Tweets.bind 'add', @addOne
    Tweets.bind 'reset', @refresh

  addOne: (tweet) =>
    view = new TweetItemDiv(model: tweet)
    @$el.append(view.render().el)
    @

  refresh: =>
    @$el.empty()
    Tweets.each(@addOne)
    @

class Form extends Backbone.View
  events:
    'submit form': '_submitHandler'

  initialize: ->
    @els =
      query: @$('input[type=text]')
    if @options.initialQuery then @val(@options.initialQuery)

  _submitHandler: (e) ->
    e.preventDefault()
    query = @els.query.val()
    @trigger 'submit', query

  val: (query) ->
    @els.query.val(query)
    @

# ============================================
# \(^o^)/
# ============================================

deck.load().done ->
  $ ->
    initialQuery = 'iPad'
    tweetsDiv = new TweetsDiv(el: $('#tweets'))
    form = new Form(el: $('#searchform'), initialQuery: initialQuery)
    form.bind 'submit', (query) ->
      Tweets.update(query)
    Tweets.bind 'noresults', -> alert 'no results!'
    Tweets.update(initialQuery)

前のSpine.jsのコードと見比べてみてもらうと分かるんですが、ほとんどおんなじ。ただ、Backbone.js の Collection+Model が Spine.js の Model に該当するので、Backbone.js の方がクラスが増えてる感じになってる。

この、クライアントMVC的なことをなにでやろうかなーどうすればいいのかなーと色々考えていて、Spine.jsよさそうということで触ってみたのだけども、色々考えた末に、やっぱりBackbone.jsでやろうかなって思ってきた。一番でかい理由が、Underscore.jsのメソッドいっぱい使うから。

以下はまとまってない考えを書いた駄文。

自分は案件でもBackbone.jsを使っていたんだけれども、はっきりいってMVCがなんたるかをあんまりよく理解してなかった。いや、今も理解したとは言いがたいけども、とにかく、自分にとってのMVCとは、ひとまずMを分離することだった。データだけの処理、APIとのやりとりはMでやってねっていう。この、Mを分離するという行為は、管理上、大いに役に立った。そして、MVCでいうCでコンポジットを作り、これらの一番上に存在するメディエイターを作って管理していた。この辺の設計思想みたいのは、この辺に書いてある。

自分が書いたモノは、MVCっぽくはあるけど、データのやりとりをMにやらせていただけという感じに近かった。言ってみれば、なんとなくMVCである。そして、MVCでいう、Cの部分がかなり大部分の役割を担っていた。たぶん、これはMVPというやつなのかも知れなかった。色々見ていたら、実際のところJavaScriptでMVCっていうのは、なんかそのクラシックなMVCと比較するとやることも違うしゆるふわな感じっぽいとのことなのだけれども。

そんな感じで、その辺の事情が曖昧な理解だったし、実際に上記の様に作ったアプリでも、更新がめんどいことがあったりしたので、真のクライアントMVCとはなんぞや見たいのを理解するため、もぞもぞ調べていたのだけど、Spine.jsとかいうのがあるぞっていうのを知り、興味を持ったので、このSpine.jsでやってみるかなーと、簡単なサンプルを書いてみたりしていた。

このSpine.jsで色々と書いてみることは、かなり勉強に立った。Backbone.jsと比較すると、Spine.jsのほうが、よりサーバーサイドのクラシックなMVCを模したフレームワークだって言えると思う。Spine.jsから見ると、Backbone.jsのほうが亜種だろうという感じだ。それは、たぶんこの記事のコードと、Spine.jsで同じ物を書いたコードを見比べてみると分かるかもしんない。Backbone.jsの方がちょっとクラスが多いし、ちょっとめんどそうなことをしていたりする。(Spine.jsはBackbone.jsにインスパイアされて作ったらしい)

そんなこんなでSpine.jsを触っていたので、今はSpine.jsを触る前よりも、かなりMVCを理解してると感じてる。Backbone.jsが悪いとかじゃないんだけれども、Backbone.jsは、柔軟に色々出来るように作られているので、実際のところどういう風に書けばMVCっぽく書けるんだ?ていうかViewって何?Controllerは何なの?MVCってそもそも何よ?みたいのを理解するのが、ちょっと難しいかもって思った。この点、Spine.jsだともちっと型にはまった書き方ができるので、素直に理解出来ると思う。継承だのmixinだのも、どうやって使うのかってのがかなりお勉強になった感。ちなみに、Backbone.jsはクラシックなMVCとは違うんだぜっていう記事が以下にある。うやむやだった考えが、これきっかけで色々と理解できたので、興味の有る方は読んでみると良いと思う。(長いけど)

そして、自分の次の問題、MVCは分かったつもりなんだけども、そのMVCらをどーやって管理するの?というのがあった。実際のところ、ほーほー、MVCってそういうことなのね!って分かったとしても、実際のWebアプリは、そのプチMVCが山のようにできるわけで、そいつらをどーすんねんっていうのがいまいちわからんかった。それは上記で挙げたページに書かれているようにやればいいんだろうけど、その、ViewだのModelだのを管理するのはModelなのか?Viewなのか?はたまた?いや、ルーターっていうURLからトリガーするアレもそのひとつではあるけどもそれない場合もあるし、それ使うにせよ使わないにせよ、なんかしらメディエイター的な人がいるよね、それって何でやんの?みたいな細かいところがいまいち分からない。以下の様なこと。

これは、UIのステートを管理するということであると分かってきた。ステートを管理するのはModelなのか?View(Backbone.jsで言う)なのか?っていう感じだ。それで、Spine.jsをみてみたら、Managerというのがあった。おーいいねー。たぶん僕が欲しかったのはコレですよー。って思って今日見てたんだけど、あーコレあんまり何もしてくれないのね…っていうのに気付いた。このManagerとやらがやってくれるのは、複数のクラスインスタンスを登録して、一つがアクティブになったら他のをアクティブじゃなくするみたいなことをちょろっとやってくれるくらいだった。というか、もともとやっぱりそういうのは、そこまで汎用的にできないものなのかも知れない。そんで、そういう事を色々しだすと、やっぱりというかなんというか、Underscore.jsのメソッドを使わざるを得なくなる。そしたら最初っからBackbone.js使えばいいんじゃないですかね?あ、そういえばBackbone.jsのModelって、そういうような、APIをラップするクラス以外にも使えるようにうまいことできてるのかしら…ということで、やっぱ僕はBackbone.jsにしますね。←イマココ

もちっと要研究

gitで前のあのタイミングに一時的に戻りたい

2012/03/02 permalink

git checkout hash

で一時的にそこまで戻る

git checkout master

で元に戻る

あの、すばらしい日々をもう一度 ♪

IEだとcookieにpath=/hoge/foo.htmlみたいな指定は無理

2012/03/01 permalink

知らんくてハマった。

path=/hoge とかじゃないと効いてくれなかった。たぶん、path=/hoge/foo.html と指定すると、/hoge/foo.html/ 以下すべてで有効なcookieになるっぽい。つまり、 /hoge.html みたいなページだけに有効なcookieというのは、path=/ と指定しないとダメなため、実質、ページ固有で有効なcookieというのは、ディレクトリをちゃんと切らないと無理っぽい気がする。他のブラウザではページ固有のcookieは有効。

NOOOOOO

gruntでcompassをやや複雑にビルド

2012/02/28 permalink

※ この記事はgrunt version 0.2.x のもので、grunt 0.3 からはAPIが変更されています

例えばこんなの

htdocs
├── common // サイト共通ファイル格納ディレクトリ
│   ├── css
│   │   ├── all.css     // コンパイルしたやつまとめたい (A)
│   │   ├── all.min.css // そんでminifyしたい (A)
│   │   └── fragments
│   │       ├── base.css
│   │       ├── modules.css
│   │       └── reset.css
│   └── scss
│       ├── base.scss    // ↑cssにコンパイルしたい (A)
│       ├── modules.scss // ↑cssにコンパイルしたい (A)
│       └── reset.scss   // ↑cssにコンパイルしたい (A)
│
├── category1 // カテゴリのディレクトリ
│   ├── css
│   │   ├── style.css     // コンパイルされたやつ (B)
│   │   └── style.min.css // そんでminifyしたい (B)
│   ├── index.html
│   └── scss
│       └── style.scss // ↑cssにコンパイルしたい (B)
│
├── category2 // カテゴリのディレクトリ
│   ├── css
│   │   ├── style.css     // コンパイルされたやつ (C)
│   │   └── style.min.css // そんでminifyしたい (C)
│   ├── index.html
│   └── scss
│       └── style.scss // ↑cssにコンパイルしたい (C)
│
└── index.html // トップページ的な何か

grunt.js作る

var proc = require('child_process');

config.init({
  watch: {
    files: [
      'common/scss/*.scss',
      'category1/scss/*.scss',
      'category2/scss/*.scss'
    ],
    tasks: 'compass concat cssmin notifyOK'
  },
  compass: {
    'common/scss': 'common/css/fragments',
    'category1/scss': 'category1/css',
    'category2/scss': 'category2/css'
  },
  concat:  {
    'common/css/all.css' : [
      'common/css/fragments/reset.css',
      'common/css/fragments/base.css',
      'common/css/fragments/module.css'
    ]
  },
  cssmin: {
    'common/css/all.min.css': 'common/css/all.css',
    'category1/css/style.min.css': 'category1/css/style.css',
    'category2/css/style.min.css': 'category2/css/style.css'
  }
});

task.registerBasicTask('compass', 'compass compile', function( data, name ) {
  var done = this.async();
  var command = 'compass compile --sass-dir ' + name + ' --css-dir ' + data + ' --boring';
  proc.exec(command, function(err, sout, serr){
    if(sout.indexOf('error')>-1){
      proc.exec("growlnotify -t 'COMPASS COMPILE ERROR!!!' -m '" + sout.replace(/^\s*/,'') + "'");
      console.log('ERROR!');
      done(false);
    }else{
      console.log('OK!');
      done(true);
    }
  });
});

task.registerBasicTask('cssmin', 'minify css', function( data, name ) {
  var done = this.async();
  var command = 'sqwish ' + data + ' -o ' + name;
  var out = proc.exec(command, function(err, sout, serr){
      done(true);
  });
});

task.registerTask('notifyOK', 'done!', function(){
  proc.exec("growlnotify -t 'grunt.js' -m '\(^o^)/'");
});

task.registerTask('default', 'compass concat cssmin notifyOK');

これで

grunt watch

ってやるとwatchしてコンパイルしてくれる。エラーと完了でgrowlも出る。
このサンプル一式は以下

gruntについて、コレを実行するのに必要な sqwish, growlnotify, compass については以下参照。

$.ImgLoader + spin.js でローディング待ってギャラリー的な

2012/02/27 permalink

画像をパラパラフェードして切り替える様なUIをJSで作る時、画像をプリロードしておかないとパラパラ画像が出てきてちょっとかっこわるい。でもプリロードするのはめんどかったり、ローディング待ちの表示を出すのもちょっとめんどい。

そもそも、ローディング待ちのくるくるスピナーなアニメGIFって、あれローディング終わったらもう用済みだし、使うにしてもそいつを先にプリロードしておかないと意味ないし、なんかすごい無駄感漂うので微妙って思ってた。

そこで、あのくるくる、CSS3でやっちゃうのあったなーとか思って見てたけど、んーIEがねーって思って色々ググってたら、spin.js ってのを見つけた。これは、ローディングのくるくるを、CSS3(フォールバックでVML)でやってくれちゃうと。これはすごい。これと、jQuery.ImgLoader を使って、Flashでよくあるようなプリロード表現をしてみる。以下は30枚の画像をプリロードした後にギャラリーみたいのをやる。

<div class="gallery">
    <div class="gallery-counter">loading...</div>
    <div class="gallery-main"></div>
</div>
$.fn.gallery = function(options){

  return this.each(function(){

    /* I need els in this */

    var $main = $('.gallery-main', this);
    var $counter = $('.gallery-counter', this);

    /* setup spinner */

    var spinner_options = {
      color: '#333',
      length: 20,
      radius: 30
    };
    var spinner = (new Spinner(spinner_options)).spin($main[0]);
    var $spinner = $(spinner.el); // spinner element

    /* setup loader */

    var $imgs = $();
    var srcs = options.srcs;
    var loader = $.ImgLoader({
      srcs: srcs,
      pipesize: 2,
      delay: 0
    });
    loader.bind('itemload', function($img, i){
      $counter.text( (i+1) + ' / ' + srcs.length );
      $imgs = $imgs.add($img);
    });
    loader.bind('allload', function(){
      $counter.text('loading complete');
      allLoaded();
    });

    /* things after load complte */

    function allLoaded(){
      setTimeout(function(){
        $.when(
          $spinner.fadeOut(),
          $counter.fadeOut(200)
        ).done(function(){
          $spinner.remove(); // we don't need there anymore
          $counter.remove(); // we don't need there anymore
          $imgs.hide().appendTo($main);
          tick();
        });
      }, 400); // I like to wait a little
    }

    function tick(){
      var $all = $('img', $main);
      var $current = $all.eq(0);
      var $next = $all.eq(1).fadeIn(function(){
        $current.appendTo($main).hide();
        setTimeout(function(){
          tick();
        }, 1000);
      });
    }

    loader.load(); // do them all!

  });
};

$(function(){

  /* imgs/1.jpg ... 30.jpg */
  var srcs = [];
  for(var i=1, l=30; i<=30; i++){
    srcs.push('imgs/' + i + '.jpg');
  }

  /* do it */
  $('.gallery').gallery({ srcs: srcs });

});

IE6でもくるくる動いてた。これはなかなかイイかも。

結構、こういうUIを作る時、画像のプリロードを意識しない人が多いかと思うので、興味あれば jQuery.ImgLoader を是非使ってみてくだせー。そもそもプリロードということ自体がなんか微妙にハックっぽくて微妙やなと思ったりはするけれども。

jQuery.ImgLoader - 画像を大量にプリロードさせるやつ

2012/02/25 permalink

画像を裏でプリロードするやつをCoffeeScriptの練習がてら書いた。過去にも似たようなの作ったけど…

いけてるところは以下

  • パイプ機能。パイプサイズ4とか指定すると、4個までしか同時にローディングを走らせないとかそういうことができる。そうした場合、5個目以降は、ローディングが終わった順に実行されていく。大量の画像を一気に取りに行くとブラウザによってはめちゃくちゃ重くなるので、そういう時に有効。
  • ローディングのキャッシュ。例えば1.jpgを読み込めと、100回同時に行ったら、ブラウザは100回画像を取りに行っちゃうかもしれない。なので、100回同時に習得せよとかいう処理が走っても、1個目の画像を取得し、その画像のローディングが終わるまでは、2個目以降の画像を取りに行かない。1個目の画像のローディングが終わってから取りに行けば、2個目以降はサーバーに取りに行かないかもしれない。(これに意味があるのかわからないけど)
  • クロスブラウザな画像のオリジナルのwidth, heightを検知する機能もある(コレもマージした)

前のエントリで書いた grunt とやらでビルドするようにしてみた。CoffeeScriptにも慣れてきた。ついでにgitHub pagesとやらにデモを置いてみた。テストも結構ちゃんと書いた。

gruntでcssをminify

2012/02/23 permalink

※ この記事はgrunt version 0.2.x のもので、grunt 0.3 からはAPIが変更されています

gruntについて

sqwishを-gでinstall

npm install -g sqwish

grunt.js

var proc = require('child_process');

config.init({
  concat:  {
    'common/css/all.css' : [
      'common/css/_src/style1.css',
      'common/css/_src/style2.css',
      'common/css/_src/style3.css'
    ]
  },
  watch: {
    files: [
      'common/css/_src/style1.css',
      'common/css/_src/style2.css',
      'common/css/_src/style3.css'
    ],
    tasks: 'concat cssmin'
  },
  cssmin: {
    'common/css/all.min.css':  'common/css/all.css'
  }
});

task.registerBasicTask('cssmin', 'minify css', function( data, name ) {
  var done = this.async();
  var command = 'sqwish ' + data + ' -o ' + name;
  var out = proc.exec(command, function(err, sout, serr){
      done(true);
  });
});
grunt watch

以下のようにminifyされる

.
├── common
│   └── css
│       ├── _src /* components */
│       │   ├── style1.css
│       │   ├── style2.css
│       │   └── style3.css
│       ├── all.css
│       └── all.min.css /* sqwished! */
├── grunt.js
└── index.html

compassとやら試した

2012/02/23 permalink

sassがLESSがどうのと前々から言われていたけども僕はさっぱり1mmも試したこと無かったので試した。なんかcompassというやつがいいとのことで、インストールした。

どこぞのサイト見つつ

compass create

とかやったら

.
├── config.rb
├── sass
│   ├── ie.scss
│   ├── print.scss
│   └── screen.scss
└── stylesheets
    ├── ie.css
    ├── print.css
    └── screen.css

ができた。config.rbが設定ファイルで、基本この中をいじれば設定が変えられる。画像のパス指定先とか。そんで、

compass watch

とかやると、sassディレクトリ内のファイル更新に合わせてcssファイルをstylesheetsディレクトリに作ってくれる。

あとは、.scssファイルにsassを書いていけばいいという感じ。別に使いそうなやつだけ覚えればいいでしょと思ったので一通りドキュメントを見て、以下を使いそうっていうメモ。

@import "compass/css3";
@import "compass/typography";

#test1{

  /* css3 things */
  @include border-radius(10px);
  @include box-shadow(red 2px 2px 10px);
  @include inline-block;
  @include opacity(0.5);
  @include single-text-shadow;
  @include transition-property(width);
  @include transition-duration(2s);

  /* typography */
  @include link-colors(red, blue, black, white, white);
  @include nowrap;
  @include force-wrap;

  /* helper */
  background:image-url(hoge) no-repeat 0 0;
  @include clearfix;

}
#test2{
  /* helper */
  @include has-layout;
}

これが、コンパイルされると以下のようになる(以下は僕の方で整形した奴)

#test1 {

  /* ===== css3 things ===== */

  -moz-border-radius: 10px;
  -webkit-border-radius: 10px;
  -o-border-radius: 10px;
  -ms-border-radius: 10px;
  -khtml-border-radius: 10px;
  border-radius: 10px;

  -moz-box-shadow: red 2px 2px 10px;
  -webkit-box-shadow: red 2px 2px 10px;
  -o-box-shadow: red 2px 2px 10px;
  box-shadow: red 2px 2px 10px;

  display: -moz-inline-box;
  -moz-box-orient: vertical;
  display: inline-block;
  vertical-align: middle;
  *vertical-align: auto;

  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=50);
  opacity: 0.5;

  text-shadow: #aaaaaa 0px 0px 1px;

  -moz-transition-property: width;
  -webkit-transition-property: width;
  -o-transition-property: width;
  transition-property: width;

  -moz-transition-duration: 2s;
  -webkit-transition-duration: 2s;
  -o-transition-duration: 2s;
  transition-duration: 2s;

  /* ===== typography ===== */

  color: red; /* link-colors */

  white-space: nowrap;

  white-space: pre;
  white-space: pre-wrap;
  white-space: pre-line;
  white-space: -pre-wrap;
  white-space: -o-pre-wrap;
  white-space: -moz-pre-wrap;
  white-space: -hp-pre-wrap;

  word-wrap: break-word;

  /* ===== helper ===== */

  background: url('/images/hoge') no-repeat 0 0;
  overflow: hidden;
  *zoom: 1;

}
#test1 {
  *display: inline; /* inline-block */
}
/* link-colors */
#test1:visited { color: white; }
#test1:focus { color: white; }
#test1:hover { color: blue; }
#test1:active { color: black; }

/* ===== helper(has-layout) ===== */

#test2 { *zoom: 1; }

-webkit-だの-moz-だのはもちろん、opacityだのclearfixだのinline-blockだのはよく使うのでこんだけ覚えておくだけでもなかなか楽になりそう。オプション指定して事項すればminifyみたいのもしてくれるので使ってみる。

vimでインデントの方法をトグル

2012/02/23 permalink

最近1インデント2スペースだの4スペースだの1タブだの色々あるんで、コマンドでちょろっと切り替えられるようにした。

gruntで快適JS/CSSビルド生活

2012/02/23 permalink

※ この記事はgrunt version 0.2.x のもので、grunt 0.3 からはAPIが変更されています

grunt というJS/CSSのビルドツールが便利だったので紹介します。(Mac/Linux)

このgruntってのは、JS,CSSを全部まとめて繋げる、まとめてJS lintする、minifyする見たいのをタスクとして登録しておくと、それ実行すればちゃちゃっとやってくれちゃうやつです。さらにwatchっていう機能使えば、ファイルが更新されたらそのタスクをやってくれるみたいなのも。

似たモノで、MakeとかRakeとかCakeとかそういうのがあります。多分、やってる人は前からやってるんですが、最近ちょっとCoffeeScriptをやり始めて、微妙に困ったことがあったので、どーすっぺと探してたら、これが自分にとってソリューションでした。

ここでは、3ステップでCoffeeScriptをコンパイルしてgrowlを出すところまでです。ここで使うサンプルは以下にまとめましたので必要であれば参考にでもすると良いかと思います。

とりあえず、gruntのインストールには、nodeとnpmが必要です。それインストールしたら以下を実行して準備OK。これでgruntコマンドが使える。

npm install -g grunt

step1: jsとcssまとめる。jsはminify

あーいっぱいcssとかjsとかあるけどまとめたい。さらにminifyもして欲しいわーという時。

.
├── common
│   ├── css
│   │   ├── _src # ここにcssのファイルら
│   │   │   ├── style1.css
│   │   │   ├── style2.css
│   │   │   └── style3.css
│   │   └── all.css # まとめたcss
│   └── js
│       ├── _src # ここにjsのファイルら
│       │   ├── script1.js
│       │   ├── script2.js
│       │   └── script3.js
│       ├── all.js     # まとめたjs
│       └── all.min.js # まとめたjsをminifyしたやつ
├── grunt.js # gruntの設定ファイル
└── index.html

上記みたいにディレクトリがあって、grunt.jsにこう書く。

config.init({
  lint: {
    files : [
      'common/js/_src/script1.js',
      'common/js/_src/script2.js',
      'common/js/_src/script3.js'
    ]
  },
  concat:  {
    'common/js/all.js' : [
      'common/js/_src/script1.js',
      'common/js/_src/script2.js',
      'common/js/_src/script3.js'
    ],
    'common/css/all.css' : [
      'common/css/_src/style1.css',
      'common/css/_src/style2.css',
      'common/css/_src/style3.css'
    ]
  },
  watch: {
    files: [
      'common/js/_src/script1.js',
      'common/js/_src/script2.js',
      'common/js/_src/script3.js',
      'common/css/_src/style1.css',
      'common/css/_src/style2.css',
      'common/css/_src/style3.css'
    ],
    tasks: 'lint concat min'
  },
  min: {
    'common/js/all.min.js': [ 'common/js/all.js' ]
  }
});

そして、grunt.jsが置いてあるディレクトリまで行き、

grunt watch

って打てばOK。_srcディレクトリに入れたファイルが更新されたらJS lintしてファイルまとめてminifyしてくれる。all.min.jsとall.cssを読み込んだがコレ

step2: CoffeeScriptコンパイルし、jsとcssまとめる。jsはminify

やりたかったのがコレ。CoffeeScriptには、Cakeというビルドをまとめる方法があるんだけれども、CoffeeScriptまとめた後、他のJSとつなげたりどうのしたりっていうのが、なんか自分で書かないと行けないっぽかった。CoffeeScript使うとは言っても、他にjQueryプラグインだの何だのを使ったりするので、コンパイルしたファイルをまた他のとつなげたりしないといけない。なんかめんどいなーと思ってたら素敵grunt。

さっきのscript1.js、script2.jsをCoffeeScriptにした例。

.
├── common
│   ├── css
│   │   ├── _src
│   │   │   ├── style1.css
│   │   │   ├── style2.css
│   │   │   └── style3.css
│   │   └── all.css
│   └── js
│       ├── _src
│       │   ├── script1.coffee # CoffeeScriptなファイル
│       │   ├── script2.coffee # CoffeeScriptなファイル
│       │   └── script3.js
│       ├── _temp_coffeecompiled.js # コンパイルしたCoffeeScript
│       ├── all.js
│       └── all.min.js
├── grunt.js
└── index.html

grunt.jsの中身はコレ

var proc = require('child_process');

config.init({
  lint: {
    files : [
      'common/js/_src/script3.js'
    ]
  },
  concat:  {
    'common/js/all.js' : [
      'common/js/_temp_coffeecompiled.js',
      'common/js/_src/script3.js'
    ],
    'common/css/all.css' : [
      'common/css/_src/style1.css',
      'common/css/_src/style2.css',
      'common/css/_src/style3.css'
    ]
  },
  watch: {
    files: [
      'common/js/_src/script1.coffee',
      'common/js/_src/script2.coffee',
      'common/js/_src/script3.js',
      'common/css/_src/style1.css',
      'common/css/_src/style2.css',
      'common/css/_src/style3.css'
    ],
    tasks: 'coffee lint concat min'
  },
  min: {
    'common/js/all.min.js': [ 'common/js/all.js' ]
  },
  coffee: {
    'common/js/_temp_coffeecompiled.js': [
      'common/js/_src/script1.coffee',
      'common/js/_src/script2.coffee'
    ]
  }
});

task.registerBasicTask('coffee', 'compile CoffeeScripts', function(data, name) {
  var done = this.async();
  var command = 'coffee -j ' + name + ' -c ' + data.join(' ');
  var out = proc.exec(command, function(err, sout, serr){
    if(err || sout || serr){
      done(false);
    }else{
      done(true);
    }
  });
});

ここでは、coffeeというオレオレタスクをgruntに登録している。そんで、その中では、coffeeプロパティとして登録されたデータを見て、まとめてコンパイルするという流れになってる。そして、watch.tasksにcoffee lint concat minとすれば、さっきと同じ、ファイルが更新されたら、CoffeeScriptをコンパイル、JS lintして、ほかのJSと繋げてall.jsを作り、minify。こんな感じで、複雑なタスクもgruntを使えばかなり手軽に設定できる。(サンプル2)

step3: CoffeeScriptコンパイルし、jsまとめる。終わったらgrowl出す

Mac使ってる人ならほぼほぼ入れていると思われるgrowl。このgrowlは、インストーラに付いてるExtraを入れると、コマンドを打ってgrowlを出すことの出来る機能が追加される。【参考

growlnotify -t "タイトル" -m "メッセージ"

これを、ビルドが終わった時、CoffeeScriptのコンパイルが失敗した時に出す。構成はさっきと同じだけどcss外した。

.
├── common
│   └── js
│       ├── _src
│       │   ├── script1.coffee
│       │   ├── script2.coffee
│       │   └── script3.js
│       ├── _temp_coffeecompiled.js
│       ├── all.js
│       └── all.min.js
├── grunt.js
└── index.html
var proc = require('child_process');

config.init({
  lint: {
    files : [
      'common/js/_src/script3.js'
    ]
  },
  concat:  {
    'common/js/all.js' : [
      'common/js/_temp_coffeecompiled.js',
      'common/js/_src/script3.js'
    ]
  },
  watch: {
    files: [
      'common/js/_src/script1.coffee',
      'common/js/_src/script2.coffee',
      'common/js/_src/script3.js'
    ],
    tasks: 'coffee lint concat min notifyOK'
  },
  min: {
    'common/js/all.min.js': [ 'common/js/all.js' ]
  },
  coffee: {
    'common/js/_temp_coffeecompiled.js': [
      'common/js/_src/script1.coffee',
      'common/js/_src/script2.coffee'
    ]
  }
});

task.registerBasicTask('coffee', 'compile CoffeeScripts', function(data, name) {
  var done = this.async();
  var command = 'coffee -j ' + name + ' -c ' + data.join(' ');
  var out = proc.exec(command, function(err, sout, serr){
    if(err || sout || serr){
      proc.exec("growlnotify -t 'COFFEE COMPILE ERROR!' -m '" + serr + "'");
      done(false);
    }else{
      done(true);
    }
  });
});

task.registerTask('notifyOK', 'done!', function(){
  proc.exec("growlnotify -t 'grunt.js' -m '\(^o^)/'");
});

CoffeeScript書いてる時、エラーあるのが分かりづらいのでコレは便利!(サンプル3)

ほか、ファイルを繋げるときに好きにコメントを入れたりとかもできるので、なんか色々そういうのをやるときは融通が効きそう。ということで、ちょっとしばらく使ってみようと思う。サンプルのgruntファイルなんかもgithubに置いてある。