脱Compassしたいけど@importでCSSスプライトを作る機能は捨てがたかったので作った話

この記事はCSS Advent Calendar 2016の23日目の記事です。
こんにちは、技術推進室の色川です。
今年も残すところわずかとなりましたがいかがお過ごしでしょうか。
私はというとこの一年取り組んできた社内のフロントエンド開発環境整備の締めくくりとして、とあるサービスで未だ使われていた Compass からの脱却に勤しんでおりました。
Compass といえばかつては「CSS を書くなら Compass を使え」とまで言う人もいたほどですが、その機能の多くは PostCSS に取って代わられ、ここ数年は話を聞くこともなくなりました。GitHub にも「Compass is no longer actively maintained.」とか「Depreciated: Compass is no longer supported.」などと書かれており、もう完全に過去のものとなってしまったようです。
そんな状況の Compass ですが、脱却を考えたのは別の2つの理由にあります。
1つは速度です。Compass を使う以上 Ruby 版の Sass を使わざるを得ないところ、Ruby 版は node-sass(C/C++ で書かれた LibSass の Node.js バインディング) と比べ格段に遅いのです。
もう1つは環境整備の煩雑さです。Ruby を使うのでその環境構築手順等を保守しなければならず、やれ動かないとか Windows だとどうすればといったトラブルシュートもその分増えます。Node.js で完結できるならそれに越したことはありません。
そんなわけで Compass から、というより Ruby から脱却したいと考えたわけです。
Compass とは何だったのか
Compass を知らずして脱却もないわけですが、導入した前任者は既に退職し、私自身は未経験。一番の課題は「Compass よう分からん」ということで、2016年も終わろうかというのに Compass 入門したりしてました。
それで分かったのは、大雑把に言えば「Compass = Mixin と Function のライブラリ集 + CSS スプライト生成機能」だということです。他にも、コンパイルやいわゆる scaffold を行うコマンドなども含まれていましたが、利用していたのは主にその2機能でした。
移行方法を模索する
目的が「脱 Ruby」になっていたので、まずは Node.js 版の Compass がないか探しました。 すぐに compass-node というライブラリが見つかりましたが、こちらも随分メンテナンスされていないせいか弊社の環境では動きませんでした。
気を取り直して探していると、今度は compass-importer というライブラリを見つけました。
これは Sass の @import
の処理をフックする importer として動作するもので、@import "compass";
という記述があると Compass のファイルを読み込んでくれます。
Mixin と Function についてはどれが Compass のものか名前だけで判断するのが難しく、利用箇所を漏れなく洗い出すのが困難に思われたので、これを使うことにしました。
残すは CSS スプライト生成機能ですが、spritesmith を使った gulp タスクを書いて Compass とオサラバした、みたいなエントリは見つかっても、Compass のように @import "my-icons/*.png";
などと書いておけば自動的に処理してくれるものはありません。
見つかったエントリと同じようにそこだけ別の方法で処理することも考えたのですが、どう見ても Compass の方が簡単で汎用的です。Mixin などについてはだいぶラクできたので、ここはひとつ作ってみることにしました。
Compass 互換の CSS スプライト生成ライブラリ
compass-importer のおかげで @import
をフックできることが分かったので、いくつかの importer ライブラリのコードを読みつつ、CSS スプライトを生成するライブラリを書きました。
sprite-magic-importer
特徴
- Sass コード内に
@import "my-icons/*.png";
のように書くだけでスプライト画像が生成され、それを利用するためのコードが埋め込まれます。 all-#{$map}-sprites
や#{$map}-sprite()
など、一部の mixin も定義します。- Magic Selectors にも対応しています。
- 一部の設定変数 はそのまま利用できます。
- 本家にはない機能として Retina 対応もしています。
本家との違い
- スプライト画像の生成に関する設定は、Sass の変数ではなくライブラリのオプションで指定する必要があります。
- 内部では spritesmith を利用しているため、レイアウトの指定方法など、細かいところでの互換性はありません。
- 画像のハッシュ値はファイル名には入らず、
background-image
の URL パラメータとして付与されます。(スプライト画像ファイル名は固定です。) sprite-map()
を使ったスプライト生成や、それと合わせて利用する関数群は利用できません。(これを実現するには functions を使う必要があるようです。)
Node.js で Compass を処理する
node-sass の importer オプションは、配列にすることで複数の importer を使うように指示できます。これを利用して compass-importer と sprite-magic-importer を指定すれば、コードを大幅に修正することなく Compass を処理できるはずです。
ところが、上記の順で指定したところ、スプライト生成の指定のところでエラーになってしまいました。
Error in plugin 'sass' Message: src/sass/app/_sprite.scss Error: File to import not found or unreadable: app/*.png Parent style sheet: /Users/...snip.../frontend/src/sass/app/_sprite.scss on line 5 of src/sass/app/_sprite.scss >> @import "app/*.png"; ^
調べたところ、importer は自分が処理しないパラメータをスルーすべきところ、compass-importer は "compass"
以外の指定でもパスを解決していたため、後続の importer に処理が回っていませんでした。
仕方がないので以下のように順番を変えてやると、今度こそ Mixin も CSS スプライトも正しく処理することができました。
var sass = require('gulp-sass'); var SpriteMagicImporter = require('sprite-magic-importer'); var CompassImporter = require('compass-importer') gulp.task('build:compass', function() { return gulp.src('src/sass/**/*.scss') .pipe(sass({ importer: [ SpriteMagicImporter({ ... }), CompassImporter ], outputStyle: 'expanded' })) .on('error', sass.logError)) .pipe(gulp.dest('path/to/dist')); });
まとめ
- Ruby/Compass を使っている場合は、今回紹介した方法ですんなり node-sass に移行できるかもしれません。
- sprite-magic-importer 単体で使っても便利なので(Compass のスプライト機能が便利なわけですが)、よかったら試してみてください。
※HTTP/2 の世界では CSS スプライトは不要らしいんですけどね…