Организация вёрстки

Олег Мохов

Как мы верстаем?

Типичная вёрстка

    templates/
    └── index.html
    └── index.css

Хороший CSS


a {
    color: red;
}

Типичный CSS


html,
body
{
    margin: 0;
    padding: 0;
    width: 100%;
    font-family: Arial, sans-serif;
}

header
{
    position: relative;
    border: 3px solid #000;
    width: 952px;
    height: 551px;
    margin: 18px auto 0;
}

/* далее ещё 400 строчек кода */

CSS по полочкам

/* сброс умолчаний */
/* общие стили */
/* шапка */
/* основная часть */
/* футер */
/* страница печати */
/* мобильная версия */
/* какие-то правки */
/* новая страница */
/* ещё какие-то правки */
/* стили новой шапки */

Большие проекты

Проблемы

  • При внесении правок результат не всегда предсказуем
  • Блоки зависят от окружения
  • DOM-lookup'ы
  • Смешение назначений

Большие проекты

  • Длительная поддержка кода
  • Много кода
  • Время на разработку ограничено
  • Большие команды
  • Единый стиль
  • Код должен быть качественным

Независимые блоки

  • Верстаем не макетами, а блоками
  • Блоки бывают атомарные или составные
  • HTML, CSS, JavaScript блока не зависит от других блоков

Достижение независимости

  • Модульность
  • Реиспользование
  • Общая предметная область
  • Разделение ответственности

Отказываемся от id

  • #id и .class идентичны по скорости наложнения на DOM-дерево (защита от «дурака»)
  • При этом работая с #id нельзя исключать, что когда-то элемент станет не уникальным
  • Нельзя полностью отказаться от использования id'шников, т.к они нужны для форм или якорей

Решение Яндекса: БЭМ

Блок

  • это кирпичик проекта
  • может быть простым или составным
  • логически и функционально независим
  • блок инкапсулирует в себе поведение, шаблоны и стили, а также другие технологии реализации
  • повторно реиспользуем

Вложенность

Свободное перемещение

Элемент

  • это часть блока, отвечающая за отдельную функцию
  • может находиться только в составе блока и не имеет смысла в отрыве от него

Модификатор

Модификатор

  • это свойство блока или элемента, которое меняет его внешний вид или поведение
  • имеет имя и значение
  • одновременно может использоваться несколько разных модификаторов

Витрина bem-components

Именование

Именование блоков

  • уникальное название, идентифицирующее блок
  • пробелы заменяются на дефисы
  • возможно использовать префиксы
  • это класс HTML-элемента

Именование элементов

  • уникальное название, идентифицирующее элемент внутри блока
  • название строится комбинированием имени блока и элемента (например блок__элемент)
  • это класс HTML-элемента

Именование модификаторов

  • уникальная пара ключ-значение, идентифицирующая определенное свойство и состояние блока/элемента
  • название строится добавлением к имени блока или элемент символа _ и названия (например, блок_модификатор или элемент_модификатор_значение)
  • это дополнительный класс HTML-элемента


     
    

Вёрстка на файловой системе

  • все сущности кладутся в отдельные директории
  • при использовании препроцессоров или постпроцессоров возможно ограничиваться только блоками
  • каждая технология в отдельный файл
    button/
    └── button.html
    └── button.css
    └── button.ie.css
    └── button.js
    └── _hovered/
        └── button_hovered.css
        └── button_hovered.png
    └── __icon/
        └── button__icon.css
        └── _color/
            └── button__icon_color_blue.css
            └── button__icon_color_red.css

Уровни переопределения

    common/
    └── header/
        └── header.css
        └── header.js
    desktop/
    └── header/
        └── header.css
    touch/
    └── header/
        └── header.css
        └── header.js

@import (common/header/header.css);
@import (desktop/header/header.css);
header.desktop.css


@import (common/header/header.css);
@import (touch/header/header.css);
header.touch.css

enb-make

https://github.com/enb-make/enb


({
    block: 'page'
})
index.bemjson.js


({
    mustDeps: [
        { block: 'header' }
    ],
    shouldDeps: [
        { block: 'button' },
        { block: 'icon', mods: ['warning', 'error'] }
    ]
})
page.deps.js

Шаблонизация


res.render('template', data);

Шаблонизация

Данные

HTML

Двухуровневая шаблонизация

Данные

БЭМ-дерево

HTML

BEMJSON


{
    block: 'page'
}



{
    block: 'page',
    mods: { type: 'main' }
}



{
    block: 'page',
    mods: { type: 'main' },
    content: {
        block: 'header'
    }
}


Императивное и декларативное программирование

Императивные языки программирования

«языки программирования, в которых описывается процесс вычисления в виде инструкций, изменяющих состояние программы (состояние памяти, состояние переменных...)»

Декларативные языки программирования

«языки высокого уровня, в которых не задается пошаговый алгоритм решения задачи ("как" решить задачу), а описывается, "что" требуется получить в качестве результата»

BH

https://github.com/bem/bh


bh.match('page', (ctx) => {
    ctx.tag('main');
});

bh.match('page', (ctx) => {
    ctx.content({
        elem: 'content',
        content: ctx.content()
    });
});

bh.match('page', (ctx) => {
    ctx.content([
        { elem: 'header' },
        ctx.content(),
        { elem: 'footer' }
    ]);
});


bh.match('page__footer', (ctx) => {
    ctx.content('THIS IS FOOOOTER!');
});

Уровни переопределения


bh.match('page', (ctx) => {
    ctx.content('CONTENT');
});
bh.match('page', (ctx) => {
    ctx.tag('div');
});

Инкапсуляция разметки

i-bem.js

1. Манипулируем не DOM, а BEM-объектами

2. Вместо событий реагируем на изменения модификаторов

3. Блок манипулирует только собой

4. Для связывания двух блоков используется не хождение вверх по DOM, а блоки-обёртки или каналы (глобальные события)

Ещё решения

  • OOCSS
  • SMACSS
  • Atomic CSS
  • MCSS
  • AMCSS
  • FUN

Способы организации CSS-кода

Как не писать CSS?

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

Код

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

CSS


.menu {
    display: inline-block;
}

.menu__item {
    background: #d0881d;
}

.menu__item:hover {
    background: #ebb96f;
}

.menu__arrow {
    background: #d0881d;
}


            

$bg_color = #d0881d;

.menu {
    display: inline-block;
}

menu__item {
    background: $bg_color;
}

.menu__item:hover {
    background: lighten($bg_color, 40%);
}

.menu__arrow {
    background: $bg_color;
}
            

.menu {
    display: inline-block;
}

.menu__item {
    background: #d0881d;
}

.menu__item:hover {
    background: #ebb96f;
}

.menu__arrow {
    background: #d0881d;
}


            


$bg_color = #d0881d;

.menu {
    display: inline-block;
}

menu__item {
    background: $bg_color;
}

.menu__item:hover {
    background: lighten($bg_color, 40%);
}

.menu__arrow {
    background: $bg_color;
}
 
            

.menu {
    display: inline-block;
}

.menu__item {
    background: #d0881d;
}

.menu__item:hover {
    background: #ebb96f;
}

.menu__arrow {
    background: #d0881d;
}
            


$bg_color = #d0881d;

.menu {
    display: inline-block;

    &__item {
        background: $bg_color;

        &:hover {
            background: lighten($bg_color, 40%);
        }
    }

    &__arrow {
        background: $bg_color;
    }
}
                
            

.menu {
    display: inline-block;
}

.menu__item {
    background: #d0881d;
}

.menu__item:hover {
    background: #ebb96f;
}

.menu__arrow {
    background: #d0881d;
}
            


$bg_color = #d0881d;

.menu {
    display: inline-block;

    &__item {
        background: $bg_color;

        &:hover {
            background: lighten($bg_color, 40%);
        }
    }

    &__arrow {
        background: $bg_color;
    }
}
                
            

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

Less

Sass

Stylus

Установка


npm install stylus
        

Компиляция


stylus index.styl --out ./css/index.css
        

stylus --watch index.styl
        

Переменные


$font = 14px Helvetica, sans-serif;
box_width = 30%;
box_height = 300px;

.block {
    font: $font;
    width: box_width;
    height: box_height;
}
            

.block {
    font: 14px Helvetica, sans-serif;
    width: 30%;
    height: 300px;
}




            

Операторы


$box_width = 300px;
$box_height = $box_width * 2;

.box {
    width: $box_width;
    height: $box_height;
}
            

.box {
    width: 300px;
    height: 600px;
}



            

Вложенность


.header {
    .title {
        font-size: 20px;
    }

    .link {
        color: green;
        text-decoration: none;

        &:hover {
            text-decoration: underline;
        }
    }
}
            

.header .title {
    font-size: 20px;
}

.header .link {
    color: #008000;
    text-decoration: none;
}

.header .link:hover {
    text-decoration: underline;
}


            

Вложенность


.header {
    &__title {
        font-size: 20px;
    }

    &__link {
        color: green;
        text-decoration: none;

        &:hover {
            text-decoration: underline;
        }
    }
}
            

.header__title {
    font-size: 20px;
}

.header__link {
    color: #008000;
    text-decoration: none;
}

.header__link:hover {
    text-decoration: underline;
}


            

Массивы и циклы


$col_list = 1 2 3 4;
            for $col in $col_list {
    td:nth-child({$col}) {
        width: 10% * $col;
    }
}
            for $col in (1..4) {...}
                            

td:nth-child(1) {
    width: 10%;
}

td:nth-child(2) {
    width: 20%;
}

td:nth-child(3) {
    width: 30%;
}

td:nth-child(4) {
    width: 40%;
}
            

Hashes


$cats = {
    cat_1: './images/cat1.jpg',
    cat_2: './images/cat2.jpg'
}
            
$cats.cat_1 = './images/cat1_new.jpg';
$cats['cat_3'] = './images/cat3.jpg';
            
for $name, $bg_img in $cats {
    #img-{$name} {
        background: url($bg_img);
    }
}
            

#img-cat1 {
    background: url("./images/cat1_new.jpg");
}

#img-cat2 {
    background: url("./images/cat2.jpg");
}

#img-cat3 {
    background: url("./images/cat3.jpg");
}




            

Условные операторы


$theme = 'day';

.sky {
    if $theme == 'day' {
        background: blue;
        background-image: url(sun.png);
    } else {
        background: black;
        background-image: url(stars.png);
    }
}
            

.sky {
    background: #00f;
    background-image: url(sun.png);
}
            

import


@import 'theme'
        

// theme_day.styl

$bg_color = blue;
$bg_img = sun.png;
            

// main.styl

@import 'theme_day'

.sky {
    background: $bg_color;
    background-image: url($bg_img);
}
            

/* main.css */

.sky {
    background: #00f;
    background-image: url(sun.png);
}
            

Миксины


set_bg_color($theme) {
    if $theme == 'day' {
        background: blue;
        background-image: url(sun.png);
    } else {
        background: black;
        background-image: url(stars.png);
    }
}

.sky {
    set_bg_color('night');
}
            

.sky {
    background: #000;
    background-image: url(stars.png);
}
            

nib


npm install nib
        
@import 'nib'
        
@import 'nib/gradients'
@import 'nib/buttons'
        

Gradient


body {
    background linear-gradient(bottom left, 80% white, blue, red)
}
            

body {
    background: -webkit-linear-gradient(bottom left, #fff 80%, #00f, #f00);
    background: -moz-linear-gradient(bottom left, #fff 80%, #00f, #f00);
    background: -o-linear-gradient(bottom left, #fff 80%, #00f, #f00);
    background: -ms-linear-gradient(bottom left, #fff 80%, #00f, #f00);
    background: linear-gradient(to top right, #fff 80%, #00f, #f00);
}
            

Position


#back-to-top {
    fixed bottom 10px right 5px
}
        

#back-to-top {
    position: fixed;
    bottom: 10px;
    right: 5px;
}
        

Transparent Mixins


.animate-item {
    animation-delay 1s;
    animation-duration 1s;
}
        

.animate-item {
    -webkit-animation-delay: 1s;
    -moz-animation-delay: 1s;
    -o-animation-delay: 1s;
    animation-delay: 1s;
    -webkit-animation-duration: 1s;
    -moz-animation-duration: 1s;
    -o-animation-duration: 1s;
    animation-duration: 1s;
}
        

Responsive Images


#logo {
    image '/images/logo.main.png'
}
        

#logo {
    background-image: url(/images/logo.main.png);
}

@media all and (-webkit-min-device-pixel-ratio: 1.5) {
    #logo {
        background-image: url(/images/logo.main@2x.png);
        background-size: auto auto;
    }
}
        

Комментарии


// Очень содержательный комментарий
        

/*
Длинный содержательный комментарий
*/
        

Debug

source maps

Создание


stylus index.styl -m
        

{
    "version": 3,
    "sources": ["index.styl"],
    "names": [],
    "mappings": "AA2BQ;EACI,YAAwB,kCAAxB;EACA,iBAAiB,KAAjB...
    "file": "index.css"
}
        

DevTools

WebStorm

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

CSS

Парсер

АSТ

Плагины

toString

CSS

PostCSS


npm install postcss
        

npm install autoprefixer
        

Запуск


postcss --use autoprefixer -c options.json -o main.css css/*.css
        

options.json


{
    "autoprefixer": {
        "browsers": "> 5%"
    }
}
        

{
    "autoprefixer": {
        "browsers": "Firefox > 20, last 2 Chrome versions"
    }
}
        

Webpack


module.exports = {
    module: {
        loaders: [
            {
                test:   /\.css$/,
                loader: "style-loader!css-loader!postcss-loader"
            }
        ]
    },

    postcss: function () {
        return [require('autoprefixer'), require('precss')];
    }
}
        

Autoprefixer


.box {
    transition: transform 1s
}
        

.box {
    -webkit-transition: -webkit-transform 1s;
    transition: -ms-transform 1s;
    transition: transform 1s
}
        

Color short


.box {
    border-bottom: 1px solid rgb(200);
    background: #20;
    color: #f;
    box-shadow: 0 1px 5px rgba(0, 0.5);
}
            

.box {
    border-bottom: 1px solid rgb(200, 200, 200);
    background: #202020;
    color: #fff;
    box-shadow: 0 1px 5px rgba(0, 0, 0, 0.5);
}
            

Size


.one {
    size: 20px 10px;
}

.two {
    size: 10px;
}
        

.one {
    width: 20px;
    height: 10px;
}

.two {
    width: 10px;
    height: 10px;
}
        

postcss-sprites


.comment {
    background: url(images/sprite/ico-comment.png) no-repeat 0 0;
}

.bubble {
    background: url(images/sprite/ico-bubble.png) no-repeat 0 0;
}
        

.comment {
    background-image: url(images/sprite.png);
    background-position: 0 0;
}

.bubble {
    background-image: url(images/sprite.png);
    background-position: 0 -50px;
}
        

Font Magician


body {
    font-family: "Alice";
}
        

@font-face {
    font-family: "Alice";
    font-style: normal;
    font-weight: 400;
    src: local("Alice"), local("Alice-Regular"),
        url("http://fonts.gstatic.com/s/alice/v7/sZyKh5NKrCk1xkCk_F1S8A.eot?#") format("eot"),
        url("http://fonts.gstatic.com/s/alice/v7/l5RFQT5MQiajQkFxjDLySg.woff2") format("woff2"),
        url("http://fonts.gstatic.com/s/alice/v7/_H4kMcdhHr0B8RDaQcqpTA.woff")  format("woff"),
        url("http://fonts.gstatic.com/s/alice/v7/acf9XsUhgp1k2j79ATk2cw.ttf")   format("truetype")
}
body {
    font-family: "Alice";
}
        

PostCSS BEM


@b nav {
    @e item {
        display: inline-block;
    }
    @m placement_header {
        background-color: red;
    }
}
        

.nav__item {
    display: inline-block;
}

.nav_placement_header {
    background-color: red;
}
        

CSSNano


h1::before, h1:before {
    margin: 10px 20px 10px 20px;
    color: #ff0000;
    -webkit-border-radius: 16px;
    border-radius: 16px;
    font-weight: normal;
    font-weight: normal;
}
/* invalid placement */
@charset "utf-8";
        

@charset "utf-8";h1:before{margin:10px 20px;
color:red;border-radius:1pc;font-weight:400}
            

CSSNext

Any-Link


nav :any-link > span {
    background-color: yellow;
}

        

nav :link > span,
nav :visited > span {
    background-color: yellow;
}
        

Matches


.rating-star:matches(:first-child, .special) {
    color: red;
}
        

.rating-star:first-child, .rating-star.special {
    color: red;
}
        

postcss-color-rgba-fallback


.rgbaFallback {
    background: rgba(0,0,0,0.5);
}
        

.rgbaFallback {
    background: #000000;
    background: rgba(0,0,0,0.5);
}
        

postcss-opacity


.opacityFallback {
    opacity: 0.5;
}
        

.opacityFallback {
    opacity: 0.5;
    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
}
        

node-pixrem


body {
    font-size: 16px;
}

.remFallback {
    height: 10rem;
    font: 2rem Arial;
}
        

body {
    font-size: 16px;
}

.remFallback {
    height: 160px;
    height: 10rem;
    font: 32px Arial;
    font: 2rem Arial;
}
        

Полезные ссылки

Документация Stylus, nib

PostCSS, список плагинов

CSSNano

CSSNext

"Используем PostCSS правильно"

И снова о вёрстке

Веб-компоненты

  • Templates
  • Custom Elements
  • Shadow DOM
  • Imports

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

  • Jade
  • Handlebars
  • Mustache
  • Django Templates (Python)
  • Smarty (PHP)
  • BEM
  • тыщи их...
  • HTML + JavaScript

Шаблоны в HTML



Шаблоны в HTML



Шаблоны в HTML



Templates

«Method of declaring a portion of reusable markup that is parsed but not rendered until cloned»
http://caniuse.com/#feat=template

Плюсы <templates>

  • Содержимое не обрабатывается и не загружается, пока шаблон не активирован
  • Содержимое не доступно с помощью querySelector и прочих функций
  • Шаблоны можно помещать куда угодно, в <head>, в <body> или даже внутрь <select>

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





var template = document.querySelector('#template1');
document.body.appendChild(template.cloneNode(true));

Поддержка браузерами

  • 2013 – 46%
  • 2015 – 63%
  • 2016 – 72%
  • 2017 – 78%

Поддержка браузерами

  • 2013 – 46%
  • 2015 – 63%
  • 2016 – 72%
  • 2017 – 78% 86%

CustomElements

Типичная вёрстка


...
...
...

Типичная вёрстка HTML5


...
...
...

Вёрстка с CustomElements


...
... ...

Создание


class MyElement extends HTMLElement {
    constructor () {
        super();
    }
};
customElements.define('my-element', MyElement);

Lifecycle callbacks


connectedCallback
// экземпляр элемента добавлен в документ


disconnectedCallback
// экземпляр элемента удалён из документа


attributeChangedCallback(attrName, oldVal, newVal)
// добавление/удаление/изменение аттрибута attrName

class ColorViewer extends HTMLElement {
    constructor () {
        super();
    }

    connectedCallback() {
        this._color = this.getAttribute('color') || '#000';

        this._colorInputElement = document.createElement('input');
        this._colorInputElement.setAttribute('type', 'text');
        this._colorInputElement.setAttribute('value', this._color);
        this.appendChild(this._colorInputElement);

        this._colorViewElement = document.createElement('div');
        this._colorViewElement.style.background = this._color;
        this.appendChild(this._colorViewElement);
    }
};

customElements.define('color-viewer', ColorViewer);

class ColorViewer extends HTMLElement {
    constructor () {
        super();
    }

    connectedCallback() {
        this._colorInputElement = document.createElement('input');
        this._colorViewElement = document.createElement('div');
        this._pickColor();
        this.appendChild(this._colorInputElement);
        this.appendChild(this._colorViewElement);
    }

    _pickColor() {
        this._color = this.getAttribute('color') || '#000';

        this._colorInputElement.setAttribute('type', 'text');
        this._colorInputElement.setAttribute('value', this._color);

        this._colorViewElement.style.background = this._color;
    }
};

customElements.define('color-viewer', ColorViewer);

class ColorViewer extends HTMLElement {
    static get observedAttributes() {
        return ['color'];
    }

    constructor () {
        super();
    }
    connectedCallback() { /* ... */ }
    _pickColor() { /* ... */ }

    attributeChangedCallback() {
        this._pickColor();
    }
};

customElements.define('color-viewer', ColorViewer);
Demo

class ColorViewer extends HTMLElement {
    static get observedAttributes() { /* ... */}
    constructor () { /* ... */ }
    connectedCallback() {
        /* ... */
        this._colorInputElement
            .addEventListener('keyup', this._onInputChange.bind(this));
    }
    _pickColor() { /* ... */ }
    attributeChangedCallback() { /* ... */ }

    _onInputChange () {
        this.setAttribute('color', this._colorInputElement.value);
    }
};

customElements.define('color-viewer', ColorViewer);

Поддержка браузерами

  • 2013 – 45%
  • 2015 – 45%
  • 2016 – 48%
  • 2017 – 48%

Поддержка браузерами

  • 2013 (v0) – 45%
  • 2015 (v0) – 45%
  • 2016 (v0) – 48%
  • 2017 (v1) – 48% 60%

Shadow DOM



     
    



Создание


var shadow = element.attachShadow({mode: 'open'});
shadow.innerHTML = "Ололо";

document.registerElement('my-element', {prototype: Shadow});

Shadow DOM + Templates + CustomElements

Всё вместе


var Shadow = Object.create(HTMLElement.prototype);
Shadow.createdCallback = function() {
    var shadow = this.createShadowRoot();
    var template = document
       .querySelector('template#myTemplate');
    shadow.appendChild(template.content);
};

document.registerElement('my-element', {
    prototype: Shadow
});

Поддержка браузерами

  • 2013 – 33%
  • 2015 – 46%
  • 2016 – 52%
  • 2017 – 59%

Поддержка браузерами

  • 2013 (v0) – 33%
  • 2015 (v0) – 46%
  • 2016 (v0) – 52%
  • 2017 (v1) – 59% 65%

Imports

Загрузка внешних ресурсов

  • <link rel="stylesheet"> для загрузки CSS
  • <script src> для загрузки скриптов
  • <img> для загрузки картинок
  • <audio> для загрузки аудио
  • <video> для загрузки видео
  • ??? для загрузки HTML

Загрузка HTML

  • <iframe> – всё своё (контекст, JS, стили), трудно взаимодействовать
  • AJAX – нужен JS, сложно кэшировать
  • <script type="text/html">

Imports


<head>
    
</head>

Особенности

  • Вёрстка и CSS глобальные
  • JavaScript глобальный, но поддерживает локальный скоуп через document.currentScript.ownerDocument
  • Кэширование вёрстки в браузере
  • Не блокируют загрузку страницы (async)

Поддержка браузерами

  • 2013 – ?
  • 2015 – 40%
  • 2016 – 48%
  • 2017 – 56%

Ссылки