キャッシュバスターの文字列が画像名に入ったスプライト画像が生成される問題を、 パフォーマンスを損なわずに解決する方法。

Compassでスプライト画像を書き出す際の問題点

スプライト画像が*-s[画像バイナリのMD5ハッシュ].pngの様に画像名内にキャッシュバスターが入ることで、スプライト画像に変更が入ると画像名が変わってしまう。これはバージョン管理や差分管理する上で不都合となることが多い。 このハッシュはbackground-image:url();で指定した画像を変更する際に、ブラウザがキャッシュを参照しないようにキャッシュ対策(キャッシュバスターと呼ばれている)として入れられている文字列だ。

コールバックを使う方法で生まれる問題点

Compassに用意されているコールバックを使った既出の解決法がある。

css - How to turn off COMPASS SASS cache busting? - Stack Overflow

この方法では、on_sprite_savedコールバックでスプライト画像をリネームし、on_stylesheet_savedでコンパイルされたCSSを書き直している。 これはコードとしての独立性が高く、キャッシュバスターを有効にしたままスプライト画像の画像名からハッシュを取り除く上手い方法だが、CSSを書き直す度にスプライト画像が書き出されてしまい1Compassの処理完了までに時間がかってしまう。

  1. Compassには前回書き出したスプライト画像を書き出し直す必要があるかを検査する機構があり、前回書きだしたスプライト画像が存在しない場合書き出し直すという処理を行っている。 「スプライト画像をリネーム」したことで「前回書きだしたスプライト画像が存在しない」と判定され、毎回スプライト画像の生成処理が走ってしまう。

モンキーパッチで解決する

Compass v0.12.2 で動作確認。

スプライト画像のファイル名にハッシュを入れない代わりに、CSSコンパイル時に*.png?[画像バイナリのMD5ハッシュ]のようにサーチクエリをつけて書き出す。 「コールバックを使う方法」と処理結果は同じだが、Sassリコンパイル時にスプライト画像を再生成する必要が無ければ生成処理を行わないため、前述の問題が発生しない。

module Compass
  module SassExtensions
    module Functions
      module Sprites
        # CSSコンパイル時にハッシュをサーチクエリにつける
        def sprite_url(map)
          verify_map(map, "sprite-url")
          map.generate
          generated_image_url(Sass::Script::String.new("#{map.path}.png?#{map.uniqueness_hash}"))
        end
      end
    end
    module Sprites
      module SpriteMethods
        # ハッシュ抜きのファイル名を返す
        def name_and_hash
          "#{path}.png"
        end
        # ハッシュ入りの画像ファイルは書き出されなくなっているはずなので古いファイルの削除は行わない
        def cleanup_old_sprites
          # do nothing
        end
      end
    end
  end
end

まとめ

「コールバックを使う方法」採用時はスプライト画像を多く使うプロジェクトでは、コンパイル時に時間が掛かることでSassの編集作業にストレスを感じることが多かった。 作業軽減のために採用したCompassの利用でストレスを感じるのは本末転倒なので、この方法を採用したことで得られるものは大きかった。

ただし、今回紹介した方法はCompassモジュールのバージョンが変わったら動かなくなる可能性があるので注意が必要だ。

minodisk/coffee-refactorを作成中にindex.coffeeを読んでわかったことのメモ。

nodes.coffee

Code#compileNode()

ノードをコンパイルする。 コンパイルの過程でScopeインスタンスが作成される。 オプションに親のスコープを渡すとvariablesは親のスコープ内で宣言された変数を考慮した変数のリストとなる。

options =
  scope: parent.scope
  indent: ''
code.compileNode options
scope = options.scope
console.log scope.variables

scope.coffee

Scope#declaredVariables()

スコープ内でvarで宣言される変数の配列を返す。パラメータは含まれない。

スコープ内で定義される全ての名前空間を取得するためには、さらにパラメータを含む必要が有るため下記のコードを使う。

declaredSymbols = (scope) ->
  realVars = []
  tempVars = []
  for v in @variables when v.type is 'var' or v.type is 'param'
    (if v.name.charAt(0) is '_' then tempVars else realVars).push v.name
  realVars.sort().concat tempVars.sort()

逆引き的ななにか。

Package書くときに使う

パッケージのSettingsにチェックボックスを表示する

module.exports =
  configDefaults:
    highlightReference: true
    highlightError    : true

文法がCoffeeScriptかを調べる

  • Grammar::scopeName
editor = atom.workspaceView.getActiveEditor()
scopeName = @editor.getGrammar().scopeName
console.log scopeName is 'source.coffee' or scopeName is 'source.litcoffee'

カーソル下の単語の範囲の取得する

  • Cursor::getCurrentWordBufferRange()
editor = atom.workspaceView.getActiveEditor()
cursor = editor.cursors[0]
range = cursor.getCurrentWordBufferRange includeNonWordCharacters: false

WorkspaceView#commandをoffする

WorkspaceView#command()ではWorkspaceView.data('documentation')にeventName/docStringを登録してるだけで、イベント自体はjQueryのon()で聞いてる。 よってoffしたい時は普通にWorkspaceView#off()すればよい。

atom.workspaceView.command 'hoge:action', handler
atom.workspaceView.off 'hoge:action', handler

Spec書く時に使う

CoffeeScriptの言語パッケージをロードする

  • PackageManager::resolvePackagePath(packageName): パッケージ名からパッケージのパスを取得する
  • Syntax::loadGrammarSync(grammarPath): 言語パッケージをロードする
languageCoffeeScriptPath = atom.packages.resolvePackagePath 'language-coffee-script'
grammarDir = path.resolve languageCoffeeScriptPath, 'grammars'
for filename in fs.readdirSync grammarDir
  atom.syntax.loadGrammarSync path.resolve grammarDir, filename

参考サイト

jQueryではIE8でtitleへのアクセスができないので直接アクセスする。

$('title').text('foo');
document.title = 'foo';

依存パッケージをインストールしておかないとnode-canvasのインストールでコケる。

CentOS

yum install cairo cairo-devel cairomm-devel libjpeg-turbo-devel pango pango-devel pangomm pangomm-devel giflib-devel
npm install canvas

MacOSX

brew install cairo
export PKG_CONFIG_PATH=/opt/X11/lib/pkgconfig
npm install canvas

即時関数の引数に別名をつける

jQuery のプラグイン書くときとかに使う。

do ($ = jQuery) ->
  console.log $

↓コンパイル

(function($) {
  return console.log($);
})(jQuery);

オブジェクトリテラルでオブジェクトを作る

a = 3
b = true
c = 'bar'
console.log { a, b, c }  # => { a: 3, b: true, c: 'bar' }

分解代入で纏めて変数宣言

a = b = c = null

の代わりに下記のように書くことができる。

[ a, b, c ] = []

↓コンパイル

var a, b, c, _ref;

_ref = [], a = _ref[0], b = _ref[1], c = _ref[2];

superを引数付きでコールする

super()ではなくsuperと書くことで引数を引き継いでコールしてくれる。

class Foo
  constructor: (@a) ->
class Bar extends Foo
  constructor: ->
    super
bar = new Bar 3
console.log bar.a  # => 3

便利な反面、superと書いた時点で継承元の関数が実行されることに気をつけなければならない。

class Foo
  func: ->
    console.log 'executed!!'
    'foo'
class Bar extends Foo
  func: ->
    @superFunc = super
    console.log @superFunc
bar = new Bar()
bar.func()  # => 'executed!!'
            # => 'foo'

このようにsuper@superFuncに格納するつもりで書いたコードを実行すると、 superを実行した戻りが@superFuncに格納されることになる。

比較演算子

比較が捗る。

a <= b < c

↓コンパイル

(a <= b && b < c);

分解代入とクラスのthis代入の合わせ技

class Foo
  constructor: ({@a})->

foo = new Foo a: 3
console.log foo.a  # => 3

Array comprehensionsのシュガー

ワンライナーで。

countdown = (num for num in [10..1])

即時関数で。

countdown = do -> num for num in [10..1]

引数の並置

a 1, 2, 3
a 1,
  2,
  3
a 1,
  2
  3

↓コンパイル

a(1, 2, 3);

Backbone.history 関連