Gulpとcssnextを使って標準仕様のシンタックスでCSSの機能を拡張する

CSSのプリプロセッサはSassがデファクトスタンダードになっています。個人的にもずっとSassを使っていて、CSSをうまく管理するためには必要だと思っています。
ただ、Sassの機能はCSSの仕様から外れた独自のものです。標準仕様の変数であるCustom Propertiesのように勧告候補ではあるものの比較的ブラウザの実装が進んでいるものもあります。
Sassで使っているような機能(やそれ以上の可能性をもった機能)がツールを通さなくても使えるように次世代の仕様が少しずつですが進められています。
今回はCSSの次世代の標準仕様にもとづいたシンタックスで機能を拡張できるcssnextというプラグインをGulpで導入する手順を紹介します。
PostCSSとcssnext
今回使用するPostCSSとcssnextについて簡単に説明をします。
PostCSSというのはCSSを解析するためのnode.js製のツールのことです。有名どころではAutoprefixerというベンダープレフィックスを自動で付与してくれるプラグインがPostCSSで作られています。
こちらの記事が詳しいです。
PostCSSとは何か - morishitter blog
PostCSSにはcssnextという仕様が固まりきっていなかったり、ブラウザの実装が進んでいない次世代の標準仕様の機能を擬似的に今使えるコードに変換してくれるプラグインがあります。正しくは次世代の標準仕様を使うためのいくつかのPostCSSのプラグインをまとめたものです。
必要なプラグインだけを導入してもいいのですが、仕様やブラウザ対応などを把握しながら複数の案件でプラグインを管理していくのは大変なので、オールインワンなcssnextを選びました。
SassのコードをCSSの次世代の標準仕様に書き換えることは運用フェーズに入ってしまうと難しくなります。cssnextを使って書くということは、寿命の長いCSSを書くための1つの方法だと考えています。
注意点として、PostCSSを通したCustom Propertiesはあくまで静的なもので、Sassの変数($)と機能的には変わりません。
本来のCustom Propertiesはcalc()のように動的に動作(表示されるときに解釈)します。PostCSSではシンタックスを同じにすることはできますが、機能を完全に補うことはできません。
Gulpとcssnextの準備
ここから実際にGulpでcssnextを使うための手順を紹介します。
まずはGulpでcssnextを使うためにpackage.jsonを作成します。以降の手順はMacでターミナルを使ったものですが、パッケージのインストールなどを除けばターミナルを使わなくてもかまいません。
コードはGitHubにアップしていますので、確認してみてください。
最終的には以下のようなディレクトリになります(最初は_module.cssというファイルはありません)。変換前のコードをsrcディレクトリで作業をして、変換後のコードをdestディレクトリに出力します。
gulp-postcss-test/
├── dest/
│   └── css/
│       └── common.css
├── gulpfile.js
├── package.json
└── src/
    └── css/
        ├── _module.css
        └── common.css
まずデスクトップに移動して、
cd ~/Desktop
gulp-postcss-testというフォルダを作ります。
mkdir gulp-postcss-test
作ったフォルダに移動します。
cd gulp-postcss-test
package.jsonを作成します。
npm init -y
npmで以下のパッケージをインストールします。cssnextにインストールされているPostCSSのプラグインはnode_modules/postcss-cssnext/lib/features.jsで確認することができます。
gulp-cssnextは非推奨(DEPRECATED)となっているようです。gulp-cssnextを使った記事もあるので気をつけて下さい。
npm install --save-dev gulp gulp-postcss postcss-cssnext
gulpfile.jsを作成します。
touch gulpfile.js
gulpfile.jsに以下のようにGulpのタスクを記述します。
var gulp = require('gulp');
var postcss = require('gulp-postcss');
var cssnext = require('postcss-cssnext');
var AUTOPREFIXER_BROWSERS = [
  'last 2 version',
  'ie >= 9',
  'iOS >= 7',
  'Android >= 4.2'
];
var src = {
  'css': ['src/css/**/*.css', '!src/css/**/_*.css'],
  'cssWatch': 'src/css/**/*.css'
};
var dest = {
  'css': './dest/css/'
};
gulp.task('css', function() {
  var plugins = [
    cssnext({
      browsers: AUTOPREFIXER_BROWSERS
    })
  ];
  return gulp.src(src.css)
  .pipe(postcss(plugins))
  .pipe(gulp.dest(dest.css));
});
gulp.task('watch', ['css'], function() {
  gulp.watch(src.cssWatch, ['css']);
});
src/cssというディレクトリを作成します。
mkdir -p src/css
作成したディレクトリ内にcommon.cssというCSSファイルを作成します。
touch src/css/common.css
common.cssに以下のように記述します。.fooでCustom Propertiesを、.barでCustom Selectorsを、.bazでCustom Media Queriesと@apply Ruleを使っています。
:root {
  --max-width: 960px;
  --color: #000;
}
@custom-selector :--onEvent :hover, :active, :focus;
@custom-media --md-up screen and (min-width: 768px);
:root {
  --clearfix: {
    &::after {
      content: "";
      display: block;
      clear: both;
    }
  }
}
html {
  font-size: 14px;
}
.foo {
  max-width: var(--max-width);
  color: var(--color);
  font-size: 1rem;
}
.bar:--onEvent {
  text-decoration: none;
}
@media (--md-up) {
  .baz {
    @apply --clearfix;
  }
}
gulp cssかgulp watchタスクを実行してdest/css/に以下のようにCSSファイルが生成されていたら成功です。
html {
  font-size: 14px;
}
.foo {
  max-width: 960px;
  color: #000;
  font-size: 1rem;
}
.bar:hover,
.bar:active,
.bar:focus {
  text-decoration: none;
}
@media screen and (min-width: 768px) {
  .baz {
  }
  .baz::after {
    content: "";
    display: block;
    clear: both;
  }
}
gulpfile.jsのAutoprefixerのブラウザ指定を以下のようにIE8以上の対応に変更してみます。
var AUTOPREFIXER_BROWSERS = [
  'last 2 version',
  'ie >= 8', // 9から8に変更
  'iOS >= 7',
  'Android >= 4.2'
];
この状態でgulp cssタスクを実行すると以下のように2箇所の出力されるコードが変わります。つまりcssnext側でIE8に対応するためにコードを変換してくれます。
html {
  font-size: 14px;
}
.foo {
  max-width: 960px;
  color: #000;
  font-size: 14px; /* pxのフォールバックを追加 */
  font-size: 1rem;
}
.bar:hover,
.bar:active,
.bar:focus {
  text-decoration: none;
}
@media screen and (min-width: 768px) {
  .baz {
  }
  .baz:after { /* `::after`から`:after`に変換 */
    content: "";
    display: block;
    clear: both;
  }
}
PostCSSプラグインを追加
cssnextは導入できましたが、Sassの@importも入れておきたいのでpostcss-importというPostCSSのプラグインを導入します。CSSのインデントなどを整えるためにstylefmtも入れておきます。
npmでパッケージをインストールします。
npm install --save-dev postcss-import stylefmt
gulpfile.jsに以下のように追記します。
var atImport = require('postcss-import'); // 追加
var stylefmt = require('stylefmt'); // 追加
  var plugins = [
    atImport, // 追加
    cssnext({
      browsers: AUTOPREFIXER_BROWSERS
    }),
    stylefmt // 追加
  ];
全体像は以下のようになります。
var gulp = require('gulp');
var postcss = require('gulp-postcss');
var cssnext = require('postcss-cssnext');
var atImport = require('postcss-import');
var stylefmt = require('stylefmt');
var AUTOPREFIXER_BROWSERS = [
  'last 2 version',
  'ie >= 9',
  'iOS >= 7',
  'Android >= 4.2'
];
var src = {
  'css': ['src/css/**/*.css', '!src/css/**/_*.css'],
  'cssWatch': 'src/css/**/*.css'
};
var dest = {
  'css': './dest/css/'
};
gulp.task('css', function() {
  var plugins = [
    atImport,
    cssnext({
      browsers: AUTOPREFIXER_BROWSERS
    }),
    stylefmt
  ];
  return gulp.src(src.css)
  .pipe(postcss(plugins))
  .pipe(gulp.dest(dest.css));
});
gulp.task('watch', ['css'], function() {
  gulp.watch(src.cssWatch, ['css']);
});
common.cssをコピーして_module.cssという名前にリネームします。
cp src/css/common.css src/css/_module.css
stylefmtの動作を試すために_module.cssのインデントをわざと崩しておきます。
  html {font-size: 14px;}
.foo {
    max-width: var(--max-width);
  color:var(--color);
 font-size: 1rem;
}
.bar:--onEvent {
text-decoration: none;
}
@media (--md-up) {
      .baz {
@apply --clearfix;
  }
}
common.cssの内容をすべて削除して、以下のように_module.cssをインポートします。
@import "_module.css";
rm -r destコマンドでdestディレクトリを削除してからgulp cssタスクを実行して、以下のように整形された状態で出力されていたら成功です。
html {
  font-size: 14px;
}
.foo {
  max-width: 960px;
  color: #000;
  font-size: 1rem;
}
.bar:hover,
.bar:active,
.bar:focus {
  text-decoration: none;
}
@media screen and (min-width: 768px) {
  .baz {
  }
  .baz::after {
    content: "";
    display: block;
    clear: both;
  }
}
まとめ
基本的な機能はcssnextだけでも問題ないのかなと感じました。
cssnextの機能はSassの機能を比べてみるとやや足りないところもあるので、必要に応じてPostCSSのプラグインを導入してもいいのかなと思います。今回の場合だとAutoprefixerとpostcss-importは標準仕様外になります。
cssnextの最大のメリットは(仕様が変更される可能性はあるものの)標準仕様のシンタックスであること。Autoprefixerの対応ブラウザの指定に応じて出力する内容を変えてくれる保守性の高さや、コンパイル速度もLibSass(gulp-sass)より4倍近く速いといったパフォーマンス面のメリットもあります。
また、BootstrapはBootstrap5でPostCSSに移行する予定で、Zurb Foundationもcssnextの導入を検討していたりと、SassからPostCSSにという流れは確実にあります。
Sassでカスタマイズをするのが前提のCSSフレームワークなどが使えなくなるデメリットはあるので、すぐに移行する必要はないかもしれませんが、試してみたり動向をみておく必要はありそうです。  
Oh, btw—Bootstrap 4 will be in SCSS. And if you care, v5 will likely be in PostCSS because holy crap that sounds cool.
— Mark Otto (@mdo) 2015年4月23日


