PostCSS: будущее после Sass и Less

Андрей Ситник, Злые марсиане

PostCSS Будущее после Sass и Less

logo.pngАндрей Ситник, Злые марсиане

План

  1. Зачем обрабатывать CSS
  2. Чем плохи препроцессоры
  3. Что такое постпроцессоры
  4. Как их использовать?
  5. Как их использовать на полную мощь?

Часть 1 Проблема

covers/problem.jpg

Части веб‑проекта

CSS
72 КБ
JS
25 КБ
Бэкенд
43 КБ
Источник: проекты Злых марсиан

Хороший код DRY

format_links(announce);
format_links(text);

Хороший код DSL

count.should.eql(1);

Хороший код Метапрограммирование

User = modelByTable('users');
User.findByLogin('ai');

CSS

.logo {
    -webkit-transition: border 200ms;
    transition: border 200ms;
    border: 2px solid #ffe644
}
.image {
    border: 2px solid #ffe644;
    background: data(data:…);
    width: 200px;
    height: 180px
}
.body::after {
    content: " ";
    visibility: hidden;
    display: block;
    height: 0;
    clear: both
}

Стандарты развиваются медленно

1. Скорость разработки и рендера

Быстро разрабатывать

Быстро рендерить

:root {
    --main: #c00
}
a {
    color: var(--main)
}
a {
    color: #c00
}

2. Всем нужно договориться

chrome.pngie.pngfirefox.pngsafari.pngopera.png

3. Обратная совместимость

Нестандартный <blink> поддерживали 19 лет

Эволюция

  1. Случайные идеи
  2. Сравнение в реальном мире
  3. Отбор

Машины должны страдать

Часть 2 Препроцессоры

covers/sass.jpg

Шаблонизаторы CSS

a {
    <%= include clickable %>
    color: <%= $link-color %>;
}

Проблема 1 Медленно

Compass на стилях ГитХаба — 5,5 секунды*

Синтаксические возможности

Проблема 2 Ограниченность

a {
    width: 20rem
}

Проблема 3 Даже JS лучше Sass

@import "compass/support";

// The the user threshold for transition support. Defaults to `$graceful-usage-threshold`
$transition-support-threshold: $graceful-usage-threshold !default;


// CSS Transitions
// Currently only works in Webkit.
//
// * expected in CSS3, FireFox 3.6/7 and Opera Presto 2.3
// * We'll be prepared.
//
// Including this submodule sets following defaults for the mixins:
//
//     $default-transition-property : all
//     $default-transition-duration : 1s
//     $default-transition-function : false
//     $default-transition-delay    : false
//
// Override them if you like. Timing-function and delay are set to false for browser defaults (ease, 0s).

$default-transition-property: all !default;

$default-transition-duration: 1s !default;

$default-transition-function: null !default;

$default-transition-delay: null !default;

$transitionable-prefixed-values: transform, transform-origin !default;



// Checks if the value given is a unit of time.
@function is-time($value) {
  @return if(type-of($value) == number, not not index(s ms, unit($value)), false);
}

// Returns `$property` with the given prefix if it is found in `$transitionable-prefixed-values`.
@function prefixed-for-transition($prefix, $property) {
  @if not $prefix {
    @return $property;
  }
  @if type-of($property) == list or type-of($property) == arglist {
    $new-list: comma-list();
    @each $v in $property {
      $new-list: append($new-list, prefixed-for-transition($prefix, $v));
    }
    @return $new-list;
  } @else {
    @if index($transitionable-prefixed-values, $property) {
      @return #{$prefix}-#{$property};
    } @else {
      @return $property;
    }
  }
}

// Returns $transition-map which includes key and values that map to a transition declaration
@function transition-map($transition) {
  $transition-map: ();

  @each $item in $transition {
    @if is-time($item) {
      @if map-has-key($transition-map, duration) {
        $transition-map: map-merge($transition-map, (delay: $item));
      } @else {
        $transition-map: map-merge($transition-map, (duration: $item));
      }
    } @else if map-has-key($transition-map, property) {
      $transition-map: map-merge($transition-map, (timing-function: $item));
    } @else {
      $transition-map: map-merge($transition-map, (property: $item));
    }
  }

  @return $transition-map;
}

// One or more properties to transition
//
// * for multiple, use a comma-delimited list
// * also accepts "all" or "none"

@mixin transition-property($properties...) {
  $properties: set-arglist-default($properties, $default-transition-property);
  @include with-each-prefix(css-transitions, $transition-support-threshold) {
    $props: if($current-prefix, prefixed-for-transition($current-prefix, $properties), $properties);
    @include prefix-prop(transition-property, $props);
  }
}

// One or more durations in seconds
//
// * for multiple, use a comma-delimited list
// * these durations will affect the properties in the same list position

@mixin transition-duration($durations...) {
  $durations: set-arglist-default($durations, $default-transition-duration);
  @include prefixed-properties(css-transitions, $transition-support-threshold, (
    transition-duration: $durations
  ));
}

// One or more timing functions
//
// * [ ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier(x1, y1, x2, y2)]
// * For multiple, use a comma-delimited list
// * These functions will effect the properties in the same list position

@mixin transition-timing-function($functions...) {
  $functions: set-arglist-default($functions, $default-transition-function);
  @include prefixed-properties(css-transitions, $transition-support-threshold, (
    transition-timing-function: $functions
  ));
}

// One or more transition-delays in seconds
//
// * for multiple, use a comma-delimited list
// * these delays will effect the properties in the same list position

@mixin transition-delay($delays...) {
  $delays: set-arglist-default($delays, $default-transition-delay);
  @include prefixed-properties(css-transitions, $transition-support-threshold, (
    transition-delay: $delays
  ));
}

// Transition all-in-one shorthand

@mixin single-transition(
  $property: $default-transition-property,
  $duration: $default-transition-duration,
  $function: $default-transition-function,
  $delay: $default-transition-delay
) {
  @include transition(compact($property $duration $function $delay));
}

@mixin transition($transitions...) {
  $default: (compact($default-transition-property $default-transition-duration $default-transition-function $default-transition-delay),);
  $transitions: if(length($transitions) == 1 and type-of(nth($transitions, 1)) == list and list-separator(nth($transitions, 1)) == comma, nth($transitions, 1), $transitions);
  $transitions: set-arglist-default($transitions, $default);


  @include with-each-prefix(css-transitions, $transition-support-threshold) {
    $delays: comma-list();
    $transitions-without-delays: comma-list();
    $transitions-with-delays: comma-list();
    $has-delays: false;


    // This block can be made considerably simpler at the point in time that
    // we no longer need to deal with the differences in how delays are treated.
    @each $transition in $transitions {
      // Declare initial values for transition
      $transition: transition-map($transition);

      $property: map-get($transition, property);
      $duration: map-get($transition, duration);
      $timing-function: map-get($transition, timing-function);
      $delay: map-get($transition, delay);

      // Parse transition string to assign values into correct variables
      $has-delays: $has-delays or $delay;

      @if $current-prefix == -webkit {
        // Keep a list of delays in case one is specified
        $delays: append($delays, if($delay, $delay, 0s));
        $transitions-without-delays: append($transitions-without-delays,
          prefixed-for-transition($current-prefix, $property) $duration $timing-function);
      } @else {
        $transitions-with-delays: append($transitions-with-delays,
          prefixed-for-transition($current-prefix, $property) $duration $timing-function $delay);
      }
    }

    @if $current-prefix == -webkit {
      @include prefix-prop(transition, $transitions-without-delays);
      @if $has-delays {
        @include prefix-prop(transition-delay, $delays);
      }
    } @else if $current-prefix {
      @include prefix-prop(transition, $transitions-with-delays);
    } @else {
      transition: $transitions-with-delays;
    }
  }
}

Часть 3 Постпроцессоры

covers/postcss.jpg

Постпроцессоры

  1. Rework:
    • первый
    • проще, меньше
  2. PostCSS:
    • более умный парсер
    • лучше поддержка карт кода
    • сохраняет форматирование
    • удобнее API

Постпроцессор

CSS
карта кода
Парсер
Плагин
Плагин
Сохранение
Новый CSS
новая карта

Использование

var postcss = require('postcss');

css = postcss()
        .use(plugin1)
        .use(plugin2)
        .process(css).css;

Плагин

var pixrem = function (css) {
    css.eachDecl(function (decl) {
        decl.value = decl.value
            .replace(/\d+rem/, function (rem) {
                return 16 * parseFloat(rem);
            });
    })
}

Разница

Препроцессор

Постпроцессор

Плагины autoprefixer

:fullscreen a {
    transition: transform 1s
}
:-webkit-full-screen a {
    -webkit-transition: -webkit-transform 1s;
            transition: transform 1s
}
:-moz-full-screen a {
    transition: transform 1s
}
:-ms-fullscreen a {
    transition: transform 1s
}
:fullscreen a {
    -webkit-transition: -webkit-transform 1s;
            transition: transform 1s
}

Плагины pleeease-filters

.blur {
    filter: blur(4px)
}
.blur {
    filter: url(data:image/svg+xml;…);
    filter: blur(4px)
}

Плагины webpcss

.icon {
    background: url(a.png)
}
.icon {
    background: url(a.png)
}
.webp .icon {
    background: url(a.webp)
}

Плагины grunt-data-separator

/* style.css */
.icon {
    width: 100px;
    background: url(data:…)
}
/* style.css */
.icon {
    width: 100px
}
/* style.icons.css */
.icon {
    background: url(data:…)
}

Плагины csswring

Минифицирует CSS и обновит предыдущие карты кода (например, после склеивания)

Плагины postcss-bem-linter

Проверка БЭМ‑стиля для Твиттера (методология SUIT CSS)

Плагины doiuse

Проверяет поддержку свойств в нужных браузерах по Can I Use

main.css: line 15, col 3 -
  CSS user-select: none not supported by: IE (8,9)
main.css: line 32, col 3 -
  CSS3 Transforms not supported by: IE (8)

Плагины cssnext

Набор полифилов для спецификаций CSS 4:

Плагины rtlcss

Изменяет дизайн для арабского и иврита

a {
    left: 10px;
    text-align: left
}
a {
    right: 10px;
    text-align: right
}

Скорость

PostCSS
36 мс
libsass
109 мс
Less
150 мс
Stylus
283 мс
Sass
1153 мс

Преимущества

  1. Скорость
  2. Плагины пишутся на JS
  3. Можно сделать гораздо больше

Часть 4 Используем

covers/usage.jpg

Интеграция

gulp-postcss

gulp.task('css', function () {
    var processors = [/* плагины */];
    return gulp.src('./src/style.css')
        .pipe( postcss(processors) )
        .pipe( gulp.dest('./dest') );
});

Выбираем плагины

var processors = [
    require('postcss-custom-properties'),
    require('pleeease-filters'),
    require('postcss-import'),
    require('autoprefixer'),
    require('postcss-calc'),
    require('postcss-url')
    require('csswring')
];

Часть 5 Создаём

covers/create.jpg

Задача

Какой символ нужно использовать для иконки из своего иконочного шрифта

.icon::before {
    content: "?"
}

Шаг 1 gulp-iconfont

gulp.task('iconfont', function() {
    gulp.src(['icons/*.svg'])
        .pipe(iconfont({ fontName: 'Icons' })
        .on('codepoints', function(data) {
            icons = data;
        })
        .pipe(gulp.dest('fonts/'));
});

Шаг 2 Плагин для PostCSS

var iconer = function (css) {
    css.eachDecl(function (decl) {
        decl.value = decl.value
            .replace(/icon-\w/, function (str) {
                var name = str.replace(/^icon-/, '');
                return '"' + icons[name].codepoint + '"';
            });
    });
};

Результат

.icon::before {
    content: icon-up
}
.icon::before {
    content: "A"
}

Вопросы

covers/ask.jpg

github.com/postcss/postcss

evilmartians.png