Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
13
Help us understand the problem. What is going on with this article?
@KitaitiMakoto

JavaScriptでSassのカスタム関数を作る

More than 5 years have passed since last update.

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.rendersass.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の段階で書いています。

13
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
13
Help us understand the problem. What is going on with this article?