Sassカスタム関数
元々のSassはRubyで書かれていて、Rubyで拡張することで追加の関数を作ることができます。これはカスタム関数と呼ばれています(@function
ディレクティブを使ってSassの文法だけで関数を定義する関数ディレクティブとは別です)。
例えばCompassというSassミックスイン集+αみたいなツールがあって、これを使うと
.book-cover {
width: image-width("path/to/image");
max-width: 36px;
}
と書くことで、コンパイル時に画像ファイルの横幅を取得し、それを設定することができます。
ところが、最近は多くの環境がそうだと思うのですが、gulpなどでSassを使うためにnode-sass
を使っていると、当然ながらRubyでカスタム関数を書くというわけにはいきません。のでCompassの便利関数なども使えなくなってしまっています。
(余談ですがSassの文法のみで定義されたミックスインは、compass-mixinsというNPMパッケージで使うことができます。)
これはnode-sass(などのlibsassバインディング達)への移行に踏み切りにくい、また移行しても不満を覚えるところでした。ところが、実はnode-sassのバージョン3から、JavaScriptでカスタム関数を書けるようになっていました: functions
JavaScript API
試しに「画像などのファイル名を与えたら、ファイル名にハッシュ値を付ける」というimage-url
関数を作ってみましょう。
h1.site-name {
background: image-url("/assets/images/logo.svg") no-repeat;
// ...
}
と書いたら、
h1.site-name {
background: url("/assets/images/logo-d8913881ba8b302dc6289957112e64f0.svg") no-repeat;
// ...
}
というCSSにコンパイルする関数です。
/assets/images/logo.svg
というファイルを/assets/images/logo-d8913881ba8b302dc6289957112e64f0.svg
というファイル名に変更する処理は既にやっておいているものとします(gulp-revなどを使うのがいいでしょう)。
CloudFrontのキャッシュ削除のタイムラグを回避したりするために使えます。
使う関数の指定
JavaScriptからカスタム関数を使うには、render
またはrenderSync
関数にfunctions
というオプションを渡します。
var sass = require("node-sass");
var compiled = sass.renderSync({
file: "path/to/style.scss",
outputStyle: "expanded",
functions: {
"image-url($url)": imageUrl
}
});
var cssString = compiled.css.toString();
console.log(cssString);
// h1.site-name {
// background: url("/assets/images/logo-d8913881ba8b302dc6289957112e64f0.svg") no-repeat;
// ...
// }
functions
オプションはオブジェクトになっていて、プロパティ名がSass関数としてのシグニチャー(名前と引数。オプション引数のデフォルト値なども指定可)、値が関数の実体です。同じ関数でも、プロパティ名を変えることで別の関数にすることができます。例えば、ここではimage-url
という名前にしていますが、やることはフォントファイルでも変わらないので、font-url
という名前で同じ関数を使い回してもいいでしょう。
JavaScriptで関数の実体を書く
functions
オプションで指定した関数には、定義したシグニチャーの通りの引数が渡ってきます。
function imageUrl(url) {
// ...
}
ここでurl
引数は、Sass内でimage-url
に渡した文字列ではなくて、渡した文字列を表す**特別なSassのオブジェクト(SassString
)**です。getValue
関数を呼び出すことでJavaScriptの文字列にすることができます。
function imageUrl(url) {
var urlValue = url.getValue();
// ...
}
戻り値も、JavaScriptの文字列(や、用途に応じて数値など)ではなくてSass用のオブジェクトに変換しておく必要があります(下のsass.types.String()
のところ)。
function imageUrl(url) {
var urlValue = url.getValue();
// ...
var returnString = 'url(" + urlValue + '")';
return sass.types.String(returnString);
}
(使えるSassの型一覧はnode-sass-functionsにあります。)
これでimage-url("/assets/images/logo.svg")
をurl("/assets/images/logo.svg")
に置き換えるだけのSassカスタム関数が出来ました。あとは通常のJavaScriptプログラミングで、途中を埋めていくだけなので省略します。
var fs = require("fs");
var path = require("path");
var crypto = require("crypto");
var sass = require("node-sass");
var baseDir = "path/to/dir";
function imageUrl(url) {
var urlString = url.getValue();
var parsedUrl = path.parse(urlString);
var realPath = fs.realpathSync(path.join(baseDir, url));
var content = fs.readFileSync(realPath);
var hash = crypto.createHash("md5").update(content).digest("hex");
parsedUrl.name += "-" + hash;
parsedUrl.base = parsedUrl.name + parsedUrl.ext;
var returnString = 'url("' + path.format(parsedUrl) + '")';
return sass.types.String(returnString);
}
(baseDir
が固定とか、ハッシュが長過ぎるとか、毎回画像の中身を読み込んでいるのやばいとか、同期API使ってるとか、とか、記事の趣旨には影響しない所は適当にやっているのが色々あるのでそのままは使わないでください。実際はgulp-revとかでファイル名変更のテーブルが出来ていると思うので、gulpのストリーム経由で渡してそれを参照するのがいいと思います。)
これで実際に、SassファイルやSCSSファイル中でimage-url
関数を使うことができるようになります。
@function
ディレクティブを使った関数定義では、ハッシュ値の計算などは(大変そうだけど)できるかも知れませんが、「ファイルを読み込む」というところができないはずです。これがnode-sassバージョン3から入ったカスタム関数の力です。
非同期の場合
sass.renderSync
で呼ぶ時は、関数からSass用のオブジェクトを返すだけでよかったですが、sass.render
で非同期に呼ばれる場合には別の方法が必要です。
関数の最後の引数として完了報告用の関数が渡されるので、それを呼びます。
function imageUrl(url, done) {
var urlString = url.getValue();
// ...
done(sass.types.String(returnString));
}
renderSync
の場合には関数になってないので、条件分岐する必要があるでしょう。
function imageUrl(url, done) {
var urlString = url.getValue();
// ...
var replacement = sass.types.String(returnString);
if (typeof done === "function") {
done(replacement);
} else {
return replacement;
}
}
コマンドライン
コマンドラインのインターフェイスをもあります。node-sassパッケージについてくるnode-sass
コマンドにfunctions
オプションを渡すことで、自作の関数を有効化した上でSassファイルを変換できます。
$ $(npm bin)/node-sass --functions=image-url.js ./app/assets/stylesheets/common.scss
h1.site-name {
background: url("/assets/images/logo-d8913881ba8b302dc6289957112e64f0.svg") no-repeat;
// ...
}
ここでfunctions
オプションに渡すimage-url.js
ファイルはこんな風になっています。
functions imageUrl(url) {
// ...
}
module.exports = {
"image-url($url)": imageUrl
};
sass.render
(sass.renderSync
)のfunctions
オプションに渡していたのと同じオブジェクトを、今度はmodule.exports
として設定します。
そういう機会があるかちょっと分かりませんが、これでコマンドラインからもカスタム関数を使えるようになりました。
注意
(今の所)カスタム関数内から、Sassとして設定されている変数等は参照できないようです。本当は
$image-url-base-dir: "./assets/images";
h1.site-name {
background: image-url("/assets/images/logo.svg") no-repeat;
}
などと柔軟に設定をしたいところなのですが(まあ多くの場合gulpで使うでしょうから、設定はできるでしょう)。
node-sassページにも書いている通りまだ実験段階の機能なのでご注意を。この記事は3.2.0の段階で書いています。