PostCSSで作ろうマイCSSビルドツール

どうも、初めて書きますnju33(じゅん)という者です。 よろしくお願いします。
書く順番が回ってきて何を書こうか迷ったのですが、今回はPostCSSについて書こうと思います。

PostCSSとは?

Autoprefixerで有名なAndrey SitnikさんらがNodeJSで開発している、 CSSパーサとそれを取り扱うのに便利なAPIを提供しているツールです。 そのAPIを使って、変換する為のプラグインが沢山が各々によってたくさん作られています。
プラグインは1つで1つの事を行うことをポリシーに作られているので、それらを組み合わせることで、自分だけのCSSビルドツールを作ることができます。

最近では「その場で実行」系で人気サービス、 Codepen(*)や jsFiddle(*) なども続々と対応してきていたり、 「Twitter Bootstarp5にはPostCSSが使われるかも」と開発者の1人 Mark Otto(@mdo) さんが言っていたりと、今後に注目したいツールです。

PostCSSの使い方

まずは実際に変換してみます。
とりあえず、下の2つのプラグインを選びました。

プラグイン 説明
autoprefixer ベンダープレフィックスを付ける
postcss-namespace セレクターにプレフィックスを付ける

ではまずは、プラグインとPostCSSをインストールします。

# 作業ディレクトリとして postcss/ を作って移動
mkdir postcss && cd $_
npm install postcss autoprefixer postcss-namespace

次に、変換用のcssファイルを作ります。

@prefix block;

.box {
  display: flex;  
}

最後に、PostCSSを使って変換するjsファイルを作ります。
postcssの第一引数に、インストールしたプラグインを適応順に、配列で記述します。

// 必要なモジュール読み込み
const fs = require('fs'),
      postcss = require('postcss'),
      autoprefixer = require('autoprefixer'),
      namespace = require('postcss-namespace');

// autoprefixerで変換されたCSSを、
// 次はpostcss-namespaceで変換
const processor = postcss([
  autoprefixer({browsers: ['last 2 versions']}),
  namespace({token: '__'})
]);

// 変換するファイル
const css = fs.readFileSync('style.css', 'utf-8');

processor.process(css)
  .then((result) => {
    console.log(result.css);
  });

ちなみに、ディレクトリ構造はこうなっています。

.
├── node_modules  // 中身は略
├── script.js
└── style.css

あとは、変換するだけです。

node script.js

こんな感じに変換されました!

.block__box {
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
}

プラグインの中には、いくつかのプラグインを集めて1つのプラグインとして提供している、 パックプラグインというものもあります。
例えば下の2つはそれです。

プラグイン 説明
cssnext CSS4シンタックスに近い方法で書けるようにする
precss Sassのような感じで書けるようにする

詳しくはそれぞれのリンク先を読んでみてください。 最初はこれらから使っていき、慣れてきたら1つ1つプラグインを選んでいくなどすると良いと思います。

僕のCSSビルドツール

最後に自分の環境を晒して終わりたいと思います。

スクランナー

流石に変更の度にコマンドを実行するのは面倒くさいのでgulpを使って、 cssファイルが変更する度にビルドされるようにします。

以下のプラグインを使います。詳細は各プラグインのリンク先で。

プラグイン 説明
stylelint cssの構文チェック
postcss-reporter stylelintログを見やすくして表示
postcss-cson-cssvars CSS内で使える変数をCSONファイルで設定
postcss-color-function カラー調整関数を追加
postcss-assets より良い画像アセット読み込み関数を追加
postcss-property-lookup 同じルール内のプロパティ値に、@xxxのような形で利用できる
postcss-animation animate.cssで定義されてる名前をanimationに指定するだけで使える
postcss-easings transition-timing-functioneasings.netにあるものを追加
lost グリッドシステムなプロパティを追加
postcss-selector-not CSS4シンタックス:notが使える
postcss-preref 前回のセレクタ&で使える
postcss-namespace セレクターにプレフィックスを付ける
doiuse caniuseを元に、サポート対象ブラウザが対応していないプロパティを使っていると警告を出してくれる
autoprefixer ベンダープレフィックスを自動で付ける
css-mqpacker @mediaの最適化
cssnano cssの最適化
const fs = require('fs'),
      gulp = require('gulp'),
      watch = require('gulp-watch'),
      sourcemaps = require('gulp-sourcemaps'),
      postcss = require('gulp-postcss');

const processors = [
  require('stylelint'),
  require('postcss-reporter')({clearMessages: true}),
  require('postcss-cson-cssvars'),
  require('postcss-color-function'),
  require('postcss-assets'),
  require('postcss-property-lookup'),
  require('postcss-animation'),
  require('postcss-easings'),
  require('lost'),
  require('postcss-selector-not'),
  require('postcss-preref'),
  require('postcss-namespace')({token: '__'}),
  require('doiuse')({browsers: ['last 2 versions']}),
  require('autoprefixer')({browser: ['last 2 versions']}),
  require('css-mqpacker'),
  // require('cssnano') 見づらくなるので今回はコメントアウト
]

gulp.task('postcss', () => {
  gulp.src('./style.css')
    .pipe(watch('./style.css'))
    .pipe(sourcemaps.init())
    .pipe(postcss(processors))
    .pipe(sourcemaps.write('.'))
  .pipe(gulp.dest('dist/'));
});

たくさん読み込んでいますが、順番はかなり重要です。
順番によって後のプラグインがうまく動かなくなったりするので、 よくREADMEを読んで頭の中で処理を想像した時におかしくならないように気をつけます。

インプット

これらを使う前提でCSSを書いてみます。

@charset "utf-8";

.u-bounce {
  animation: bounce;
}

@prefix block;

.box {
  color: color($color.font l(+10));
  border: 1px solid @color;
}

@media screen and (min-width: $size.ipadpro) {
  .box {
    lost-center: $width;
  }
}

.image {
  width: width(500x403.jpg);
  height: height(500x403.jpg);
}

.card {
  lost-column: 1/2 no-flex;
}

@media screen and (min-width: $size.ipadpro) {
  .card {
    lost-column: 1/4 no-flex;
  }
}

.input:not(.not1, .not2) {
  transition: .3s easeInOutBack;
  border: 1px solid $color.quiet;
}

&:hover {
  border: 1px solid $color.accent;
}

こんな感じになりました。後はビルドするだけです。

アウトプット

@charset "utf-8";

.u-bounce {
  -webkit-animation: bounce;
          animation: bounce;
}

@-webkit-keyframes bounce{

  from, 20%, 53%, 80%, to{
    -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
            animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
    -webkit-transform: translate3d(0,0,0);
            transform: translate3d(0,0,0);
  }

  40%, 43%{
    -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
            animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
    -webkit-transform: translate3d(0, -30px, 0);
            transform: translate3d(0, -30px, 0);
  }

  70%{
    -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
            animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
    -webkit-transform: translate3d(0, -15px, 0);
            transform: translate3d(0, -15px, 0);
  }

  90%{
    -webkit-transform: translate3d(0,-4px,0);
            transform: translate3d(0,-4px,0);
  }
}

@keyframes bounce{

  from, 20%, 53%, 80%, to{
    -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
            animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
    -webkit-transform: translate3d(0,0,0);
            transform: translate3d(0,0,0);
  }

  40%, 43%{
    -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
            animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
    -webkit-transform: translate3d(0, -30px, 0);
            transform: translate3d(0, -30px, 0);
  }

  70%{
    -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
            animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
    -webkit-transform: translate3d(0, -15px, 0);
            transform: translate3d(0, -15px, 0);
  }

  90%{
    -webkit-transform: translate3d(0,-4px,0);
            transform: translate3d(0,-4px,0);
  }
}

.block__box {
  color: rgb(92, 92, 92);
  border: 1px solid white;
  color: white;
}

.block__image {
  width: 500px;
  height: 403px;
}

.block__card {
  width: calc(99.99% * 1/2 - (30px - 30px * 1/2));
}

.block__card:nth-child(n){
  float: left;
  margin-right: 30px;
  clear: none;
}

.block__card:last-child{
  margin-right: 0;
}

.block__card:nth-child(2n){
  margin-right: 0;
}

.card:nth-child(2n + 1){
  clear: left;
}

.block__input:not(.not1):not(.not2) {
  -webkit-transition: .3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
  transition: .3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
  border: 1px solid #b8b8b8;
}

.block__input:not(.not1):not(.not2):hover {
  border: 1px solid #EB7A77;
}

@media screen and (min-width: 1026px){

  .block__box{
    *zoom: 1;
    max-width: 960px;
    margin-left: auto;
    margin-right: auto;
  }

  .block__box:before{
    content: '';
    display: table;
  }

  .block__box:after{
    content: '';
    display: table;
    clear: both;
  }

  .block__card{
    width: calc(99.99% * 1/4 - (30px - 30px * 1/4));
  }

  .block__card:nth-child(n){
    float: left;
    margin-right: 30px;
    clear: none;
  }

  .block__card:last-child{
    margin-right: 0;
  }

  .block__card:nth-child(4n){
    margin-right: 0;
  }

  .card:nth-child(4n + 1){
    clear: left;
  }
}

/*# sourceMappingURL=style.css.map */

少ない記述量で、こんな感じに変換されました!

プラグイン一覧は最低でも週に1〜2個ずつ増えていっています。お気に入りのプラグインを探してみてください。
ちなみに、 postcss-namespacepostcss-prerefpostcss-cson-cssvarsは僕が自作してみたプラグインです。

思っていたほどプラグイン作成は難しいものではないので、 自分が欲しいプラグインが一覧に無ければ、似たプラグインで我慢せず自分で作ってしまうのも1つの手だと思います!


(p.s.)
この環境はpostcss-environment-nju33に置いておきました。