Главная » Css live » Никто не знает CSS: специфичность — не каскад

Никто не знает CSS: специфичность — не каскад

Пролог (в котором едва обошлось без драки)

Прошедшие выходные ознаменовались небольшой драмой в веб-сообществе. Началась она с безобидного «теста» по CSS в твиттере Макса Штойбера, разработчика styled-components и react-boilerplate:

Насколько хорошо вы знаете CSS? (эмодзи: ученик у доски)

При таких классах:

.red { color: red; }
.blue { color: blue; }

какого цвета будут эти дивы?

<div class="red blue">
<div class="blue red">

44% из свыше 14 тысяч ответивших выбрали вариант «первый див синий, второй красный». Правильный ответ, конечно же — оба дива синие: из правил с одинаковой специфичностью применяется последнее в CSS-коде, порядок классов в HTML-разметке на это никак не влияет.

Реакция на опрос, а особенно на его результаты, была довольно бурной. Кто-то искал в вопросе дальнейший подвох и придирался к незакрытым тегам или отсутствию контента (без которого цвету, а не фону, не к чему примениться, и формально оба дива будут прозрачными:). Но во многих ответах сквозила мысль, что ни к чему разработчикам тратить время на эту путаницу, и как хорошо, мол, что нынешние инструменты типа CSS-in-JS позволяют не забивать голову такой ерундой и «сосредоточиться на главном». А такие «каверзные вопросы», мол, годятся лишь для того, чтоб «срезать» неугодных кандидатов на собеседованиях.

Многих евангелистов CSS эта мысль просто возмутила. Эрик Мейер, автор легендарной «книги с рыбами», в запале написал:

Если вы так боитесь каскада, что отказываетесь его изучать, вы не понимаете веба и лучше вам посмотреть в сторону чего-нибудь попроще в другой области кодинга. Спасибо, что прослушали мой доклад для фронтендеров.

Потом он пояснял, что не хотел никого обидеть и действительно лишь предлагал разработчикам обратить внимание на области, основы которых не кажутся им настолько чуждыми. Но пожар уже разгорелся. Тут закономерно возмутились уже JS-разработчики: что это за язык такой, для входа в который непременно нужно показать знание каких-то странных и нелогичных шаманских ритуалов, почему-то называемых «самыми основами», хотя без них можно прекрасно обходиться? Эхо той битвы прокатывается по твиттеру до сих пор, почти не утихая.

Что сильнее всего удивило в этой драме меня — то, как много спорщиков с обеих сторон баррикад не знали, что такое каскад.

Никто не знает CSS

Это сильное утверждение — не фигура речи, я готов за него ответить. Никто не знает CSS. Ни вы. Ни я. Ни даже Эрик Мейер или Таб Аткинс. И это абсолютно нормально.

«Знать CSS» невозможно хотя бы потому, что само определение CSS как языка постоянно меняется. Модули спецификации появляются и отмирают. В них вносятся исправления, подчас незаметно переворачивающие основы. Некоторые модули существуют в двух-трех вариантах (разного уровня) одновременно, и иногда один и тот же код считается ошибкой по одному из них и валидным по второму. Поддержка браузерами — тоже не критерий: веб-стандарты — дело добровольное, поэтому браузеры поддерживают то, что им удобно и так, как им удобно. Включая такое, чего ни в одном стандарте не было.

Так что не знать, какая сейчас специфичность у какого-нибудь :matches(), не стыдно. На таких вопросах можно «засыпать» даже редакторов спецификации (они не всегда успевают за правками друг друга:). Но некоторые вещи, мне кажется, достаточно фундаментальны для того, чтобы знать их в принципе. Откуда вообще берутся стили. Как CSS обрабатывает ошибки. Общие принципы построения раскладки. Опять же, не нужно помнить это всё наизусть — даже эти знания в CSS в любой момент могут устареть и потерять актуальность! Но как минимум помнить, где найти: где лежат первоисточники (спецификации), какие первоисточники наиболее актуальны (текущий редакторский черновик, отмеченный в списке как «Current work») и т.п.

Понятие каскада, на мой взгляд, вполне заслуживает места в списке таких основ. Не зря первая буква в аббревиатуре CSS обозначает именно его, и никакие «CSS-in-JS» эту букву отменить не в силах! Тем более, что у нас есть замечательный повод обновить знания: совсем недавно спецификация для него обновилась, и в ней, как водится, появилось немало сюрпризов. С конца прошлого месяца основной спецификацией для CSS-каскада стал…

Модуль каскада и наследования 4 уровня

Между прочим, кандидат в рекомендации, т.е. полностью готов теоретически. Но мы будем сверяться с редакторским черновиком — он заведомо актуальнее, со всеми текущими исправлениями ошибок.

Модуль немаленький, так что начнем знакомство с ним с раздела 8, «Изменения»: что новенького (по сравнению с предыдущим, 3-м уровнем)? Основных новинок три:

  • Новое ключевое слово revert, чтобы «откатить каскад» (!);
  • Условное подключение стилей через @import с помощью директивы @supports;
  • Добавлено определение того, как каскадируются стили с ограниченной областью видимости («scoped styles» в оригинале).

Если вы внимательно следили за недавней эволюцией HTML, последний пункт может вас удивить. Казалось бы, «scoped styles» выпилили из спецификации еще два года назад, браузеры тоже отказались от их поддержки, что ж CSS-ники только сейчас «выспались» с этой новинкой? На всякий случай приготовьтесь: с этим пунктом связана одна небольшая сенсация. И (как минимум) одна большая загадка.

Но сначала надо наконец разобраться как следует, что же такое этот самый каскад?

Главная часть (в которой без драки не обошлось:)

Как часто бывает в CSS-спецификациях, ключевые понятия даются либо вообще без определения, либо определяются косвенно, через то, что они делают. Каскад — не исключение:

Каскад берет неупорядоченный список объявленных значений для данного свойства у данного элемента, сортирует их по приоритету объявлений как описано ниже, и выводит единственное каскадное значение.

Так что каскад — никакие не «вложенные селекторы» и прочие домыслы, что часто ошибочно ассоциируют с этим словом. На самом деле это своего рода турнир на выбывание, этакий «мортал комбат» для CSS-объявлений разного вида и происхождения, которые должны сразиться друг с другом, чтобы в итоге остался только один — сильнейший. Этот победитель получит всё, именно его значение будет определять вид элемента, а проигравших ждет полное забвение. Оставшаяся часть 6-го раздела спецификации разъясняет правила этого турнира.


Рис. 1. CSS-каскад в представлении художника XIX века: водопад, на котором значения свойств борются друг с другом до последнего выжившего (фрагмент изображения с Wikimedia Commons)

Первое, на что обращают внимание для CSS-объявлений — их происхождение и важность (эх, совсем как в жизни:). Затем — новинка! — область видимости. Специфичность, вокруг которой вечно ломают столько копий, идет лишь третьей (так что те, кто не считал ее «фундаментальным знанием», не так уж неправы:). Наконец, последним идет порядок в коде — но именно он решает исход поединка, когда прочие факторы равны. Свежий боец всегда побеждает равного по силе, но уставшего, следующее правило того же происхождения, важности, области видимости и специфичности перекрывает предыдущее. Кратко рассмотрим все эти факторы.

Происхождение (источник) и важность

Небольшой лайфхак: если вам что-то очень не нравится в CSS-спецификациях — непонятно, громоздко, просто не нравится — можно пойти на гитхаб рабочей группы CSS и попросить это убрать. Лично мне в предыдущих редакциях CSS-каскада не нравились какие-то «перекрывающие стили»: я их не понимал (на чем однажды попался), они загромождали алгоритм, были плохо документированы и — главное — никогда (?) не работали ни в одном браузере. В общем, их больше нет:). И порядок приоритетности стилей по источнику и важности отныне такой (по убыванию):

  1. Стили во время CSS-переходов (transition)
  2. Браузерные стили с !important
  3. Пользовательские стили с !important
  4. Авторские (т.е. наши, разработческие) стили с !important
  5. Стили во время анимаций
  6. Обычные (т.е. без !important) авторские стили
  7. Обычные пользовательские стили
  8. Обычные браузерные стили

У этого порядка есть логика. Браузерные стили — последний резерв, если ничего лучше не нашлось. Если пользователю они неудобны, он вправе переопределить их. Но дизайнер и разработчик сайта/приложения, скорее всего, могут сделать еще лучше и удобнее (они же профессионалы!), так что есть смысл положиться на их выбор. Далее, от анимации не было бы видимого проку, если бы значения, которые она меняет, не перекрывали статичные стили — поэтому эти изменения должны быть приоритетнее. Кроме действительно важных стилей. Которые, в свою очередь, в совсем уж крайнем случае тоже может переопределить пользователь (да, обычно дизайнер знает лучше, но всегда найдутся исключения). Наконец, у браузеров могут быть свои «секреты», которые нельзя переопределять никому (например, невыделяемость текста в поле типа password в WebKit). Ну а плавный переход от одного значения к другому возможен лишь тогда, когда это «переходное» значение приоритетнее начального и конечного — откуда бы те ни пришли (иначе вместо перехода будет резкий «перескок»).

Со стилями при анимациях есть два неочевидных, но важных нюанса. Во-первых, к ним относятся не только обычные @keyframe-анимации, но и анимации через Web Animations API: для CSS-каскада это одно и то же. А во-вторых, как и три года назад, правильно по стандарту им назначает приоритет один лишь Firefox. В остальных браузерах анимации перекрывают даже стиль с !important. В некоторых зарубежных статьях встречается утверждение, будто именно это — стандартное поведение: не верьте им, они врут (как минимум, заблуждаются).

И все те, кто гордо бьют себя пяткой в грудь «Я не использую каскад!» и при этом не довольствуются браузерными стилями, а перекрывают их своими — без разницы, «-in-JS» или просто так — получается, тоже врут:). Не шутите с каскадом!

Область видимости

В этом пункте, как в сказке, чем дальше — тем страньше и чудесатее:)

Общее правило очень похоже на правило специфичности: чем «прицельнее» стиль, то есть чем уже его область видимости, тем он приоритетнее. Но для стилей с !important всё опять наоборот — чем шире область видимости, тем они важнее. Если область видимости явно не задана, то она ограничена корневым элементом.

Теперь — внимание: в последнем абзаце (между двумя примечаниями) этого пункта притаилась обещанная сенсация!

Область видимости обычных объявлений из атрибутов style считается ограниченной элементом с этим атрибутом.

Такие дела: оказывается, стили в атрибуте style, такие простые и знакомые… и есть те самые таинственные scoped-стили! И у них есть своё место в каскаде. А значит, и все варианты CSS-in-JS, работающие через инлайновые стили (например, Radium) не «избавляют от каскада», а занимают в нем это же место.

Если раньше стили из атрибута style отличались только наивысшей специфичностью, то теперь у них появилась отдельная область видимости, ограниченная самим элементом, и поэтому он важнее. Но у !important-стилей из атрибута style область видимости считается ограниченной… опять корневым элементом. А чтоб выделить их из прочих !important-стилей, им по-прежнему присваивается наивысшая специфичность.

Правда, если перевести это всё с спецификаческого на человеческий, то функционально с атрибутом style ничего не изменилось, работает он как привычно. Просто это поведение стало частным случаем мудрёного общего правила. Но мне стало интересно поискать и другие случаи. Первым делом, конечно, я подумал про теневую DOM. Ведь целый модуль CSS под названием «CSS Scoping», т.е. «ограничение области видимости CSS», сейчас посвящен именно ей!

И тут я наткнулся на очередной сюрприз. Оказывается, теневая DOM не использует критерий области видимости из каскада, а вместо этого

…должен быть добавлен дополнительный критерий каскада, между «Источником» и «Областью видимости», под названием «Теневое дерево».

При сравнении двух объявлений, у которых разный контекст дерева, для обычных правил побеждает объявление, идущее раньше в дереве, включающем теневую DOM, а для правил с !important побеждает объявление, идущее в нем позже.

Примечание: это противоположность тому, как работают scoped-стили.

Признаюсь, на этом месте я совсем запутался. Новейший модуль каскада не всё знает про каскад? Критериев каскада всё больше и больше? Представить ситуацию, когда к одному элементу применяются правила из разных уровней теневой иерархии (вложенные веб-компоненты?) мне сходу не удалось. Казалось бы, границы теневых элементов для CSS-селекторов непреодолимы, можно только наследовать свойства окружения, да выделять сам корень теневой DOM изнутри через :host и его друзей. В погоне за иными селекторами, способными «проникать в Сумрак» и «переходить между его уровнями», пришлось продираться сквозь дебри других спецификаций… и в какой-то момент, глядя на чудесный пример из статьи Моники Динкулеску про эти теневые части, я обреченно осознал: да, я не знаю CSS.

Может быть, в комментариях вы поможете мне разобраться?..

Специфичность

А вот про нее мы много говорить не будем. Про нее вы и так знаете. Или нет… но это и впрямь не так уж важно. Специфичность — не первое дело в каскаде. И даже не второе.

И правила специфичности можно не зубрить. Классические шпаргалки, помогающие в них ориентироваться — по рыбам или с помощью темной стороны Силы — еще актуальны. Не забывайте только про :not(), который получает специфичность своего аргумента. Об остальных исключениях говорить пока рано.

Порядок в коде

Здесь тоже вроде бы всё ясно: чем позже в коде, тем важнее. Ясно, что классы-модификаторы должны идти после немодифицированных классов (и позже подключаться, если они в отдельном файле). Но не забывайте, что стили анимаций — даже из того же самого файла — считаются другим источником, причем более приоритетным. И даже если объявить их в самом начале, они всё равно перекроют по каскаду вашу идеально плоскую структуру классов. И даже стили, бережно расставленные скриптом по атрибутам style! А в Chrome, Safari и Edge — даже если у них будет !important (это уже не по стандарту, но учитывать это надо).

Дальнейшие приключения CSS-значений

Но сражение и победа в смертельной битве каскада — еще не конец истории CSS-значения. Это всего лишь переход с первой стадии их жизненного цикла на вторую. А всего этих стадий целых шесть! Давайте быстренько пробежимся по ним: это тоже важные понятия для того, чтобы претендовать на знание CSS.

Объявленные значения (declared values)

Сначала CSS отбирает «претендентов» на участие в турнире: выбирает правила, в принципе относящиеся к данному элементу — из стилей, присоединенных к документу, внутри сработавших условий @media, @supports и т.д., с совпадающими селекторами (а может, и не селекторами — взять тот же атрибут style). Из браузерных стилей, из стилей сайта/приложения — откуда угодно. Может случиться и такое, что претендентов на какое-то свойство для какого-то элемента не окажется вообще (список объявленных значений может быть пустым).

Каскадное значение (cascaded value)

Этот «титул» достается единственному победителю в каскаде. Если претендентов не нашлось и турнир не состоялся — нет и победителя (каскадного значения).

Так что, чтобы избавиться от каскада, нужно избавиться от всех объявленных значений для всех свойств — в том числе от браузерных стилей. Иначе, даже если вы не ничего не перекрываете своими стилями, в браузере вы всё равно видите результат каскада!

Указанное значение (specified value)

Право считаться указанным значением свойства для элемента — тот «приз», за который значения «бились» в каскаде. Но если каскадного значения нет (турнир не состоялся), указанное значение свойству всё равно назначается. Это значение по умолчанию. То, что помечено в спецификации для этого свойства как «начальное» (initial).

Вот почему, например, все неизвестные HTML-элементы, для которых заведомо нет никаких браузерных стилей, отображаются как display:inline — это начальное значение для display.

Вычисленное значение (computed value)

Чтобы как-то работать со значением дальше, указанное значение надо преобразовать во что-то конкретное. Например, практически всё с размерностью длины переводится в пиксели: font-size: 0.6em при дефолтных 16px у предка превратятся в 9.6px. И потомки элемента унаследуют именно этот результат вычисления — вычисленное значение.

Для разных свойств бывают свои нюансы: скажем, для line-height 1.5em и 150% пересчитаются в пиксели (и это значение в пикселях достанется потомку, независимо от его собственного font-size), а вот безразмерный множитель 1.5 так и унаследуется безразмерным (и для каждого потомка будет переводиться в пиксели заново, с его font-size). Напротив, для width проценты наследуются именно как проценты, без перевода в пиксели (так что width: inherit в родителе с width: 80% будет соответствовать тем же 80% от родителя, или 64% от его контейнера). Обычно такие нюансы оговариваются в спецификации для каждого свойства, в отдельной одноименной графе, так что присматривайтесь к ней повнимательнее.


Рис. 2. Пример, где искать нюансы про вычисленные значения в спецификациях

А вот метод getComputedStyle, несмотря на название, далеко не всегда возвращает именно вычисленное значение (о чем спецификация CSS Cascade честно предупреждает).

И еще важный нюанс: вычисленное значение у свойства есть даже тогда, когда оно к этому элементу не применяется. Например, вычисленные значения flex могут быть не только у флекс-элементов. Или border-spacing — не только у таблиц. А поскольку последнее как раз наследуется, это может привести к занятным побочным эффектам. Скажем, весьма забавно выглядит результат <html style=”border-spacing:100px”> на странице с Bootstrap 3 (и вообще везде, где активно используется «micro clearfix» Галлахера). Можете даже по-доброму разыграть так кого-то из менее опытных в CSS коллег (главное, потом всё-таки объясните им, в чем был фокус и откуда взялись гигантские отступы:).

Используемое значение (used value)

На этой стадии значения уже никуда не передаются, так что браузер может «допиливать» их до чего-то совсем конкретного: переводит в пиксели всё, что можно в них перевести (включая безразмерные line-height и процентные width), все значения типа normal и auto тоже переводятся в какие-то конкретные, нормальные числа.

Именно эти значения возвращает метод getComputedStyle для некоторых свойств (по историческим причинам). А вот если свойство к элементу не применяется, то и используемого значения у него нет.

Фактическое значение (actual value)

Но и используемое значение — не конец его приключений. Иногда браузер не может применить значение, не округлив его: скажем, вместо рамки толщиной 2.2px приходится рисовать просто 2px. Или упомянутые 9.6px для размера шрифта отображать как 10px (или 9px… когда-нибудь мы узнаем это более точно. Или не узнаем. Но попытаемся. На всякий случай не забывайте следить за обновлениями!:).

В спецификации каскада есть целая таблица примеров, что может происходить с разными значениями разных свойств на разных этапах их дальнейшей «карьеры» после победы в каскаде.

Управление каскадом

Итак, совсем избавиться от каскада нельзя. Но можно укротить и подчинить его стихию своей воле. Спецификация всегда готова рассказать, как.

Для этого модуль каскада и наследования предусмотрел целых 4 универсальных значения, подходящих для всех свойств: initial, inherit, unset и revert. И целое универсальное свойство all, которое умеет применять эти универсальные значения ко всем свойствам сразу (ну, почти). Познакомимся с ними поближе?

Значение initial

Присваивает свойству его начальное значение. То самое, которое свойство получает при «неявке претендентов на турнир», при отсутствии каскадного. Для display это всегда будет inline, для большинства свойств шрифта — nоrmal, для размеров — auto, для отступов — 0, для большинства «спецэффектов» — none

Значение inherit

Заставляет свойство элемента унаследовать значение этого же свойства его родителя. То есть в его указанное, а затем и вычисленное значение переходит вычисленное значение от родителя. Причем для любого свойства, даже изначально ненаследуемого.

Например, можно поставить width: inherit элементу, вложенному в контейнер с процентной шириной, и он всегда будет занимать в нем такую же часть, как сам контейнер в своем родителе. Получится такой «фрактальный» интерфейс, где части подобны целому (правда, не могу подобрать пример, где это надо:).

Значение unset

Фактически, отменяет каскадное значение, как будто для этого свойства элемента нигде никаких стилей не задано. Наследуемые свойства при этом получают родительское вычисленное значение (как при inherit), а ненаследуемые — «обнуляются» до своего начального (как при initial).

Фактически, это значение, особенно если «коврово» применить его для всего подряд — это готовый CSS-ресет («ластик»):

* { all: unset; /* отменит все браузерные и пользовательские стили без !important для всех элементов */
}

Только, пожалуй, слишком уж радикальный. Сбрасывать display у <div> и <p> до inline — наверное, чересчур…

Значение revert

Для тех, кому unset кажется слишком жестким решением, модуль каскада 4 уровня добавил более мягкую альтернативу: «обнулить» каскадное значение не полностью («вообще нигде никакого»), а отменить только один текущий уровень каскада. Если оно в авторских стилях — оставить только браузерные и пользовательские. Если в пользовательских — оставить только браузерные. Ну а если в браузерных — не оставлять ничего (там revert и unset эквивалентны). Стили анимаций для этого механизма (и только для него!) считаются частью авторских стилей.

К сожалению: опробовать это значение в деле труднее, чем предыдущие: поддерживается оно пока только в Safari. Но значения initial и unset (появившиеся еще на 3 уровне модуля) работают везде, кроме IE, а inherit (оно вообще еще из CSS2.1) — даже в нем.

Свойство all

Это уникальное свойство формально считается сокращением (shorthand) для всех остальных свойств CSS. За исключением кастомных свойств (которые CSS-переменные), а также (по историческим причинам) свойств, отвечающих за направление текста — direction и unicode-bidi. Впрочем, от использования последних спецификация всячески отговаривает — мол, за это должна отвечать разметка (атрибут dir и т.п.).

Но в отличие от других сокращений, у него нет полной записи (и хорошо: представьте, как выглядела бы запись свыше 580 свойств, не считая нестандартных, одной строкой!). И значение у него может быть только одно из четырех универсальных, перечисленных выше: все свойства сразу можно только либо сбросить на начальные значения, либо унаследовать от родителя, либо «отменить», как будто они вообще не были указаны, либо «откатить» на один уровень каскада (в нашем случае, до браузерных стилей).

CSS-ресет на этом свойстве — фактически в одну строчку — мы уже видели. Увы, использовать его на практике пока проблематично: по обыкновению, картину портят браузеры от Microsoft (и примкнувшая к ним Opera Mini), а существующие полифилы просто разворачивают его в огромный список свойств (может быть, в вашем частном случае это годится, но веб и так слишком раздут…). Но для современных браузеров уже есть возможность легко и быстро избавить компонент от любых зависимостей от внешних стилей. А нужные стили, например, для подстройки под тему оформления, передавать в него теми же CSS-переменными.

На Microsoft-овском форуме UserVoice у свойства all стоит статус «Мы подумаем», то есть «Поставлено на очередь» (on the backlog). Но почему-то за него отдано всего чуть больше 600 голосов. По-моему, маловато будет (для такой важной фичи-то). Давайте поднажмем и добьем хотя бы до тысячи! 🙂

Эпилог (в котором обязательно должна победить дружба)

Веб-платформа стала настолько необъятной, что в ней не всегда ориентируются даже ее создатели. CSS — важная часть платформы, большая и сложная. Он не идеален, но реальной альтернативы для него нет. Инструменты — пре-, пост-, и чего-угодно-процессоры, фреймворки, библиотеки и методологии — не заменяют его, а лишь упрощают работу с ним, помогая обойти типичные острые углы и очевидные грабли. У него немало недостатков, но многие из них — продолжение его сильных сторон.

CSS сложен. Времена, когда он был простым языком для раскрашивания ссылок, прошли тогда же, когда и JavaScript перестал быть несерьезным языком для менюшек и летающих по экрану снежинок. Не знать CSS не стыдно: он постоянно меняется и развивается, и знания в нем, как и в других областях IT, быстро устаревают. Стыдно — на мой взгляд — потешаться над людьми, которые знают его лучше, обладая лишь иллюзией знания а-ля «есть какой-то там “каскад”, он злая-бяка-выжечь-каленым железом, JS — спасение».

Так относится ли каскад к основам CSS, без знания которых нельзя и подступаться к нему, или это «тайное знание для маньяков», которое обычному человеку ни к чему? На мой взгляд, всё дело в глубине этого знания. Знать наизусть все тонкости специфичности, включая хаки вида .please.please.please {} и :not(#just-ordinary-class), пожалуй, и впрямь ни к чему. Но в принципе знать, что 1) все стили в вебе каскадные, 2) они специально устроены так, что значения для свойств могут приходить из нескольких мест и иметь разный приоритет в зависимости от каких-то условий, 3) если делать всё правильно — хорошими инструментами, по хорошей методологии и т.п. — то у нужных стилей будет достаточный приоритет, но 4) существуют особые случаи (как с теми же анимациями), и это не обязательно баг браузеров или стандарта — на мой взгляд, человек, считающий себя веб-разработчиком, всё-таки должен. А частности всегда можно уточнить в справочнике, спецификации… или у более опытных в CSS коллег:). Которые не «дурака валяют, раскрашивая буковки», а точно так же решают свою сложную часть общей задачи по созданию современного динамичного, интерактивного, адаптивно-отзывчивого, доступного, удобного, красивого и быстрого веб-интерфейса.

Сила веба — в объединении разных технологий, сила команды — в объединении разных знаний и умений. Давайте вместе делать веб лучше, учиться друг у друга и стремиться стать выдающимися специалистами!

P.S. Это тоже может быть интересно: