Русский
Русский
English
Статистика
Реклама

Front-end

Из песочницы Как верстать веб-интерфейсы быстро, качественно и интересно

24.06.2020 18:08:11 | Автор: admin

image


Всем привет! Давно хотел и наконец написал небольшую книжку бодрое пособие по своей профессиональной области: актуальным подходам к разметке интерфейсов, экранному дизайну и доступности. Она о моем оригинальном подходе к созданию GUI, препроцессорам CSS (для объективности, немного и об альтернативных подходах), и его эффективном практическом использовании с javascript и популярными реактивными компонентными фреймворками Vue и React. Материал представлен аккуратно последовательно, но безумно интенсивно и динамично ничего лишнего или даже слишком подробного для того чтобы увлеченный и подготовленный читатель не потерял интереса и проглотил на одном дыхании. С другой стороны, текст, достаточно сложный ближе к концу, и на всем протяжении густо насыщенный идеями, ссылками на технологии и подходы поэтому, очевидно, будет на вырост начинающим. Но, в любом случае, как и если вы только начали интересоваться данной тематикой, так и если уже давно занимаетесь веб-дизайном, версткой и программированием фронтенда вам может быть полезно на него взглянуть.


Мотивация


Я уже много лет занимаюсь, прежде всего, разметкой интерфейсов, их оформлением и поведением, экранами и GUI. Участвовал на позиции верстальщика в небольших командах, стартапах, а также реализовал некоторое количество проектов самостоятельно как фрилансер, дизайнер и верстальщик. За это время я накопил некоторый опыт по специфическим проблемам в данной области, которым мне очень хочется поделиться.


Начну с некоторого огульно обобщающего и, поэтому, несколько провокационного наблюдения, вброса: большинство, не только начинающих, но даже опытных программистов испытывают своеобразное предубеждение-стереотип о некой несерьезности верстки интерфейса как отрасли деятельности в веб-индустрии верстка это не программирование!. Очень многие считают что заниматься разметкой некруто и скучно для серьезного специалиста. И как следствие уделяют предмету мало своего внимания и времени, имеют мало реального релевантного опыта. Проще говоря, большинство разработчиков не любит и не умеет верстать. И уже и как причина и как следствие зачастую сами подходы, технологии и архитектура используемые для организации GUI даже на серьезных коммерческих проектах отставляют желать лучшего, не отвечают реалиям современной веб-индустрии, устарели и недостаточно эффективны. Неудачные, якобы временные, слабые решения и дальнейшие бесконечные быстрые фиксы, кривые кряки наслаиваются друг-на-друга, кодовая база неоправданно распухает и становится все менее связной и контролируемой. Шаблоны, или ныне в эру компонентных фреймворков компоненты, стили и логика для них часто и закономерно превращаются в невнятную и крайне излишнюю по сути свалку, густой лес, джунгли с очевидно неподъемной ценой рефакторинга и масштабирования. Очевидно, что такое состояние системы может легко приводить к серьезным затруднениям, снижению темпов или даже срыву сроков и, ирония как раз в том, что в реальной коммерческой ситуации, авторам подобной громоздкой и неуклюжей системы, все равно придется потратить еще больше времени на то, чтобы, по крайней мере, исправить все оплошности и несовершенства разметки, оформления и логики для них, но делать это придется в изначально плохо организованном коде, по замкнутому кругу только увеличивая беспорядок и вероятность сбоев. С того момента как вы перестаете быть джуном, ваша ответственность состоит не только в том чтобы закрыть все баги на трекере и навернуть как можно скорее фичи, любой ценой. Надежная и легко поддерживаемая система продукт который вы продаете бизнесу.


Реальный жизненный кейс: будучи начинающим специалистом я работал удаленно в одном приятном стартапе. Когда проект запустили, после презентации многие присутствовавшие на ней высказались о том, что кегль основного текста и полей ввода в интерфейсе мелковат. Получив задачу в трекере, я потратил всего пару минут, поправив одну переменную своей системы чтобы поднять кегль на всех нужных полях и контролах, и еще 15 минут чтобы удостовериться что ничего действительно не сломалось ни на одном шаблоне. Ваша система изначально должна быть написана так, и только так, чтобы ее было можно легко скорректировать и улучшить, поддерживать и расширять. По-настоящему лаконичный и выразительный качественный код невероятно мощно экономит время и нервы даже если вы работаете с проектом в одиночку. Кроме того, уважаемые авторитеты в коммерческом программировании утверждают [см. Роберт Мартин Чистая архитектура] что то что сделано изначально плохо в реальности, не только никогда не будет переписано, но и приводит к постоянному крутому росту стоимости дальнейшей доставки нового функционала, а в перспективе способно полностью блокировать прогресс по проекту!


Хорошее понимание возможностей и ограничений современных браузерных технологий очень желательно и полезно не только для веб-разработчиков, но и для гуманитариев-дизайнеров проектирующих GUI. Часто даже востребованный специалист, выдающий приятный стиль и не делающих серьезных ошибок по UХ, не может внятно ответить на простейшие технические вопросы о своем дизайне при сдаче макета на верстку, то есть не понимает что рисует.


В этом пособии я стараюсь в достаточно доступной форме дать некоторые полезные идеи о современном экранном дизайне и эффективных способах его реализации для браузера. Я буду приводить конкретные примеры кода, но не ради специфических моментов и подробностей реализации, а, прежде всего, иллюстрируя и подчеркивая общие мысли, идеи, практические подходы, годные для написания крупных сложных современных проектов. Этот текст не еще один занудный скучный учебник, а, скорее, методичка углубленного спецкурса. Материал будет подаваться максимально интенсивно и насыщенно, и новичок может не справиться сходу с примерами кода или даже содержанием некоторых разделов. Не паникуйте. Важнее всего в нашей отрасли научиться развиваться самостоятельно в выбранном направлении. Этот текст скорее призван помочь вам понять что гуглить дальше?.


Кому будет полезен текст?


Начинающим. Вы минимально освоились со спецификациями HTML и CSS, начали пробовать JavaScript, успешно сверстали свои первые страницы, макеты и хотите большего, хотите получить некий дружеский пинок под зад, который поможет вам осознать дальнейшие горизонты, выйти на новый уровень и начать получать больше удовольствия и удовлетворения, как и от самой этой деятельности, так и от ее результатов.


Не верстающим как бог. Вы давно занимаетесь веб-программированием, но верстаете нехотя и по-старинке, при этом, осознаете свой пробел в знаниях и навыках, созрели для того чтобы отбросить оковы лени и невежества, переломить ситуацию ведь на каждом проекте разработка GUI для вас это скучная трудная рутина и мука.


Дизайнерам. Вы веб-дизайнер, но хотите начать верстать.


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


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


В первой части представлена мощная практическая метода, дающая яркие идеи и четкие рекомендации по организации разработки стилей и разметки. Материал выбран таким образом, чтобы последовательно, но с крутой интенсивностью ввести, возможно даже, только еще минимально овладевшего HTML и CSS читателя, в мир удивительных возможностей препроцессора. Хочется показать, как можно стремительно организовать рабочую кухню, необходимую и достаточную для того чтобы эффективно создавать качественную адаптивную разметку. В этой части почти ничего не говорится о самом javascript, для того, чтобы, если читатель не освоил еще в достаточной степени язык программирования, но уже начал верстать все равно смог бы все понять и начать внедрять на практике.


Препроцессор, JavaScript и фреймворки


Вторая часть не такая злая. Она, например, показывает в увлекательной форме некоторые углубленные интересные кейсы современного экранного дизайна и вопросов связанных с доступностью веб-приложений. В ней речь идет о практическом применении подходов к препроцессору представленных в первой части, в связке с javascript для GUI, особенно в контексте компонентных фреймворков.


Левон Гамбарян.
Июнь 2020 года.



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




Простейший пример плохого кода


Безобразного кода с каждым днем становится все больше, несмотря на то, что больнее всего страдают от этого сами авторы, горе-разработчики. И если определенную функциональность или рендер можно покрыть тестами, то ошибки оформления выявляются только ручками.


Также, просто парадоксально, что все программисты слышали о простых основополагающих принципах качественного программирования KISS и SOLID, но подавляющее большинство напрочь забывает о них, когда речь заходит об организации оформления, представления веб-интерфейса.


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


Давайте посмотрим на самый простейший пример плохого кода на CSS:


/* Примитивнейший пример обычного плохого кода на CSS *//* Где-нибудь в файлах стилей: */.selector--1 {  width: 200px;  height: 200px;  border: 1px solid #ADADAD;  border-radius: 3px;  /* ... и дальше еще огромное количество самых разных правил */}.selector--2 {  width: 200px;  height: 400px;  border: 1px solid #ADADAD;  border-radius: 3px;  /* ... и дальше еще огромное количество самых разных правил */}

Не делайте так больше почти никогда! ))) Почему? Код валидный, в браузере все по макету, да и все именно так обычно и пишут. Но все и не верстают как бог, правильно? В контексте любого проекта чуть большего чем совсем крохотный, подобный код плохой и чреват проблемами в будущем. Он конкретен и невыразителен, излишен, его сложно модифицировать и переиспользовать.


А как надо?


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


Справедливости ради, нужно упомянуть, что последние годы, в связи с стремительным ростом популярности компонентных js-фреймворков и их подходов, все больше сторонников набирают также различные CSS-in-JS-реализации (например: Styled Components). Скоро, вероятно, можно будет спокойно использовать переменные в самом CSS (CSS Custom Properties). Тема холиварная, существуют контексты и ситуации когда подобный CSS-in-JS подход может оказаться более оправданным и изящным, без сомнения. И даже существует масса реалистичных кейсов когда проще всего будет действительно обойтись несколькими наборами правил на CSS, а любое его расширение будет излишним. Но в общем случае, в реальной коммерческой практике, имхо, для верстки сложных дизайнов и интерфейсов удобнее и эффективнее всего сейчас использовать любой препроцессор, и, шок даже с компонентным фреймворком, дальше я планирую показать как именно это лучше всего делать. Препроцессоры дают максимум возможностей и позволяют стремиться к максимальной выразительности и переиспользуемости. Вот во что превратился бы плохой код выше в SCSS-синтаксисе, наверное самого популярного на сегодняшний день препроцессора Sass:


// В @/src/scss/utils/_variables.scss:$colors__border: #adadad;$border-radius: 3px;// В @/src/scss/utils/_placeholders.scss:%border-block {  border: 1px solid $colors__border;  border-radius: $border-radius;}// В @/src/scss/utils/_mixins.scss:@mixin size($width, $height) {  width: $width;  height: $height;}// В любом месте проекта:.selector {  $selector--1__size: 200px;  $selector--2__width: 200px;  $selector--2__height: 400px;  &--1,  &--2 {    @extend %border-block;    /* ... включение других сущностей препроцессора      и специфическиих правил общих для селекторов */  }  &--1 {    @include size($selector--1__size, $selector--1__size);    /* ... включение других сущностей препроцессора      и специфических правил уникальных для селектора */  }  &--2 {    @include size($selector--2__width, $selector--2__height);    /* ... включение других сущностей препроцессора      и специфических правил уникальных для селектора */  }}

Точно тоже самое легко сделать и на, кажется, недооцененном, но очень удачном Stylus совершенно не важно какой именно расширенный синтаксис вы используете, главное как и зачем. Очень много раз мне приходилось видеть плохой чужой код написанный якобы для препроцессора, видимо, потому что так сейчас модно, но, на самом деле, практически ничем не отличающийся от кода CSS. Не делайте так! Препроцессор дает нам крайне ценную возможность абстрагировать общие качества гайдлайна, стиль и основанные на нем частные стили, организовать их намного более выразительно и лаконично, легко модифицировать и переиспользовать при необходимости.


В данном, вырванном из контекста, но, при этом, вполне жизненном примере, кажется, что кода препроцессора сильно больше. Он еще и раскидан по нескольким разным файлам, что, как будто, еще все усложняет. Зачем так напрягаться, а? Прежде всего, привычка начинать писать разметку с переменных и обобщений очевидно грамотная. Перестаньте плодить изолированные глухие кряки с магическими числами, начните применять абстракцию! Делайте хорошо сразу, потому что вы почти никогда и ничего не переделаете потом, на самом деле. Когда наш стартап наконец взлетит, и как раз во многом из-за такого отношения он может и не взлететь, в результате. Чем детализированнее и сложнее ваш интерфейс, его дизайн, тем больше строк и времени вы будете экономить просто оптимизируя общие наборы правил, переиспользуя стили. Кроме того, поддерживать код и, тем более, вносить серьезные изменения будет на порядок проще.


Первый пример демонстрирует что на начальных этапах развития проекта хорошо продуманного кода препроцессора может быть даже визуально несколько больше, чем неорганизованного, внутренне плоского, скучного CSS. Но давайте разберем очень часто встречающийся кейс, в котором мы очевидно сразу сильно экономим много трафика и явно оптимизируем возможную поддержку. Такое очень часто встречается: нам нужно создать большое количество, предположим 20 штук модификаторов одного селектора квадратной иконки размеров в 100 пикселей и поставить в них нужные картинки в бекграунд. В Sass мы можем написать цикл с интерполяцией для создания селектора модификатора и указания пути до ресурса. И хотя такая синтаксическая возможность не является чем-то идейно решающе важным на практике она экономит кучу времени и повышает качество жизни на работе:


// В @/src/scss/utils/_variables.scss:// Paths$images__path--root: "../../assets/images/";// Sizes $icons__size: 100px;// Views$icons: 20;// В любом месте проекта (в папке В @/src/scss/project/):.icon {  // корректируем путь до картинок  $image-path: $image_path--root + "icons/";  @include size($icons__size, $icons__size); // эта примесь уже создана выше  @for $i from 1 through $icons {    &.icon--#{$i} {      background: url("#{$image-path}icon--#{$i}.svg") center center no-repeat;    }  }}

Пример предполагает что в вашем проекте следующая структура:


. src    assets      images         icon--1.svg         icon--2.svg         ...    sscs       project         ...       utils          _mixins.scss          _variables.scss

Теперь в шаблонах мы можем использовать:


<div class="icon icon--1"></div>

Если вы желаете чтобы картинки были с осмысленными именами можете перебирать список:


.icon {  $image-path: $image_path--root + "icons/";  $images: "name1", "name2", "name3"; // Список имен  @include size($icons__size, $icons__size);  @each $image in $images {    &.icon--#{$image} {      background: url("#{$image-path}#{$image}.svg") center center no-repeat;    }  }}

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


.selector {  $width: 100px;  width: calc(100vw - #{$width});}

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


Абстрагируй все!


Что такое дизайн, если совсем кратко? Дизайн это гайдлайн строгая система, набор стилевых правил и ограничений, перечень констант, аксиом и отношений в разметке и оформлении интерфейса, которым он неукоснительно должен соответствовать. Задача верстальщика в том чтобы правильно воспринять эту систему и максимально эффективно перевести ее с языка графических прототипов в работающий по заявленным требованиям код.


Поэтому маниакально абстрагируй все что только можно. Все что должно и может быть переиспользовано и любые конкретные значения, те, которые, когда-нибудь, но, в принципе, могут измениться. К примеру, на Stylus:


// В @/src/stylus/utils/variables.styl:$colors = {  mint: #44c6a8,  // ... другие конкретные значения цветов}// Создаем "основной цвет", абстрагируясь от конкретного цвета$colors['primary'] = $colors.mint// ... другие "функциональные" цвета

Любое имеющее глобальное значение и потенциально переиспользуемое качество гайдлайна и дизайна должно быть отражено в файле переменных препроцессора. Теперь в любом месте где потребуется предоставить основной брендовый цвет:


.selector  color $colors.primary

Очевидно, что если весь остальной код будет аккуратно использовать правильную переменную просто по щелчку пальцев возможно изменить этот основной цвет по всему интерфейсу! Все это логично и закономерно приводить нас к идее некой общей стилевой базы, медиатора единого стиля оформления интерфейса. Такую глобальную абстракцию легко способен предоставить препроцессор, и ее, вероятно, будет удобно использовать даже для оформления изолированных компонентов.


Структура и стилевая база препроцессора


Дальше я покажу определенную логичную и простую структуру организации файлов препроцессора, которую использую на проектах сам. Но вы можете что-то делать совсем иначе и, в результате, прийти к совсем другой, удобной именно для ваших методов работы системе. Важно не следовать каким-то зазубренным до автоматизма теориям и якобы лучшим практикам, а гибко и к месту применять возможности, и даже, вероятно все время немного экспериментировать, стараясь расти и меняться к лучшему. Категорически важно только то, что ваши стили должны быть действительно всегда четко и понятно организованы.


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


Но давайте уже организуем препроцессор, если с SCSS:


. src    sscs       core // обшие и компилируемые сущности препроцессора         _animations.scss // keyframes         _base.scss // минимальная нормализация основных HTML-элементов         _grid.scss // сетки         _typography.scss // типографика         _utilities.scss // быстрые удобные классы-утилиты для включения прямо в разметку       libraries // папка с файлами стилизаций сторонних модулей         _modal.scss - например какая-нибудь готовая модаль       project // стили конкретного проекта         _elements.scss // отдельные простые элементы-компоненты         _fixes.scss // этот файл всегда должен быть практически пустой, и предназначен только для редких общеизвестных "собственных проблем браузеров"         _layout.scss - стили общей для всех страниц GUI-обертки над контентом интерфейса         _widgets.scss - сложные составные комбинации простых элементов-компонентов       utils // обшие и некомпилируемые основные сущности препроцессора         _functions.scss // на практике нужны крайне редко         _mixins.scss // параметризируемые и способные принимать контент примеси-микстуры         _placeholders.scss // повторяющиеся наборы правил - растворы         _variables.scss // самый важный файл с переменными )       _main.scss // точка сборки всех стилей препроцессора       _stylebase.scss // стилевая база

То есть, на самом деле порядок сборки всей кухни имеет значение, конечно же:


// В @/src/scss/_stylebase.scss:// Stylebase////////////////////////////////////////////////////////////////////////////////////////////////////////////// Uncompiled kitchen@import "./utils/_functions";@import "./utils/_variables";@import "./utils/_mixins";@import "./utils/_placeholders";// Core base normal style and common utils@import "./core/_animations";@import "./core/_typography";@import "./core/_base";@import "./core/_grid";@import "./core/_utilities";// В @/src/scss/_main.scss:// Project styles////////////////////////////////////////////////////////////////////////////////////////////////////////////// Stylebase for components@import "_stylebase";// App styles@import "./project/_fixes";@import "./project/_elements";@import "./project/_widgets";@import "./project/_layout";/* External libraries customization */@import "./libraries/_modal";

Итак, стилевой базой мы будем называть некое основное ядро стилей, доступный всем остальным компонентам системы общий код препроцессора. Более детально, он состоит из условно двух разных видов файлов:


  1. Растворяемые при компиляции инструменты-помощники, сущности позволяющие генерировать лаконичный, оптимальный, связный код:


    1. функции
    2. переменные
    3. параметризуемые примеси
    4. включения-плейсхолдеры

  2. Компилируемые глобальные стили:


    1. анимации keyframes
    2. типографика
    3. базовая нормализация основных HTML-элементов
    4. сетки
    5. утилитарные классы-помощники для разметки


В папки @/src/scss/project и @/src/scss/libraries вы можете добавлять файлы по необходимости.


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


У меня вот, например, можно посмотреть есть различные такие заготовки-болванки для быстрого старта на разных комбинациях актуальных технологий:



Читать книжку дальше можно пройдя по ссылке: Как верстать веб-интерфейсы быстро, качественно и интересно.

Подробнее..

Как стать Front-End разработчиком

25.07.2020 02:14:21 | Автор: admin
image

Кто такой Front-End разработчик?


Front-End разработчик это человек который пишет код для внешнего вида сайта, также есть Back-End разработчик который пишет код для функциональной части сайта. Если скрестить эти две профессии получится Full-Stack разработчик

1. Азы которые нужно знать


Для написания Front-End кода можно использовать множество языков, но для начала я советую вам начать изучения с языка HTML. С помощью его вы сможете делать конечно не сайты, но стандартные интернет страницы с текстом, картинками и т.д, но HTML-я вам конечно же не хватит что бы считать себя Front-End разработчиком. Способов изучить HTML есть много их мы разберём немного позже

image

Далее я вам посоветую выучить азы языка CSS. Он вам пригодиться что бы уже делать неплохой дизайн сайта, стандартные анимации, оформление страницы. Также способов изучить CSS есть много их мы разберём немного позже

Что бы хорошо писать дизайн для сайта вам не всегда хватит языка CSS, поэтому вам нужны азы JavaScript-а. Но если-же вы полностью выучите язык JavaScript и Node.js вы вполне сможете писать даже Back-End и стать Full-Stack разработчиком

image

2. Время


Что бы выучить эти языки программирования вам понадобиться не много времени, но это не значит что это так легко. Всегда нужно учиться не только на теории, а также и на практике. Что бы полностью ориентироваться в языке программирования нужно много практики, иначе вы будете очень путаться если раньше времени возьмётесь за работу на фрилансе. Попробуйте написать парочку страниц для себя что бы хорошо понимать и разбираться в языках

image

3. Математика


Долго мы на этом заостряться не будем, если ты хорошо знаешь математику-отлично, если не очень это не беда. Тут вполне хватит курса математики до 9 класса

4. Обучающие ресурсы


Для азов HTML и CSS посоветую эти видео
HTML: www.youtube.com/watch?v=5pBcKKiZSGE
CSS: www.youtube.com/watch?v=iPV5GKeHyV4
После можете читать и узнавать аспекты HTML и CSS на htmlbook.ru

Для азов JavaScript советую это видео
www.youtube.com/watch?v=Bluxbh9CaQ0&t=5328s
Потом JavaScript во всех аспектах можно доучить на learn.javascript.ru
Подробнее..

Перевод Почему стоит использовать тег ltpicturegt вместо ltimggt

05.05.2021 10:13:29 | Автор: admin
image

Использование изображений и анимаций в интерфейсах пользователя стало распространённой практикой в современных веб-приложениях. Хотя эти примеры современного дизайна делают упор на повышение удобства работы с приложениями, если изображения не адаптируются ко всем устройствам, то могут возникать проблемы.

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

Выбор между тегами picture и img может показаться мелким решением, но сделав правильный выбор, вы сможете повысить и удобство для пользователя, и производительность.

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

Когда в следующий раз вы будете создавать компонент изображения на React, то воспользуйтесь полученными в этой статье знаниями. Возвращайте подходящий тег в соответствии с полученными свойствами и обрабатывайте все необходимые альтернативы. Благодаря этому вы получите супероптимизированнй компонент изображения, который можно использовать во всех веб-проектах.

Почему тега img недостаточно для современных веб-приложений?


Как все мы знаем, тег Img в течение длительного периода времени был одним из фундаментальных элементов HTML, и никто не сомневался в его простоте и применимости.

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

Все эти вопросы можно сгруппировать в две большие категории:

  1. Смена разрешения проблема передачи изображений меньшего размера для устройств с маленькими экранами.
  2. Ориентация графики проблема отображения различных изображений при разных размерах экрана.

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

Смена разрешения при помощи атрибутов srcset и sizes


Как говорилось выше, современные веб-дизайнеры для привлечения пользователей часто используют изображения высокого разрешения. Но мы, разработчики, должны внимательно выбирать подходящий HTML-элемент.

Допустим, вы используете простой тег Img для изображений высокого разрешения. В таком случае при запуске приложения одно и то же изображение будет использоваться на каждом устройстве, что приведёт к проблемам с производительностью в устройствах с низким разрешением экрана, например, в мобильных телефонах.

Это может привести к более долгой загрузке изображений и частичной загрузке изображений сверху вниз.


Проблема загрузки изображения сверху вниз

Эту проблему можно легко решить тегом picture при помощи атрибутов srcset и sizes.

<picture>   <source      srcset="small-car-image.jpg 400w,              medium-car-image.jpg 800w,              large-car-image.jpg 1200w"      sizes="(min-width: 1280px) 1200px,             (min-width: 768px) 400px,             100vw">   <img src="medium-car-image.jpg" alt="Car"></picture>

Атрибут srcset может принимать несколько изображений с соответствующей шириной в пикселях, а браузер использует эти значения для выбора между указанными изображениями. В показанном выше примере есть три версии одного и того же изображения в трёх разных размерах.

Атрибут sizes задаёт пространство, которое изображение будет занимать на экране. В показанном выше примере изображение займёт до 1200px, если минимальная ширина экрана равна 1280px.

Тем не менее, рекомендуется не использовать тег Picture только для смены разрешения, потому что ту же задачу можно решить с помощью новой версии тега Img (имеющей поддержку большего количества браузеров).

<img srcset="small-car-image.jpg 400w,             medium-car-image.jpg 800w,             large-car-image.jpg 1200w"     sizes="(min-width: 1280px) 1200px,            (min-width: 768px) 400px,            100vw"          src="medium-car-image.jpg" alt="Car">

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

Поэтому давайте посмотрим, как можно решить проблему ориентации графики с помощью тега picture.

Ориентация графики при помощи атрибута media


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

Мы можем решить эту проблему, создав различные версии изображения для разных размеров экрана. Эти разные версии могут быть альбомными, портретными или любыми другими изменёнными версиями того же изображения.

С помощью тега picture мы можем легко обеспечить смену разрешения, воспользовавшись несколькими тегами source внутри тега picture.

<picture>      <source ....>   <source ....>   <source ....></picture>

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

В показанном ниже примере демонстрируется полная реализация ориентации графики и смены разрешения при помощи тега picture.

<picture>        <source media="(orientation: landscape)"                   srcset="land-small-car-image.jpg 200w,              land-medium-car-image.jpg 600w,              land-large-car-image.jpg 1000w"                   sizes="(min-width: 700px) 500px,             (min-width: 600px) 400px,             100vw">        <source media="(orientation: portrait)"                   srcset="port-small-car-image.jpg 700w,              port-medium-car-image.jpg 1200w,              port-large-car-image.jpg 1600w"                   sizes="(min-width: 768px) 700px,             (min-width: 1024px) 600px,             500px">        <img src="land-medium-car-image.jpg" alt="Car"></picture>

Если экран находится в альбомной ориентации, то браузер будет отображать изображения из первого набора, а если в портретной, то из второго набора. Кроме того, можно использовать атрибут media с параметрами max-width и min-width:

<picture>     <source media="(max-width: 767px)" ....>     <source media="(min-width: 768px)" ....></picture>

Последний тег img используется для обратной совместимости с браузерами, не поддерживающими теги picture.

Использование с частично поддерживаемыми типами изображений


Благодаря быстрому развитию технологий ежедневно появляется множество современных типов изображений. Некоторые из них, например webp, svg и avif, обеспечивают большее удобство для пользователей.

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

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

<picture>  <source srcset="test.avif" type="image/avif">  <source srcset="test.webp" type="image/webp">  <img src="test.png" alt="test image"></picture>

В показанный выше пример включены три типа изображений в форматах avif, webp и png. Сначала браузер попробует формат avif, если не получится, то попробует webp. Если браузер не поддерживает ни один из них, то использует изображение png.

Ситуация с тегом picture стала ещё интереснее, когда разработчики Chrome объявили о том, что во вкладке Rendering инструментов DevTools появится две новые эмуляции для эмулирования частично поддерживаемых типов изображений.

Начиная с Chrome 88 и далее можно использовать Chrome DevTools для проверки совместимости браузера с типами изображений.


Использование Chrome DevTools для эмулирования совместимости изображений

В заключение


Хоть мы и говорили о том, насколько лучше тег picture по сравнению с тегом img, я уверен, что img не умер и умрёт ещё не скоро.

Если мы будем с умом использовать имеющиеся у него атрибуты srcset и size, то можем выжать из тега img максимум. Например, можно решить проблему смены разрешения при помощи одного только тега img.

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

Среди прочих достоинств тега picture способность работать с частично поддерживаемыми типами изображений и поддержка Chrome DevTools.

Однако оба эти элемента имеют свои плюсы и минусы. Поэтому нам нужно обдумывать и выбирать наиболее подходящий к нашим требованиям элемент.



На правах рекламы


Эпичные серверы это VDS для размещения сайтов от маленького интернет-магазина на Opencart до серьёзных проектов с огромной аудиторией. Создавайте собственные конфигурации серверов в пару кликов!

Подписывайтесь на наш чат в Telegram.

Подробнее..

Модульные front-end блоки пишем свой мини фреймворк

09.05.2021 16:23:04 | Автор: admin

Доброго времени суток уважаемые читатели хабра. С каждым годом в веб разработке появляется все больше разнообразных решений которые используют модульный подход и упрощают разработку и редактирование кода. В данной статье я предлагаю вам свой взгляд на то, какими могут быть переиспользуемые front-end блоки (для проектов с бэкендом на php) и предлагаю пройти все шаги от идеи до реализации вместе со мной. Звучит интересно? Тогда добро пожаловать под кат.

Предисловие

Представлюсь - я молодой веб разработчик с опытом работы 5 лет. Крайний год я работаю на фрилансе и большая часть текущих проектов связана с WordPress. Несмотря на различую критику CMS в общем и WordPress в часности, я считаю сама архитектура WordPress это довольно удачное решение, хотя конечно не без определенных недостатков. И один из них на мой взгляд это шаблоны. В крайних обновлениях сделаны большие шаги чтобы это исправить, и Gutenberg в целом становится мощным инструментом, однако к сожалению в большинстве тем продолжается каша в шаблонах, стилях и скриптах, которая делает редактирование чего-либо крайне болезненным, а переиспользование кода зачастую невозможным. Именно эта проблема и подтолкнуло меня к идее своего мини фреймворка (читай пакета, но поскольку он будет предъявлять требования к структуре, то гордо назовем мини фреймворком), который бы организовывал структуру и позволял переиспользовать блоки.

Реализация будет в виде composer пакета, который можно будет использовать в совершенно различных проектах, без привязки к WordPress.

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

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

Постановка задачи

Понятие блок ниже будет по сути тем же понятием что и блок в BEM методологии, т.е. это будет группа html/js/css кода которая будет представлять одну сущность.

Генерировать html и управлять зависимостями блоков мы будем через php, что говорит о том, что наш пакет будет подходить для проектов с бекендом на php. Также условимся на берегу что, не вдаваясь в споры, не будем поддаваться влиянию новомодных вещей, таких как css-in-js или bem-json и будем придерживаться эль-классико классического подхода, т.е. предполагать что html, css и js это разные файлы.

Теперь давайте сформулируем наши основные требования к будущему мини-фреймворку:

  • Обеспечить структуру блоков

  • Предоставить поддержку наследования (расширения) блоков

  • Предоставить возможность использовать блок в блоке и соответственно поддержку зависимости ресурсов одного блока от ресурсов других блоков

Структура мини фреймворка

Как условились выше, такие ресурсы как css и js всегда будут в виде обычных файлов, т.е. это будут .js и .css или .min.css и .min.js в случае использования препроцесссоров и сборщиков (как webpack например). Для вставки данных в html код мы будем использовать шаблонизатор Twig (для тех кто не знаком ссылка). Кто-то может заметить, что Php и сам по себе хороший шаблонизатор, не будем вдаваться в споры, кроме доводов указанных на главной странице проекта Twig, отмечу важный для меня пункт, то что он дисциплинирует, т.е. заставляет отделять обработку от вывода и подготавливать переменные заранее, и в данном случае мы будем использовать его.

Теперь давайте продумаем структуру нашего мини фреймворка более детально.

  1. Блок

    Каждый блок будет состоять из:

    1. Статических ресурсов (css/js/twig)

    2. Класса модели (его поля мы будет предоставлять как данные для twig шаблона)

    3. Класса контролера (он будет отвечать за наши ресурсы, их зависимости друг от друга и связывать модель с twig шаблоном)

  2. Вспомогательные классы : Класс Settings (будет содержать путь к блокам, их пространство имен и т.д.), класс обертка для Twig пакета

  3. Blocks класс

    Связующий класс, который :

    1. будет содержать вспомогательные классы (Settings, Twig)

    2. предоставлять функцию рендера блока

    3. содержать список использованных блоков, чтобы иметь возможность получить их ресурсы (css/js)

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

Требования к блокам

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

  • php 7.4+

  • Все блоки должны иметь одну родительскую директорию

  • Классы моделей и контроллеров должны иметь PSR-4 совместимое пространство имен с автозагрузчиком (PSR-4 де факто стандарт, если вы используете автозагрузчик от composer, т.е. указываете autoload/psr4 директиву в вашем composer.json то ваш проект уже соответствует этому требованию)

  • Соглашение об именах:

    • Имя контроллера должно содержать _C суффикс

    • Класс модели должен иметь то же пространство имен и то же имя (без суффикса) что и соответствующих контроллер

    • Имена ресурсов должны соответствовать имени контроллера, но с данными отличиями:

      • Без суффикса контроллера

      • Верблюжья нотация в имени должны быть заменена на тире (CamelCase = camel-case)

      • Нижнее подчеркивание в имени должно быть заменено на тире (just_block = just-block)

      • Таким образом по правилам выше имя ресурса с контроллером Block_Theme_Main_C будет blocktheme--main

Реализация

Пришло время перейти к реализации нашей идеи, т.е. к коду.

Ниже части реализации (классы) будут в формате : текстовое описание, код реализации и код тестов. Согласен с людьми, которые говорят что тесты есть лучшая документация, однако к своему стыду я начал использовать их в своих проектах недавно, по-этому не смотря на мои старания их имена или структура могут повергнуть в шок сбивать с толку, просьба не принимать близко к сердцу.

FIELDS_READER

Все наша магия при работе с моделями и контроллерами будет строится на функции get_class_vars которая предоставит нам имена полей класса и на ReflectionProperty классе, который предоставит нам информацию об этих полях, такую как видимость поля (protected/public) и его тип. Мы будем собирать информацию только о protected полях.

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

FIELDS_READER.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework;use Exception;use ReflectionProperty;abstract class FIELDS_READER {private array $_fieldsInfo;public function __construct() {$this->_fieldsInfo = [];$this->_readFieldsInfo();$this->_autoInitFields();}final protected function _getFieldsInfo(): array {return $this->_fieldsInfo;}protected function _getFieldType( string $fieldName ): ?string {$fieldType = null;try {// used static for child support$property = new ReflectionProperty( static::class, $fieldName );} catch ( Exception $ex ) {return $fieldType;}if ( ! $property->isProtected() ) {return $fieldType;}return $property->getType() ?$property->getType()->getName() :'';}private function _readFieldsInfo(): void {// get protected fields without the '__' prefix$fieldNames = array_keys( get_class_vars( static::class ) );$fieldNames = array_filter( $fieldNames, function ( $fieldName ) {$prefix = substr( $fieldName, 0, 2 );return '__' !== $prefix;} );foreach ( $fieldNames as $fieldName ) {$fieldType = $this->_getFieldType( $fieldName );// only protected fieldsif ( is_null( $fieldType ) ) {continue;}$this->_fieldsInfo[ $fieldName ] = $fieldType;}}private function _autoInitFields(): void {foreach ( $this->_fieldsInfo as $fieldName => $fieldType ) {// ignore fields without a typeif ( ! $fieldType ) {continue;}$defaultValue = null;switch ( $fieldType ) {case 'int':case 'float':$defaultValue = 0;break;case 'bool':$defaultValue = false;break;case 'string':$defaultValue = '';break;case 'array':$defaultValue = [];break;}try {if ( is_subclass_of( $fieldType, MODEL::class ) ||     is_subclass_of( $fieldType, CONTROLLER::class ) ) {$defaultValue = new $fieldType();}} catch ( Exception $ex ) {$defaultValue = null;}// ignore fields with a custom type (null by default)if ( is_null( $defaultValue ) ) {continue;}$this->{$fieldName} = $defaultValue;}}}
FIELDS_READERTest.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocksFramework\CONTROLLER;use LightSource\FrontBlocksFramework\FIELDS_READER;use LightSource\FrontBlocksFramework\MODEL;class FIELDS_READERTest extends Unit {public function testReadProtectedField() {$fieldsReader = new class extends FIELDS_READER {protected $_loadedField;public function __construct() {parent::__construct();}public function getFields() {return $this->_getFieldsInfo();}};$this->assertEquals( ['_loadedField' => '',], $fieldsReader->getFields() );}public function testIgnoreReadProtectedPrefixedField() {$fieldsReader = new class extends FIELDS_READER {protected $__unloadedField;public function __construct() {parent::__construct();}public function getFields() {return $this->_getFieldsInfo();}};$this->assertEquals( [], $fieldsReader->getFields() );}public function testIgnoreReadPublicField() {$fieldsReader = new class extends FIELDS_READER {public $unloadedField;public function __construct() {parent::__construct();}public function getFields() {return $this->_getFieldsInfo();}};$this->assertEquals( [], $fieldsReader->getFields() );}public function testIgnoreReadPrivateField() {$fieldsReader = new class extends FIELDS_READER {private $unloadedField;public function __construct() {parent::__construct();}public function getFields() {return $this->_getFieldsInfo();}};$this->assertEquals( [], $fieldsReader->getFields() );}public function testReadFieldWithType() {$fieldsReader = new class extends FIELDS_READER {protected string $_loadedField;public function __construct() {parent::__construct();}public function getFields() {return $this->_getFieldsInfo();}};$this->assertEquals( ['_loadedField' => 'string',], $fieldsReader->getFields() );}public function testReadFieldWithoutType() {$fieldsReader = new class extends FIELDS_READER {protected $_loadedField;public function __construct() {parent::__construct();}public function getFields() {return $this->_getFieldsInfo();}};$this->assertEquals( ['_loadedField' => '',], $fieldsReader->getFields() );}////public function testAutoInitIntField() {$fieldsReader = new class extends FIELDS_READER {protected int $_int;public function __construct() {parent::__construct();}public function getInt() {return $this->_int;}};$this->assertTrue( 0 === $fieldsReader->getInt() );}public function testAutoInitFloatField() {$fieldsReader = new class extends FIELDS_READER {protected float $_float;public function __construct() {parent::__construct();}public function getFloat() {return $this->_float;}};$this->assertTrue( 0.0 === $fieldsReader->getFloat() );}public function testAutoInitStringField() {$fieldsReader = new class extends FIELDS_READER {protected string $_string;public function __construct() {parent::__construct();}public function getString() {return $this->_string;}};$this->assertTrue( '' === $fieldsReader->getString() );}public function testAutoInitBoolField() {$fieldsReader = new class extends FIELDS_READER {protected bool $_bool;public function __construct() {parent::__construct();}public function getBool() {return $this->_bool;}};$this->assertTrue( false === $fieldsReader->getBool() );}public function testAutoInitArrayField() {$fieldsReader = new class extends FIELDS_READER {protected array $_array;public function __construct() {parent::__construct();}public function getArray() {return $this->_array;}};$this->assertTrue( [] === $fieldsReader->getArray() );}public function testAutoInitModelField() {$testModel        = new class extends MODEL {};$testModelClass   = get_class( $testModel );$fieldsReader     = new class ( $testModelClass ) extends FIELDS_READER {protected $_model;private $_testClass;public function __construct( $testClass ) {$this->_testClass = $testClass;parent::__construct();}public function _getFieldType( string $fieldName ): ?string {return ( '_model' === $fieldName ?$this->_testClass :parent::_getFieldType( $fieldName ) );}public function getModel() {return $this->_model;}};$actualModelClass = $fieldsReader->getModel() ?get_class( $fieldsReader->getModel() ) :'';$this->assertEquals( $actualModelClass, $testModelClass );}public function testAutoInitControllerField() {$testController      = new class extends CONTROLLER {};$testControllerClass = get_class( $testController );$fieldsReader        = new class ( $testControllerClass ) extends FIELDS_READER {protected $_controller;private $_testClass;public function __construct( $testControllerClass ) {$this->_testClass = $testControllerClass;parent::__construct();}public function _getFieldType( string $fieldName ): ?string {return ( '_controller' === $fieldName ?$this->_testClass :parent::_getFieldType( $fieldName ) );}public function getController() {return $this->_controller;}};$actualModelClass    = $fieldsReader->getController() ?get_class( $fieldsReader->getController() ) :'';$this->assertEquals( $actualModelClass, $testControllerClass );}public function testIgnoreInitFieldWithoutType() {$fieldsReader = new class extends FIELDS_READER {protected $_default;public function __construct() {parent::__construct();}public function getDefault() {return $this->_default;}};$this->assertTrue( null === $fieldsReader->getDefault() );}}

MODEL

Данный класс по сути лишь небольшая обертка для класса FIELDS_READER, который содержит поле _isLoaded, что отвечает за состояние модели, оно пригодится нам когда мы будем работать с twig, и функции getFields, которая возвращает массив со значениями protected полей, в котором ключи это их имена.

MODEL.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework;abstract class MODEL extends FIELDS_READER {private bool $_isLoaded;public function __construct() {parent::__construct();$this->_isLoaded = false;}final public function isLoaded(): bool {return $this->_isLoaded;}public function getFields(): array {$args = [];$fieldsInfo = $this->_getFieldsInfo();foreach ( $fieldsInfo as $fieldName => $fieldType ) {$args[ $fieldName ] = $this->{$fieldName};}return $args;}final protected function _load(): void {$this->_isLoaded = true;}}
MODELTest.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocksFramework\MODEL;class MODELTest extends Unit {public function testGetFields() {$model = new class extends MODEL {protected string $_field1;public function __construct() {parent::__construct();}public function update() {$this->_field1 = 'just string';}};$model->update();$this->assertEquals( ['_field1'   => 'just string',], $model->getFields() );}}

CONTROLLER

Данный класс также как и MODEL наследует класс FIELDS_READER, однако имеет и другие важные задачи. Содержит два поля модель и массив __external, который пригодится нам далее при работе с twig шаблоном.

Статический метод GetResourceInfo позволяет получить информацию о статических ресурсах данного блока (twig,css,js) , такую как имя ресурса или относительный путь к нему (мы можем получить это из имени и пространства имен контроллера благодаря соблюдению требований выше).

Метод getTemplateArgs будет возвращать данные для twig шаблона, это все protected поля соответствующей модели (без префикса _ если есть) и два дополнительных поля, _template и _isLoaded, первое будет содержать путь к шаблону, а второе отображать состояние модели. Также в этом методе мы реализуем возможность использовать блок в блоке (т.е. иметь класс Model в другом классе Model как поле) - мы соединяем поля контроллера и поля соответствующей модели по имени : т.е. если каждому полю с типом контроллер мы находим соответствующее поле в модели (с типом модель), то мы инициализируем поле контроллер моделью и вызываем метод getTemplateArgs у этого контроллера, получая таким образом все необходимую информацию для отображения этого вложенного блока.

Метод getDependencies на основании наших полей с типом контроллер рекурсивно (с обходом всех подзависимостей) возвращает нам уникальный (т.е. без повторений) список используемых классов-контроллеров, что помогает нам реализовать возможность зависимости ресурсов одного блока от другого.

Также отмечу отдельный момент, автоматическую инициализацию поля модели в конструкторе контроллера, т.е. если существует класс с таким же именем как у контроллера, но без суффикса (смотри требования выше), то это поле будет инициализировано объектом этого класса. Это позволит избежать лишнего кода в дальнейшем (т.е. создания модели отдельно от контроллера) и необходимо для расширения (это выходит за рамки нашей статьи).

CONTROLLER.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework;use Exception;abstract class CONTROLLER extends FIELDS_READER {const TEMPLATE_KEY__TEMPLATE = '_template';const TEMPLATE_KEY__IS_LOADED = '_isLoaded';private ?MODEL $_model;// using the prefix to prevent load this fieldprotected array $__external;public function __construct( ?MODEL $model = null ) {parent::__construct();$this->_model     = $model;$this->__external = [];$this->_autoInitModel();}final public static function GetResourceInfo( Settings $settings, string $controllerClass = '' ): array {// using static for children support$controllerClass = ! $controllerClass ?static::class :$controllerClass;// e.g. $controllerClass = Example/Theme/Main/Example_Theme_Main_C$resourceInfo = ['resourceName'         => '',// e.g. example--theme--main'relativePath'         => '',// e.g. Example/Theme/Main'relativeResourcePath' => '', // e.g. Example/Theme/Main/example--theme--main];$controllerSuffix = Settings::$ControllerSuffix;//  e.g. Example/Theme/Main/Example_Theme_Main$relativeControllerNamespace = $settings->getBlocksDirNamespace() ?str_replace( $settings->getBlocksDirNamespace() . '\\', '', $controllerClass ) :$controllerClass;$relativeControllerNamespace = substr( $relativeControllerNamespace, 0, mb_strlen( $relativeControllerNamespace ) - mb_strlen( $controllerSuffix ) );// e.g. Example_Theme_Main$phpBlockName = explode( '\\', $relativeControllerNamespace );$phpBlockName = $phpBlockName[ count( $phpBlockName ) - 1 ];// e.g. example--theme--main (from Example_Theme_Main)$blockNameParts    = preg_split( '/(?=[A-Z])/', $phpBlockName, - 1, PREG_SPLIT_NO_EMPTY );$blockResourceName = [];foreach ( $blockNameParts as $blockNamePart ) {$blockResourceName[] = strtolower( $blockNamePart );}$blockResourceName = implode( '-', $blockResourceName );$blockResourceName = str_replace( '_', '-', $blockResourceName );// e.g. Example/Theme/Main$relativePath = explode( '\\', $relativeControllerNamespace );$relativePath = array_slice( $relativePath, 0, count( $relativePath ) - 1 );$relativePath = implode( DIRECTORY_SEPARATOR, $relativePath );$resourceInfo['resourceName']         = $blockResourceName;$resourceInfo['relativePath']         = $relativePath;$resourceInfo['relativeResourcePath'] = $relativePath . DIRECTORY_SEPARATOR . $blockResourceName;return $resourceInfo;}// can be overridden if Controller doesn't have own twig (uses parents)public static function GetPathToTwigTemplate( Settings $settings, string $controllerClass = '' ): string {return self::GetResourceInfo( $settings, $controllerClass )['relativeResourcePath'] . $settings->getTwigExtension();}// can be overridden if Controller doesn't have own model (uses parents)public static function GetModelClass(): string {$controllerClass = static::class;$modelClass      = rtrim( $controllerClass, Settings::$ControllerSuffix );return ( $modelClass !== $controllerClass &&         class_exists( $modelClass, true ) &&         is_subclass_of( $modelClass, MODEL::class ) ?$modelClass :'' );}public static function OnLoad() {}final public function setModel( MODEL $model ): void {$this->_model = $model;}private function _getControllerField( string $fieldName ): ?CONTROLLER {$controller = null;$fieldsInfo = $this->_getFieldsInfo();if ( key_exists( $fieldName, $fieldsInfo ) ) {$controller = $this->{$fieldName};// prevent possible recursion by a mistake (if someone will create a field with self)// using static for children support$controller = ( $controller &&                $controller instanceof CONTROLLER ||                get_class( $controller ) !== static::class ) ?$controller :null;}return $controller;}public function getTemplateArgs( Settings $settings ): array {$modelFields  = $this->_model ?$this->_model->getFields() :[];$templateArgs = [];foreach ( $modelFields as $modelFieldName => $modelFieldValue ) {$templateFieldName = ltrim( $modelFieldName, '_' );if ( ! $modelFieldValue instanceof MODEL ) {$templateArgs[ $templateFieldName ] = $modelFieldValue;continue;}$modelFieldController = $this->_getControllerField( $modelFieldName );$modelFieldArgs       = [];$externalFieldArgs    = $this->__external[ $modelFieldName ] ?? [];if ( $modelFieldController ) {$modelFieldController->setModel( $modelFieldValue );$modelFieldArgs = $modelFieldController->getTemplateArgs( $settings );}$templateArgs[ $templateFieldName ] = HELPER::ArrayMergeRecursive( $modelFieldArgs, $externalFieldArgs );}// using static for children supportreturn array_merge( $templateArgs, [self::TEMPLATE_KEY__TEMPLATE  => static::GetPathToTwigTemplate( $settings ),self::TEMPLATE_KEY__IS_LOADED => ( $this->_model && $this->_model->isLoaded() ),] );}public function getDependencies( string $sourceClass = '' ): array {$dependencyClasses = [];$controllerFields  = $this->_getFieldsInfo();foreach ( $controllerFields as $fieldName => $fieldType ) {$dependencyController = $this->_getControllerField( $fieldName );if ( ! $dependencyController ) {continue;}$dependencyClass = get_class( $dependencyController );// 1. prevent the possible permanent recursion// 2. add only unique elements, because several fields can have the same typeif ( ( $sourceClass && $dependencyClass === $sourceClass ) ||     in_array( $dependencyClass, $dependencyClasses, true ) ) {continue;}// used static for child support$subDependencies = $dependencyController->getDependencies( static::class );// only unique elements$subDependencies = array_diff( $subDependencies, $dependencyClasses );// sub dependencies are before the main dependency$dependencyClasses = array_merge( $dependencyClasses, $subDependencies, [ $dependencyClass, ] );}return $dependencyClasses;}// Can be overridden for declare a target model class and provide an IDE supportpublic function getModel(): ?MODEL {return $this->_model;}private function _autoInitModel() {if ( $this->_model ) {return;}$modelClass = static::GetModelClass();try {$this->_model = $modelClass ?new $modelClass() :$this->_model;} catch ( Exception $ex ) {$this->_model = null;}}}
CONTROLLERTest.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocksFramework\{CONTROLLER,MODEL,Settings};class CONTROLLERTest extends Unit {private function _getModel( array $fields, bool $isLoaded = false ): MODEL {return new class ( $fields, $isLoaded ) extends MODEL {private array $_fields;public function __construct( array $fields, bool $isLoaded ) {parent::__construct();$this->_fields = $fields;if ( $isLoaded ) {$this->_load();}}public function getFields(): array {return $this->_fields;}};}private function _getController( ?MODEL $model ): CONTROLLER {return new class ( $model ) extends CONTROLLER {public function __construct( ?MODEL $model = null ) {parent::__construct( $model );}};}private function _getTemplateArgsWithoutAdditional( array $templateArgs ) {$templateArgs = array_diff_key( $templateArgs, [CONTROLLER::TEMPLATE_KEY__TEMPLATE  => '',CONTROLLER::TEMPLATE_KEY__IS_LOADED => '',] );foreach ( $templateArgs as $templateKey => $templateValue ) {if ( ! is_array( $templateValue ) ) {continue;}$templateArgs[ $templateKey ] = $this->_getTemplateArgsWithoutAdditional( $templateValue );}return $templateArgs;}////public function testGetResourceInfoWithoutCamelCaseInBlockName() {$settings = new Settings();$settings->setControllerSuffix( '_C' );$settings->setBlocksDirNamespace( 'Namespace' );$this->assertEquals( ['resourceName'         => 'block','relativePath'         => 'Block','relativeResourcePath' => 'Block/block',], CONTROLLER::GetResourceInfo( $settings, 'Namespace\\Block\\Block_C' ) );}public function testGetResourceInfoWithCamelCaseInBlockName() {$settings = new Settings();$settings->setControllerSuffix( '_C' );$settings->setBlocksDirNamespace( 'Namespace' );$this->assertEquals( ['resourceName'         => 'block-name','relativePath'         => 'BlockName','relativeResourcePath' => 'BlockName/block-name',], CONTROLLER::GetResourceInfo( $settings, 'Namespace\\BlockName\\BlockName_C' ) );}public function testGetResourceInfoWithoutCamelCaseInTheme() {$settings = new Settings();$settings->setControllerSuffix( '_C' );$settings->setBlocksDirNamespace( 'Namespace' );$this->assertEquals( ['resourceName'         => 'block--theme--main','relativePath'         => 'Block/Theme/Main','relativeResourcePath' => 'Block/Theme/Main/block--theme--main',], CONTROLLER::GetResourceInfo( $settings, 'Namespace\\Block\\Theme\\Main\\Block_Theme_Main_C' ) );}public function testGetResourceInfoWithCamelCaseInTheme() {$settings = new Settings();$settings->setControllerSuffix( '_C' );$settings->setBlocksDirNamespace( 'Namespace' );$this->assertEquals( ['resourceName'         => 'block--theme--just-main','relativePath'         => 'Block/Theme/JustMain','relativeResourcePath' => 'Block/Theme/JustMain/block--theme--just-main',], CONTROLLER::GetResourceInfo( $settings, 'Namespace\\Block\\Theme\\JustMain\\Block_Theme_JustMain_C' ) );}////public function testGetTemplateArgsWhenModelContainsBuiltInTypes() {$settings   = new Settings();$model      = $this->_getModel( ['stringVariable' => 'just string',] );$controller = $this->_getController( $model );$this->assertEquals( ['stringVariable' => 'just string',], $this->_getTemplateArgsWithoutAdditional( $controller->getTemplateArgs( $settings ) ) );}public function testGetTemplateArgsWhenModelContainsAnotherModel() {$settings = new Settings();$modelA              = $this->_getModel( ['_modelA' => 'just string from model a',] );$modelB              = $this->_getModel( ['_modelA' => $modelA,'_modelB' => 'just string from model b',] );$controllerForModelA = $this->_getController( null );$controllerForModelB = new class ( $modelB, $controllerForModelA ) extends CONTROLLER {protected $_modelA;public function __construct( ?MODEL $model = null, $controllerForModelA ) {parent::__construct( $model );$this->_modelA = $controllerForModelA;}};$this->assertEquals( ['modelA' => ['modelA' => 'just string from model a',],'modelB' => 'just string from model b',], $this->_getTemplateArgsWithoutAdditional( $controllerForModelB->getTemplateArgs( $settings ) ) );}public function testGetTemplateArgsWhenControllerContainsExternalArgs() {$settings = new Settings();$modelA              = $this->_getModel( ['_additionalField' => '','_modelA'          => 'just string from model a',] );$modelB              = $this->_getModel( ['_modelA' => $modelA,'_modelB' => 'just string from model b',] );$controllerForModelA = $this->_getController( null );$controllerForModelB = new class ( $modelB, $controllerForModelA ) extends CONTROLLER {protected $_modelA;public function __construct( ?MODEL $model = null, $controllerForModelA ) {parent::__construct( $model );$this->_modelA               = $controllerForModelA;$this->__external['_modelA'] = ['additionalField' => 'additionalValue',];}};$this->assertEquals( ['modelA' => ['additionalField' => 'additionalValue','modelA'          => 'just string from model a',],'modelB' => 'just string from model b',], $this->_getTemplateArgsWithoutAdditional( $controllerForModelB->getTemplateArgs( $settings ) ) );}public function testGetTemplateArgsContainsAdditionalFields() {$settings   = new Settings();$model      = $this->_getModel( [] );$controller = $this->_getController( $model );$this->assertEquals( [CONTROLLER::TEMPLATE_KEY__TEMPLATE,CONTROLLER::TEMPLATE_KEY__IS_LOADED,], array_keys( $controller->getTemplateArgs( $settings ) ) );}public function testGetTemplateArgsWhenAdditionalIsLoadedIsFalse() {$settings   = new Settings();$model      = $this->_getModel( [] );$controller = $this->_getController( $model );$actual = array_intersect_key( $controller->getTemplateArgs( $settings ), [ CONTROLLER::TEMPLATE_KEY__IS_LOADED => '', ] );$this->assertEquals( [ CONTROLLER::TEMPLATE_KEY__IS_LOADED => false, ], $actual );}public function testGetTemplateArgsWhenAdditionalIsLoadedIsTrue() {$settings   = new Settings();$model      = $this->_getModel( [], true );$controller = $this->_getController( $model );$actual = array_intersect_key( $controller->getTemplateArgs( $settings ), [ CONTROLLER::TEMPLATE_KEY__IS_LOADED => '', ] );$this->assertEquals( [ CONTROLLER::TEMPLATE_KEY__IS_LOADED => true, ], $actual );}public function testGetTemplateArgsAdditionalTemplateIsRight() {$settings   = new Settings();$model      = $this->_getModel( [] );$controller = $this->_getController( $model );$actual = array_intersect_key( $controller->getTemplateArgs( $settings ), [ CONTROLLER::TEMPLATE_KEY__TEMPLATE => '', ] );$this->assertEquals( [CONTROLLER::TEMPLATE_KEY__TEMPLATE => $controller::GetPathToTwigTemplate( $settings ),], $actual );}////public function testGetDependencies() {$controllerA = $this->_getController( null );$controllerB = new class ( null, $controllerA ) extends CONTROLLER {protected $_controllerA;public function __construct( ?MODEL $model = null, $controllerA ) {parent::__construct( $model );$this->_controllerA = $controllerA;}};$this->assertEquals( [get_class( $controllerA ),], $controllerB->getDependencies() );}public function testGetDependenciesWithSubDependencies() {$controllerA = new class extends CONTROLLER {public function getDependencies( string $sourceClass = '' ): array {return ['A',];}};$controllerB = new class ( null, $controllerA ) extends CONTROLLER {protected $_controllerA;public function __construct( ?MODEL $model = null, $controllerA ) {parent::__construct( $model );$this->_controllerA = $controllerA;}};$this->assertEquals( ['A',get_class( $controllerA ),], $controllerB->getDependencies() );}public function testGetDependenciesWithSubDependenciesRecursively() {$controllerA = new class extends CONTROLLER {public function getDependencies( string $sourceClass = '' ): array {return ['A',];}};$controllerB = new class ( null, $controllerA ) extends CONTROLLER {protected $_controllerA;public function __construct( ?MODEL $model = null, $controllerA ) {parent::__construct( $model );$this->_controllerA = $controllerA;}};$controllerC = new class ( null, $controllerB ) extends CONTROLLER {protected $_controllerB;public function __construct( ?MODEL $model = null, $controllerB ) {parent::__construct( $model );$this->_controllerB = $controllerB;}};$this->assertEquals( ['A',get_class( $controllerA ),get_class( $controllerB ),], $controllerC->getDependencies() );}public function testGetDependenciesWithSubDependenciesInOrderWhenSubBeforeMainDependency() {$controllerA = new class extends CONTROLLER {public function getDependencies( string $sourceClass = '' ): array {return ['A',];}};$controllerB = new class ( null, $controllerA ) extends CONTROLLER {protected $_controllerA;public function __construct( ?MODEL $model = null, $controllerA ) {parent::__construct( $model );$this->_controllerA = $controllerA;}};$this->assertEquals( ['A',get_class( $controllerA ),], $controllerB->getDependencies() );}public function testGetDependenciesWithSubDependenciesWhenBlocksAreDependentFromEachOther() {$controllerA = new class extends CONTROLLER {protected $_controllerB;public function setControllerB( $controllerB ) {$this->_controllerB = $controllerB;}};$controllerB = new class ( null, $controllerA ) extends CONTROLLER {protected $_controllerA;public function __construct( ?MODEL $model = null, $controllerA ) {parent::__construct( $model );$this->_controllerA = $controllerA;}};$controllerA->setControllerB( $controllerB );$this->assertEquals( [get_class( $controllerA ),], $controllerB->getDependencies() );}public function testGetDependenciesWithoutDuplicatesWhenSeveralWithOneType() {$controllerA = $this->_getController( null );$controllerB = new class ( null, $controllerA ) extends CONTROLLER {protected $_controllerA;protected $_controllerAA;protected $_controllerAAA;public function __construct( ?MODEL $model = null, $controllerA ) {parent::__construct( $model );$this->_controllerA   = $controllerA;$this->_controllerAA  = $controllerA;$this->_controllerAAA = $controllerA;}};$this->assertEquals( [get_class( $controllerA ),], $controllerB->getDependencies() );}////public function testAutoInitModel() {$modelClass      = str_replace( [ '::', '\\' ], '_', __METHOD__ );$controllerClass = $modelClass . Settings::$ControllerSuffix;eval( 'class ' . $modelClass . ' extends ' . MODEL::class . ' {}' );eval( 'class ' . $controllerClass . ' extends ' . CONTROLLER::class . ' {}' );$controller = new $controllerClass();$actualModelClass = $controller->getModel() ?get_class( $controller->getModel() ) :'';$this->assertEquals( $modelClass, $actualModelClass );}public function testAutoInitModelWhenModelHasWrongClass() {$modelClass      = str_replace( [ '::', '\\' ], '_', __METHOD__ );$controllerClass = $modelClass . Settings::$ControllerSuffix;eval( 'class ' . $modelClass . ' {}' );eval( 'class ' . $controllerClass . ' extends ' . CONTROLLER::class . ' {}' );$controller = new $controllerClass();$this->assertEquals( null, $controller->getModel() );}}

Settings

Вспомогательный класс, думаю что не нуждается в комментариях

Settings.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework;class Settings {public static string $ControllerSuffix = '_C';private string $_blocksDirPath;private string $_blocksDirNamespace;private array $_twigArgs;private string $_twigExtension;private $_errorCallback;public function __construct() {$this->_blocksDirPath      = '';$this->_blocksDirNamespace = '';$this->_twigArgs           = [// will generate exception if a var doesn't exist instead of replace to NULL'strict_variables' => true,// disable autoescape to prevent break data'autoescape'       => false,];$this->_twigExtension      = '.twig';$this->_errorCallback      = null;}public function setBlocksDirPath( string $blocksDirPath ): void {$this->_blocksDirPath = $blocksDirPath;}public function setBlocksDirNamespace( string $blocksDirNamespace ): void {$this->_blocksDirNamespace = $blocksDirNamespace;}public function setTwigArgs( array $twigArgs ): void {$this->_twigArgs = array_merge( $this->_twigArgs, $twigArgs );}public function setErrorCallback( ?callable $errorCallback ): void {$this->_errorCallback = $errorCallback;}public function setTwigExtension( string $twigExtension ): void {$this->_twigExtension = $twigExtension;}public function setControllerSuffix( string $controllerSuffix ): void {$this->_controllerSuffix = $controllerSuffix;}public function getBlocksDirPath(): string {return $this->_blocksDirPath;}public function getBlocksDirNamespace(): string {return $this->_blocksDirNamespace;}public function getTwigArgs(): array {return $this->_twigArgs;}public function getTwigExtension(): string {return $this->_twigExtension;}public function callErrorCallback( array $errors ): void {if ( ! is_callable( $this->_errorCallback ) ) {return;}call_user_func_array( $this->_errorCallback, [ $errors, ] );}}

Twig

Также вспомогательный класс, лишь уточню что мы расширили twig своей функцией _include (которая является оберткой для встроенного и использует наши поля _isLoaded и _template из метода CONROLLER->getTemplateArgs выше) и фильтр _merge (который отличается тем, что рекурсивно сливает массивы).

Twig.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework;use Exception;use Twig\Environment;use Twig\Loader\FilesystemLoader;use Twig\Loader\LoaderInterface;use Twig\TwigFilter;use Twig\TwigFunction;class Twig {private ?LoaderInterface $_twigLoader;private ?Environment $_twigEnvironment;private Settings $_settings;public function __construct( Settings $settings, ?LoaderInterface $twigLoader = null ) {$this->_twigEnvironment = null;$this->_settings        = $settings;$this->_twigLoader      = $twigLoader;$this->_init();}// e.g for extend a twig with adding a new filterpublic function getEnvironment(): ?Environment {return $this->_twigEnvironment;}private function _extendTwig(): void {$this->_twigEnvironment->addFilter( new TwigFilter( '_merge', function ( $source, $additional ) {return HELPER::ArrayMergeRecursive( $source, $additional );} ) );$this->_twigEnvironment->addFunction( new TwigFunction( '_include', function ( $block, $args = [] ) {$block = HELPER::ArrayMergeRecursive( $block, $args );return $block[ CONTROLLER::TEMPLATE_KEY__IS_LOADED ] ?$this->render( $block[ CONTROLLER::TEMPLATE_KEY__TEMPLATE ], $block ) :'';} ) );}private function _init(): void {try {$this->_twigLoader      = ! $this->_twigLoader ?new FilesystemLoader( $this->_settings->getBlocksDirPath() ) :$this->_twigLoader;$this->_twigEnvironment = new Environment( $this->_twigLoader, $this->_settings->getTwigArgs() );} catch ( Exception $ex ) {$this->_twigEnvironment = null;$this->_settings->callErrorCallback( ['message' => $ex->getMessage(),'file'    => $ex->getFile(),'line'    => $ex->getLine(),'trace'   => $ex->getTraceAsString(),] );return;}$this->_extendTwig();}public function render( string $template, array $args = [], bool $isPrint = false ): string {$html = '';// twig isn't loadedif ( is_null( $this->_twigEnvironment ) ) {return $html;}try {// will generate ean exception if a template doesn't exist OR broken// also if a var doesn't exist (if using a 'strict_variables' flag, see Twig_Environment->__construct)$html .= $this->_twigEnvironment->render( $template, $args );} catch ( Exception $ex ) {$html = '';$this->_settings->callErrorCallback( ['message'  => $ex->getMessage(),'file'     => $ex->getFile(),'line'     => $ex->getLine(),'trace'    => $ex->getTraceAsString(),'template' => $template,] );}if ( $isPrint ) {echo $html;}return $html;}}
TwigTest.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework\Tests\unit;use Codeception\Test\Unit;use Exception;use LightSource\FrontBlocksFramework\CONTROLLER;use LightSource\FrontBlocksFramework\Settings;use LightSource\FrontBlocksFramework\Twig;use Twig\Loader\ArrayLoader;class TwigTest extends Unit {private function _renderBlock( array $blocks, string $renderBlock, array $renderArgs = [] ): string {$twigLoader = new ArrayLoader( $blocks );$settings   = new Settings();$twig    = new Twig( $settings, $twigLoader );$content = '';try {$content = $twig->render( $renderBlock, $renderArgs );} catch ( Exception $ex ) {$this->fail( 'Twig render exception, ' . $ex->getMessage() );}return $content;}public function testExtendTwigIncludeFunctionWhenBlockIsLoaded() {$blocks      = ['block-a.twig' => '{{ _include(blockB) }}','block-b.twig' => 'block-b content',];$renderBlock = 'block-a.twig';$renderArgs  = ['blockB' => [CONTROLLER::TEMPLATE_KEY__TEMPLATE  => 'block-b.twig',CONTROLLER::TEMPLATE_KEY__IS_LOADED => true,],];$this->assertEquals( 'block-b content', $this->_renderBlock( $blocks, $renderBlock, $renderArgs ) );}public function testExtendTwigIncludeFunctionWhenBlockNotLoaded() {$blocks      = ['block-a.twig' => '{{ _include(blockB) }}','block-b.twig' => 'block-b content',];$renderBlock = 'block-a.twig';$renderArgs  = ['blockB' => [CONTROLLER::TEMPLATE_KEY__TEMPLATE  => 'block-b.twig',CONTROLLER::TEMPLATE_KEY__IS_LOADED => false,],];$this->assertEquals( '', $this->_renderBlock( $blocks, $renderBlock, $renderArgs ) );}public function testExtendTwigIncludeFunctionWhenArgsPassed() {$blocks      = ['block-a.twig' => '{{ _include(blockB, {classes:["test-class",],}) }}','block-b.twig' => '{{ classes|join(" ") }}',];$renderBlock = 'block-a.twig';$renderArgs  = ['blockB' => [CONTROLLER::TEMPLATE_KEY__TEMPLATE  => 'block-b.twig',CONTROLLER::TEMPLATE_KEY__IS_LOADED => true,'classes'                           => [ 'own-class', ],],];$this->assertEquals( 'own-class test-class', $this->_renderBlock( $blocks, $renderBlock, $renderArgs ) );}public function testExtendTwigMergeFilter() {$blocks      = ['block-a.twig' => '{{ {"array":["a",],}|_merge({"array":["b",],}).array|join(" ") }}',];$renderBlock = 'block-a.twig';$renderArgs  = [];$this->assertEquals( 'a b', $this->_renderBlock( $blocks, $renderBlock, $renderArgs ) );}}

Blocks

Это наш объединяющий класс.

Статический метод LoadAll загружает все контроллеры и вызывает статический метод OnLoad у каждого контроллера (в нашем случае это не используется, как было указано выше это необходимо для расширения и выходит за рамки статьи).

Метод renderBlock принимает объект контроллера и производит рендер блока, передавая в twig шаблон аргументы из метода CONROLLER->getTemplateArgs выше. Также добавляет класс используемого контроллера и классы всех его зависимостей в список использованных блоков, что позволит нам далее получить используемый css и js.

Ну и наконец метод getUsedResources используя список выше и статический метод CONTROLLER::GetResourceInfo позволяет нам после рендера блоков получить используемый css и js код, объединенный в правильной последовательности, т.е. с учетом всех зависимостей./

Blocks.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework;class Blocks {private array $_loadedControllerClasses;private array $_usedControllerClasses;private Settings $_settings;private Twig $_twig;public function __construct( Settings $settings ) {$this->_loadedControllerClasses = [];$this->_usedControllerClasses   = [];$this->_settings                = $settings;$this->_twig                    = new Twig( $settings );}final public function getLoadedControllerClasses(): array {return $this->_loadedControllerClasses;}final public function getUsedControllerClasses(): array {return $this->_usedControllerClasses;}final public function getSettings(): Settings {return $this->_settings;}final public function getTwig(): Twig {return $this->_twig;}final public function getUsedResources( string $extension, bool $isIncludeSource = false ): string {$resourcesContent = '';foreach ( $this->_usedControllerClasses as $usedControllerClass ) {$getResourcesInfoCallback = [ $usedControllerClass, 'GetResourceInfo' ];if ( ! is_callable( $getResourcesInfoCallback ) ) {$this->_settings->callErrorCallback( ['message' => "Controller class doesn't exist",'class'   => $usedControllerClass,] );continue;}$resourceInfo = call_user_func_array( $getResourcesInfoCallback, [$this->_settings,] );$pathToResourceFile = $this->_settings->getBlocksDirPath() . DIRECTORY_SEPARATOR . $resourceInfo['relativeResourcePath'] . $extension;if ( ! is_file( $pathToResourceFile ) ) {continue;}$resourcesContent .= $isIncludeSource ?"\n/* " . $resourceInfo['resourceName'] . " */\n" :'';$resourcesContent .= file_get_contents( $pathToResourceFile );}return $resourcesContent;}private function _loadController( string $phpClass, array $debugArgs ): bool {$isLoaded = false;if ( ! class_exists( $phpClass, true ) ||     ! is_subclass_of( $phpClass, CONTROLLER::class ) ) {$this->_settings->callErrorCallback( ['message' => "Class doesn't exist or doesn't child",'args'    => $debugArgs,] );return $isLoaded;}call_user_func( [ $phpClass, 'OnLoad' ] );return true;}private function _loadControllers( string $directory, string $namespace, array $controllerFileNames ): void {foreach ( $controllerFileNames as $controllerFileName ) {$phpFile   = implode( DIRECTORY_SEPARATOR, [ $directory, $controllerFileName ] );$phpClass  = implode( '\\', [ $namespace, str_replace( '.php', '', $controllerFileName ), ] );$debugArgs = ['directory' => $directory,'namespace' => $namespace,'phpFile'   => $phpFile,'phpClass'  => $phpClass,];if ( ! $this->_loadController( $phpClass, $debugArgs ) ) {continue;}$this->_loadedControllerClasses[] = $phpClass;}}private function _loadDirectory( string $directory, string $namespace ): void {// exclude ., ..$fs = array_diff( scandir( $directory ), [ '.', '..' ] );$controllerFilePreg = '/' . Settings::$ControllerSuffix . '.php$/';$controllerFileNames = HELPER::ArrayFilter( $fs, function ( $f ) use ( $controllerFilePreg ) {return ( 1 === preg_match( $controllerFilePreg, $f ) );}, false );$subDirectoryNames   = HELPER::ArrayFilter( $fs, function ( $f ) {return false === strpos( $f, '.' );}, false );foreach ( $subDirectoryNames as $subDirectoryName ) {$subDirectory = implode( DIRECTORY_SEPARATOR, [ $directory, $subDirectoryName ] );$subNamespace = implode( '\\', [ $namespace, $subDirectoryName ] );$this->_loadDirectory( $subDirectory, $subNamespace );}$this->_loadControllers( $directory, $namespace, $controllerFileNames );}final public function loadAll(): void {$directory = $this->_settings->getBlocksDirPath();$namespace = $this->_settings->getBlocksDirNamespace();$this->_loadDirectory( $directory, $namespace );}final public function renderBlock( CONTROLLER $controller, array $args = [], bool $isPrint = false ): string {$dependencies                 = array_merge( $controller->getDependencies(), [ get_class( $controller ), ] );$newDependencies              = array_diff( $dependencies, $this->_usedControllerClasses );$this->_usedControllerClasses = array_merge( $this->_usedControllerClasses, $newDependencies );$templateArgs = $controller->getTemplateArgs( $this->_settings );$templateArgs = HELPER::ArrayMergeRecursive( $templateArgs, $args );return $this->_twig->render( $templateArgs[ CONTROLLER::TEMPLATE_KEY__TEMPLATE ], $templateArgs, $isPrint );}}
BlocksTest.php
<?phpdeclare( strict_types=1 );namespace LightSource\FrontBlocksFramework\Tests\unit;use Codeception\Test\Unit;use Exception;use LightSource\FrontBlocksFramework\Blocks;use LightSource\FrontBlocksFramework\CONTROLLER;use LightSource\FrontBlocksFramework\MODEL;use LightSource\FrontBlocksFramework\Settings;use LightSource\FrontBlocksFramework\Twig;use org\bovigo\vfs\vfsStream;use org\bovigo\vfs\vfsStreamDirectory;class BlocksTest extends Unit {private function _getBlocks( string $namespace, vfsStreamDirectory $rootDirectory, array $structure, array $usedControllerClasses = [] ): ?Blocks {vfsStream::create( $structure, $rootDirectory );$settings = new Settings();$settings->setBlocksDirNamespace( $namespace );$settings->setBlocksDirPath( $rootDirectory->url() );$twig = $this->make( Twig::class, ['render' => function ( string $template, array $args = [], bool $isPrint = false ): string {return '';},] );try {$blocks = $this->make( Blocks::class, ['_loadedControllerClasses' => [],'_usedControllerClasses'   => $usedControllerClasses,'_twig'                    => $twig,'_settings'                => $settings,] );} catch ( Exception $ex ) {$this->fail( "Can't make Blocks stub, " . $ex->getMessage() );}$blocks->loadAll();return $blocks;}// get a unique namespace depending on a test method to prevent affect other testsprivate function _getUniqueControllerNamespaceWithAutoloader( string $methodConstant, vfsStreamDirectory $rootDirectory ): string {$namespace = str_replace( '::', '_', $methodConstant );spl_autoload_register( function ( $class ) use ( $rootDirectory, $namespace ) {$targetNamespace = $namespace . '\\';if ( 0 !== strpos( $class, $targetNamespace ) ) {return;}$relativePathToFile = str_replace( $targetNamespace, '', $class );$relativePathToFile = str_replace( '\\', '/', $relativePathToFile );$absPathToFile = $rootDirectory->url() . DIRECTORY_SEPARATOR . $relativePathToFile . '.php';include_once $absPathToFile;} );return $namespace;}// get a unique directory name depending on a test method to prevent affect other testsprivate function _getUniqueDirectory( string $methodConstant ): vfsStreamDirectory {$dirName = str_replace( [ ':', '\\' ], '_', $methodConstant );return vfsStream::setup( $dirName );}private function _getControllerClassFile( string $namespace, string $class ): string {$vendorControllerClass = '\LightSource\FrontBlocksFramework\CONTROLLER';return '<?php namespace ' . $namespace . '; class ' . $class . ' extends ' . $vendorControllerClass . ' {}';}private function _getController( array $dependencies = [] ) {return new class ( null, $dependencies ) extends CONTROLLER {private array $_dependencies;public function __construct( ?MODEL $model = null, array $dependencies ) {parent::__construct( $model );$this->_dependencies = $dependencies;}function getDependencies( string $sourceClass = '' ): array {return $this->_dependencies;}function getTemplateArgs( Settings $settings ): array {return [CONTROLLER::TEMPLATE_KEY__TEMPLATE => '',];}};}////public function testLoadAllControllersWithPrefix() {// fixme$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, ['Block' => ['Block_C.php' => $this->_getControllerClassFile( "{$namespace}\Block", 'Block_C' ),],] );$this->assertEquals( ["{$namespace}\Block\Block_C",], $blocks->getLoadedControllerClasses() );}public function testLoadAllIgnoreControllersWithoutPrefix() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, ['Block' => ['Block.php' => $this->_getControllerClassFile( "{$namespace}\Block", 'Block' ),],] );$this->assertEquals( [], $blocks->getLoadedControllerClasses() );}public function testLoadAllIgnoreWrongControllers() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, ['Block' => ['Block_C.php' => $this->_getControllerClassFile( "{$namespace}\Block", 'WrongBlock_C' ),],] );$this->assertEquals( [], $blocks->getLoadedControllerClasses() );}////public function testRenderBlockAddsControllerToUsedList() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, [] );$controller    = $this->_getController();$blocks->renderBlock( $controller );$this->assertEquals( [get_class( $controller ),], $blocks->getUsedControllerClasses() );}public function testRenderBlockAddsControllerDependenciesToUsedList() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, [] );$controller    = $this->_getController( [ 'A', ] );$blocks->renderBlock( $controller );$this->assertEquals( ['A',get_class( $controller ),], $blocks->getUsedControllerClasses() );}public function testRenderBlockAddsDependenciesBeforeControllerToUsedList() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, [] );$controller    = $this->_getController( [ 'A', ] );$blocks->renderBlock( $controller );$this->assertEquals( ['A',get_class( $controller ),], $blocks->getUsedControllerClasses() );}public function testRenderBlockIgnoreDuplicateControllerWhenAddsToUsedList() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, [] );$controllerA   = $this->_getController();$blocks->renderBlock( $controllerA );$blocks->renderBlock( $controllerA );$this->assertEquals( [get_class( $controllerA ),], $blocks->getUsedControllerClasses() );}public function testRenderBlockIgnoreDuplicateControllerDependenciesWhenAddsToUsedList() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, [] );$controllerA   = $this->_getController( [ 'A', ] );$controllerB   = $this->_getController( [ 'A', ] );$blocks->renderBlock( $controllerA );$blocks->renderBlock( $controllerB );$this->assertEquals( ['A',get_class( $controllerA ),// $controllerB has the same class], $blocks->getUsedControllerClasses() );}////public function testGetUsedResourcesWhenBlockWithResources() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, ['Block' => ['Block_C.php' => $this->_getControllerClassFile( "{$namespace}\Block", 'Block_C' ),'block.css'   => 'just css code',],], ["{$namespace}\Block\Block_C",] );$this->assertEquals( 'just css code',$blocks->getUsedResources( '.css', false ) );}public function testGetUsedResourcesWhenBlockWithoutResources() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, ['Block' => ['Block_C.php' => $this->_getControllerClassFile( "{$namespace}\Block", 'Block_C' ),],], ["{$namespace}\Block\Block_C",] );$this->assertEquals( '',$blocks->getUsedResources( '.css', false ) );}public function testGetUsedResourcesWhenSeveralBlocks() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, ['BlockA' => ['BlockA_C.php' => $this->_getControllerClassFile( "{$namespace}\BlockA", 'BlockA_C' ),'block-a.css'  => 'css code for a',],'BlockB' => ['BlockB_C.php' => $this->_getControllerClassFile( "{$namespace}\BlockB", 'BlockB_C' ),'block-b.css'  => 'css code for b',],], ["{$namespace}\BlockA\BlockA_C","{$namespace}\BlockB\BlockB_C",] );$this->assertEquals( 'css code for acss code for b',$blocks->getUsedResources( '.css', false ) );}public function testGetUsedResourcesWithIncludedSource() {$rootDirectory = $this->_getUniqueDirectory( __METHOD__ );$namespace     = $this->_getUniqueControllerNamespaceWithAutoloader( __METHOD__, $rootDirectory );$blocks        = $this->_getBlocks( $namespace, $rootDirectory, ['SimpleBlock' => ['SimpleBlock_C.php' => $this->_getControllerClassFile( "{$namespace}\SimpleBlock", 'SimpleBlock_C' ),'simple-block.css'  => 'css code',],], ["{$namespace}\SimpleBlock\SimpleBlock_C",] );$this->assertEquals( "\n/* simple-block */\ncss code",$blocks->getUsedResources( '.css', true ) );}}

Вот и все, теперь осталось объединить эти части и наш мини-фреймворк готов. Момент публикации composer пакета я опущу, т.к. это довольно простая задача и если вам будет интересно то вы без труда найдете информацию по этому поводу.

Демонстрационный пример

Ниже приведу демонстрационный пример использования пакета, с одним чистым css для наглядности, если кому-то интересен пример с scss/webpack смотрите ссылки в конце статьи.

Создаем блоки для теста, BlockA и BlockC будут независимыми блоками, BlockB будет содержкать BlockC.

BlockA

BlockA.php
<?phpnamespace LightSource\FrontBlocksExample\BlockA;use LightSource\FrontBlocksFramework\MODEL;class BlockA extends MODEL {protected string $_name;public function load() {parent::_load();$this->_name = 'I\'m BlockA';}}
BlockA_C.php

/sp

<?phpnamespace LightSource\FrontBlocksExample\BlockA;use LightSource\FrontBlocksFramework\Blocks;use LightSource\FrontBlocksFramework\CONTROLLER;class BlockA_C extends CONTROLLER {public function getModel(): ?BlockA {/** @noinspection PhpIncompatibleReturnTypeInspection */return parent::getModel();}}
block-a.twig

/

<div class="block-a">    {{ name }}</div>
block-a.css

Bl

.block-a {    color: green;    border:1px solid green;    padding: 10px;}

BlockB

BlockB.php
<?phpnamespace LightSource\FrontBlocksExample\BlockB;use LightSource\FrontBlocksExample\BlockC\BlockC;use LightSource\FrontBlocksFramework\MODEL;class BlockB extends MODEL {protected string $_name;protected BlockC $_blockC;public function __construct() {parent::__construct();$this->_blockC = new BlockC();}public function load() {parent::_load();$this->_name = 'I\'m BlockB, I contain another block';$this->_blockC->load();}}
BlockB_C.php
<?phpnamespace LightSource\FrontBlocksExample\BlockB;use LightSource\FrontBlocksExample\BlockC\BlockC_C;use LightSource\FrontBlocksFramework\CONTROLLER;class BlockB_C extends CONTROLLER {protected BlockC_C $_blockC;public function getModel(): ?BlockB {/** @noinspection PhpIncompatibleReturnTypeInspection */return parent::getModel();}}
block-b.twig
<div class="block-b">    <p class="block-b__name">{{ name }}</p>    {{ _include(blockC) }}</div>
block-b.css

Blo

.block-b {    color: orange;    border: 1px solid orange;    padding: 10px;}.block-b__name {    margin: 0 0 10px;    line-height: 1.5;}

BlocksC

BlockC.php
<?phpnamespace LightSource\FrontBlocksExample\BlockC;use LightSource\FrontBlocksFramework\MODEL;class BlockC extends MODEL {protected string $_name;public function load() {parent::_load();$this->_name = 'I\'m BlockC';}}
BlockC_C.php

/

<?phpnamespace LightSource\FrontBlocksExample\BlockC;use LightSource\FrontBlocksFramework\CONTROLLER;class BlockC_C extends CONTROLLER {public function getModel(): ?BlockC {/** @noinspection PhpIncompatibleReturnTypeInspection */return parent::getModel();}}

Подключаем наш пакет и рендерим блоки

block-c.twig
<div class="block-c">    {{ name }}</div>
block-c.css
.block-c {    color: black;    border: 1px solid black;    padding: 10px;}

Подключаем наш пакет и рендерим блоки, результат вставляем в html код страницы, в шапке выводим использованный css код

example.php
<?phpuse LightSource\FrontBlocksExample\{BlockA\BlockA_C,BlockB\BlockB_C,};use LightSource\FrontBlocksFramework\{Blocks,Settings};require_once __DIR__ . '/vendors/vendor/autoload.php';//// settings$settings = new Settings();$settings->setBlocksDirNamespace( 'LightSource\FrontBlocksExample' );$settings->setBlocksDirPath( __DIR__ . '/Blocks' );$settings->setErrorCallback( function ( array $errors ) {// todo log or any other actionsecho '<pre>' . print_r( $errors, true ) . '</pre>';});$blocks = new Blocks( $settings );//// usage$blockA_Controller = new BlockA_C();$blockA_Controller->getModel()->load();$blockB_Controller = new BlockB_C();$blockB_Controller->getModel()->load();$content = $blocks->renderBlock( $blockA_Controller );$content .= $blocks->renderBlock( $blockB_Controller );$css     = $blocks->getUsedResources( '.css', true );//// html?><html><head>    <title>Example</title>    <style>        <?= $css ?>    </style>    <style>        .block-b {            margin-top: 10px;        }    </style></head><body><?= $content ?></body></html>

в результате вывод будет примерно таким

example.png

Послесловие

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

Вот и все, спасибо за внимание.

Ссылки:

репозиторий с мини фреймворком

репозиторий с демонстрационным примером

репозиторий с примером использования scss и js в блоках (webpack сборщик)

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

Подробнее..

Модульные frond-end блоки пишем свой пакет. Часть 2

20.05.2021 14:22:33 | Автор: admin

В первой части я поделился своим взглядом на то, какими могут быть переиспользуемые front-end блоки, получил конструктивную критику, доработал пакет и теперь хотел бы поделиться с вами новой версией. Она позволит легко организовать использование модульных блоков для любого проекта с бекендом на php.

Для тех кто не знаком с первой частью я буду оставлять спойлеры из нее, которые введут в курс дела. Тем кому интересен конечный результат - демонстрационный пример и ссылки на репозитории в конце статьи.

Предисловие

Представлюсь - я молодой веб разработчик с опытом работы 5 лет. Крайний год я работаю на фрилансе и большая часть текущих проектов связана с WordPress. Несмотря на различую критику CMS в общем и WordPress в часности, я считаю сама архитектура WordPress это довольно удачное решение, хотя конечно не без определенных недостатков. И один из них на мой взгляд это шаблоны. В крайних обновлениях сделаны большие шаги чтобы это исправить, и Gutenberg в целом становится мощным инструментом, однако к сожалению в большинстве тем продолжается каша в шаблонах, стилях и скриптах, которая делает редактирование чего-либо крайне болезненным, а переиспользование кода зачастую невозможным. Именно эта проблема и подтолкнуло меня к идее своего пакета, который бы организовывал структуру и позволял переиспользовать блоки.

Реализация будет в виде composer пакета, который можно будет использовать в совершенно различных проектах, без привязки к WordPress.

Постановка задачи

Понятие блок ниже будет по сути тем же понятием что и блок в BEM методологии, т.е. это будет группа html/js/css кода которая будет представлять одну сущность.

Генерировать html и управлять зависимостями блоков мы будем через php, что говорит о том, что наш пакет будет подходить для проектов с бекендом на php. Также условимся на берегу что, не вдаваясь в споры, не будем поддаваться влиянию новомодных вещей, таких как css-in-js или bem-json и будем придерживаться эль-классико классического подхода, т.е. предполагать что html, css и js это разные файлы.

Теперь давайте сформулируем наши основные требования к пакету:

  • Обеспечить структуру блоков

  • Предоставить поддержку наследования (расширения) блоков

  • Предоставить возможность использовать блок в блоке и соответственно поддержку зависимости ресурсов одного блока от ресурсов других блоков

Структура пакета

О ресурах блока и twig шаблонах

Как условились выше, такие ресурсы как css и js всегда будут в виде обычных файлов, т.е. это будут .js и .css или .min.css и .min.js в случае использования препроцесссоров и сборщиков (как webpack например). Для вставки данных в html код мы будем использовать шаблонизатор Twig (для тех кто не знаком ссылка). Кто-то может заметить, что php и сам по себе хороший шаблонизатор, не будем вдаваться в споры, кроме доводов указанных на главной странице проекта Twig, отмечу важный для меня пункт, то что он дисциплинирует, т.е. заставляет отделять обработку от вывода и подготавливать переменные заранее, и в данном случае мы будем использовать его.

  1. Блок

    Каждый блок будет состоять из:

    1. Статических ресурсов (css/js/twig)

    2. Класса блока, который будет предоставлять данные для twig шаблона и управлять зависимостями.

  2. Вспомогательные классы: Settings (пути к блокам и их пространства имен), TwigWrapper (обертка для Twig пакета), BlocksLoader (автозагрузка всех блоков, опционально), Helper (набор статических доп. функций)

  3. Renderer класс - связующий класс, который будет объединять вспомогательные классы, предоставлять функцию рендера блока, содержать список использованных блоков и их ресурсы (css, js)

Требования к блокам

В отличии от первого пакета количество требований сократилось, теперь это:

  • php 7.4

  • Классы блоков должны иметь PSR-4 совместимое пространство имен с автозагрузчиком (PSR-4 де факто стандарт, если вы используете автозагрузчик от composer, т.е. указываете autoload/psr4 директиву в вашем composer.json то ваш проект уже соответствует этому требованию)

  • Имена ресурсов должны совпадать с именем блока (например для Button.php будут Button.css и Button.twig)

Реализация

Ниже части реализации (классы) будут в формате : текстовое описание, код реализации и код тестов.

Block

Основной действующий класс, его потомки будут содержать данные для twig шаблона (в protected полях) и предоставлять список зависимостей, а также мы сможем получить путь к ресурсам (шаблону, стилям). Все наша магия при работе с полями будет строится на функции get_class_vars которая предоставит имена полей класса и на ReflectionProperty классе, который предоставит информацию об этих полях, такую как видимость поля (protected/public) и его тип. Мы будем собирать информацию только о protected полях.

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

Block.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks;use Exception;use ReflectionProperty;abstract class Block{    public const TEMPLATE_KEY_NAMESPACE = '_namespace';    public const TEMPLATE_KEY_TEMPLATE = '_template';    public const TEMPLATE_KEY_IS_LOADED = '_isLoaded';    public const RESOURCE_KEY_NAMESPACE = 'namespace';    public const RESOURCE_KEY_FOLDER = 'folder';    public const RESOURCE_KEY_RELATIVE_RESOURCE_PATH = 'relativeResourcePath';    public const RESOURCE_KEY_RELATIVE_BLOCK_PATH = 'relativeBlockPath';    public const RESOURCE_KEY_RESOURCE_NAME = 'resourceName';    private array $fieldsInfo;    private bool $isLoaded;    public function __construct()    {        $this->fieldsInfo = [];        $this->isLoaded   = false;        $this->readFieldsInfo();        $this->autoInitFields();    }    public static function onLoad()    {    }    public static function getResourceInfo(Settings $settings, string $blockClass = ''): ?array    {        // using static for child support        $blockClass = ! $blockClass ?            static::class :            $blockClass;        // e.g. $blockClass = Namespace/Example/Theme/Main/ExampleThemeMain        $resourceInfo = [            self::RESOURCE_KEY_NAMESPACE              => '',            self::RESOURCE_KEY_FOLDER                 => '',            self::RESOURCE_KEY_RELATIVE_RESOURCE_PATH => '',// e.g. Example/Theme/Main/ExampleThemeMain            self::RESOURCE_KEY_RELATIVE_BLOCK_PATH    => '',// e.g. Example/Theme/Main            self::RESOURCE_KEY_RESOURCE_NAME          => '',// e.g. ExampleThemeMain        ];        $blockFolderInfo = $settings->getBlockFolderInfoByBlockClass($blockClass);        if (! $blockFolderInfo) {            $settings->callErrorCallback(                [                    'error'      => 'Block has the non registered namespace',                    'blockClass' => $blockClass,                ]            );            return null;        }        $resourceInfo[self::RESOURCE_KEY_NAMESPACE] = $blockFolderInfo['namespace'];        $resourceInfo[self::RESOURCE_KEY_FOLDER]    = $blockFolderInfo['folder'];        //  e.g. Example/Theme/Main/ExampleThemeMain        $relativeBlockNamespace = str_replace($resourceInfo[self::RESOURCE_KEY_NAMESPACE] . '\\', '', $blockClass);        // e.g. ExampleThemeMain        $blockName = explode('\\', $relativeBlockNamespace);        $blockName = $blockName[count($blockName) - 1];        // e.g. Example/Theme/Main        $relativePath = explode('\\', $relativeBlockNamespace);        $relativePath = array_slice($relativePath, 0, count($relativePath) - 1);        $relativePath = implode(DIRECTORY_SEPARATOR, $relativePath);        $resourceInfo[self::RESOURCE_KEY_RELATIVE_RESOURCE_PATH] = $relativePath . DIRECTORY_SEPARATOR . $blockName;        $resourceInfo[self::RESOURCE_KEY_RELATIVE_BLOCK_PATH]    = $relativePath;        $resourceInfo[self::RESOURCE_KEY_RESOURCE_NAME]          = $blockName;        return $resourceInfo;    }    private static function getResourceInfoForTwigTemplate(Settings $settings, string $blockClass): ?array    {        $resourceInfo = self::getResourceInfo($settings, $blockClass);        if (! $resourceInfo) {            return null;        }        $absTwigPath = implode(            '',            [                $resourceInfo['folder'],                DIRECTORY_SEPARATOR,                $resourceInfo['relativeResourcePath'],                $settings->getTwigExtension(),            ]        );        if (! is_file($absTwigPath)) {            $parentClass = get_parent_class($blockClass);            if ($parentClass &&                is_subclass_of($parentClass, self::class) &&                self::class !== $parentClass) {                return self::getResourceInfoForTwigTemplate($settings, $parentClass);            } else {                return null;            }        }        return $resourceInfo;    }    final public function getFieldsInfo(): array    {        return $this->fieldsInfo;    }    final public function isLoaded(): bool    {        return $this->isLoaded;    }    private function getBlockField(string $fieldName): ?Block    {        $block      = null;        $fieldsInfo = $this->fieldsInfo;        if (key_exists($fieldName, $fieldsInfo)) {            $block = $this->{$fieldName};            // prevent possible recursion by a mistake (if someone will create a field with self)            // using static for children support            $block = ($block &&                      $block instanceof Block &&                      get_class($block) !== static::class) ?                $block :                null;        }        return $block;    }    public function getDependencies(string $sourceClass = ''): array    {        $dependencyClasses = [];        $fieldsInfo        = $this->fieldsInfo;        foreach ($fieldsInfo as $fieldName => $fieldType) {            $dependencyBlock = $this->getBlockField($fieldName);            if (! $dependencyBlock) {                continue;            }            $dependencyClass = get_class($dependencyBlock);            // 1. prevent the possible permanent recursion            // 2. add only unique elements, because several fields can have the same type            if (                ($sourceClass && $dependencyClass === $sourceClass) ||                in_array($dependencyClass, $dependencyClasses, true)            ) {                continue;            }            // used static for child support            $subDependencies = $dependencyBlock->getDependencies(static::class);            // only unique elements            $subDependencies = array_diff($subDependencies, $dependencyClasses);            // sub dependencies are before the main dependency            $dependencyClasses = array_merge($dependencyClasses, $subDependencies, [$dependencyClass,]);        }        return $dependencyClasses;    }    // can be overridden for add external arguments    public function getTemplateArgs(Settings $settings): array    {        // using static for child support        $resourceInfo = self::getResourceInfoForTwigTemplate($settings, static::class);        $pathToTemplate = $resourceInfo ?            $resourceInfo[self::RESOURCE_KEY_RELATIVE_RESOURCE_PATH] . $settings->getTwigExtension() :            '';        $namespace      = $resourceInfo[self::RESOURCE_KEY_NAMESPACE] ?? '';        $templateArgs = [            self::TEMPLATE_KEY_NAMESPACE => $namespace,            self::TEMPLATE_KEY_TEMPLATE  => $pathToTemplate,            self::TEMPLATE_KEY_IS_LOADED => $this->isLoaded,        ];        if (! $pathToTemplate) {            $settings->callErrorCallback(                [                    'error' => 'Twig template is missing for the block',                    // using static for child support                    'class' => static::class,                ]            );        }        foreach ($this->fieldsInfo as $fieldName => $fieldType) {            $value = $this->{$fieldName};            if ($value instanceof self) {                $value = $value->getTemplateArgs($settings);            }            $templateArgs[$fieldName] = $value;        }        return $templateArgs;    }    protected function getFieldType(string $fieldName): ?string    {        $fieldType = null;        try {            // used static for child support            $property = new ReflectionProperty(static::class, $fieldName);        } catch (Exception $ex) {            return $fieldType;        }        if (! $property->isProtected()) {            return $fieldType;        }        return $property->getType() ?            $property->getType()->getName() :            '';    }    private function readFieldsInfo(): void    {        $fieldNames = array_keys(get_class_vars(static::class));        foreach ($fieldNames as $fieldName) {            $fieldType = $this->getFieldType($fieldName);            // only protected fields            if (is_null($fieldType)) {                continue;            }            $this->fieldsInfo[$fieldName] = $fieldType;        }    }    private function autoInitFields(): void    {        foreach ($this->fieldsInfo as $fieldName => $fieldType) {            // ignore fields without a type            if (! $fieldType) {                continue;            }            $defaultValue = null;            switch ($fieldType) {                case 'int':                case 'float':                    $defaultValue = 0;                    break;                case 'bool':                    $defaultValue = false;                    break;                case 'string':                    $defaultValue = '';                    break;                case 'array':                    $defaultValue = [];                    break;            }            try {                if (is_subclass_of($fieldType, Block::class)) {                    $defaultValue = new $fieldType();                }            } catch (Exception $ex) {                $defaultValue = null;            }            // ignore fields with a custom type (null by default)            if (is_null($defaultValue)) {                continue;            }            $this->{$fieldName} = $defaultValue;        }    }    final protected function load(): void    {        $this->isLoaded = true;    }}
BlockTest.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocks\Block;use LightSource\FrontBlocks\Settings;use org\bovigo\vfs\vfsStream;use UnitTester;class BlockTest extends Unit{    protected UnitTester $tester;    public function testReadProtectedFields()    {        $block = new class extends Block {            protected $loadedField;        };        $this->assertEquals(            ['loadedField',],            array_keys($block->getFieldsInfo())        );    }    public function testIgnoreReadPublicFields()    {        $block = new class extends Block {            public $ignoredField;        };        $this->assertEquals(            [],            array_keys($block->getFieldsInfo())        );    }    public function testReadFieldWithType()    {        $block = new class extends Block {            protected string $loadedField;        };        $this->assertEquals(            [                'loadedField' => 'string',            ],            $block->getFieldsInfo()        );    }    public function testReadFieldWithoutType()    {        $block = new class extends Block {            protected $loadedField;        };        $this->assertEquals(            [                'loadedField' => '',            ],            $block->getFieldsInfo()        );    }    public function testAutoInitIntField()    {        $block = new class extends Block {            protected int $int;            public function getInt()            {                return $this->int;            }        };        $this->assertTrue(0 === $block->getInt());    }    public function testAutoInitFloatField()    {        $block = new class extends Block {            protected float $float;            public function getFloat()            {                return $this->float;            }        };        $this->assertTrue(0.0 === $block->getFloat());    }    public function testAutoInitStringField()    {        $block = new class extends Block {            protected string $string;            public function getString()            {                return $this->string;            }        };        $this->assertTrue('' === $block->getString());    }    public function testAutoInitBoolField()    {        $block = new class extends Block {            protected bool $bool;            public function getBool()            {                return $this->bool;            }        };        $this->assertTrue(false === $block->getBool());    }    public function testAutoInitArrayField()    {        $block = new class extends Block {            protected array $array;            public function getArray()            {                return $this->array;            }        };        $this->assertTrue([] === $block->getArray());    }    public function testAutoInitBlockField()    {        $testBlock        = new class extends Block {        };        $testBlockClass   = get_class($testBlock);        $block            = new class ($testBlockClass) extends Block {            protected $block;            private $testClass;            public function __construct($testClass)            {                $this->testClass = $testClass;                parent::__construct();            }            public function getFieldType(string $fieldName): ?string            {                return ('block' === $fieldName ?                    $this->testClass :                    parent::getFieldType($fieldName));            }            public function getBlock()            {                return $this->block;            }        };        $actualBlockClass = $block->getBlock() ?            get_class($block->getBlock()) :            '';        $this->assertEquals($actualBlockClass, $testBlockClass);    }    public function testIgnoreAutoInitFieldWithoutType()    {        $block = new class extends Block {            protected $default;            public function getDefault()            {                return $this->default;            }        };        $this->assertTrue(null === $block->getDefault());    }    public function testGetResourceInfo()    {        $settings = new Settings();        $settings->addBlocksFolder('TestNamespace', 'test-folder');        $this->assertEquals(            [                Block::RESOURCE_KEY_NAMESPACE              => 'TestNamespace',                Block::RESOURCE_KEY_FOLDER                 => 'test-folder',                Block::RESOURCE_KEY_RELATIVE_RESOURCE_PATH => 'Button/Theme/Red/ButtonThemeRed',                Block::RESOURCE_KEY_RELATIVE_BLOCK_PATH    => 'Button/Theme/Red',                Block::RESOURCE_KEY_RESOURCE_NAME          => 'ButtonThemeRed',            ],            Block::getResourceInfo($settings, 'TestNamespace\\Button\\Theme\\Red\\ButtonThemeRed')        );    }    public function testGetDependenciesWithSubDependenciesRecursively()    {        $spanBlock   = new class extends Block {        };        $buttonBlock = new class ($spanBlock) extends Block {            protected $spanBlock;            public function __construct($spanBlock)            {                parent::__construct();                $this->spanBlock = $spanBlock;            }        };        $formBlock   = new class ($buttonBlock) extends Block {            protected $buttonBlock;            public function __construct($buttonBlock)            {                parent::__construct();                $this->buttonBlock = $buttonBlock;            }        };        $this->assertEquals(            [                get_class($spanBlock),                get_class($buttonBlock),            ],            $formBlock->getDependencies()        );    }    public function testGetDependenciesInRightOrder()    {        $spanBlock   = new class extends Block {        };        $buttonBlock = new class ($spanBlock) extends Block {            protected $spanBlock;            public function __construct($spanBlock)            {                parent::__construct();                $this->spanBlock = $spanBlock;            }        };        $formBlock   = new class ($buttonBlock) extends Block {            protected $buttonBlock;            public function __construct($buttonBlock)            {                parent::__construct();                $this->buttonBlock = $buttonBlock;            }        };        $this->assertEquals(            [                get_class($spanBlock),                get_class($buttonBlock),            ],            $formBlock->getDependencies()        );    }    public function testGetDependenciesWhenBlocksAreDependentFromEachOther()    {        $buttonBlock = new class extends Block {            protected $formBlock;            public function __construct()            {                parent::__construct();            }            public function setFormBlock($formBlock)            {                $this->formBlock = $formBlock;            }        };        $formBlock   = new class ($buttonBlock) extends Block {            protected $buttonBlock;            public function __construct($buttonBlock)            {                parent::__construct();                $this->buttonBlock = $buttonBlock;            }        };        $buttonBlock->setFormBlock($formBlock);        $this->assertEquals(            [                get_class($buttonBlock),            ],            $formBlock->getDependencies()        );    }    public function testGetDependenciesWithoutDuplicatesWhenSeveralWithOneType()    {        function getButtonBlock()        {            return new class extends Block {            };        }        $inputBlock = new class (getButtonBlock()) extends Block {            protected $buttonBlock;            public function __construct($buttonBlock)            {                parent::__construct();                $this->buttonBlock = $buttonBlock;            }        };        $formBlock = new class ($inputBlock) extends Block {            protected $inputBlock;            protected $firstButtonBlock;            protected $secondButtonBlock;            public function __construct($inputBlock)            {                parent::__construct();                $this->inputBlock        = $inputBlock;                $this->firstButtonBlock  = getButtonBlock();                $this->secondButtonBlock = getButtonBlock();            }        };        $this->assertEquals(            [                get_class(getButtonBlock()),                get_class($inputBlock),            ],            $formBlock->getDependencies()        );    }    public function testGetTemplateArgsWhenBlockContainsBuiltInTypes()    {        $settings    = new Settings();        $buttonBlock = new class extends Block {            protected string $name;            public function __construct()            {                parent::__construct();                $this->name = 'button';            }        };        $this->assertEquals(            [                Block::TEMPLATE_KEY_NAMESPACE => '',                Block::TEMPLATE_KEY_TEMPLATE  => '',                Block::TEMPLATE_KEY_IS_LOADED => false,                'name'                        => 'button',            ],            $buttonBlock->getTemplateArgs($settings)        );    }    public function testGetTemplateArgsWhenBlockContainsAnotherBlockRecursively()    {        $settings    = new Settings();        $spanBlock   = new class extends Block {            protected string $name;            public function __construct()            {                parent::__construct();                $this->name = 'span';            }        };        $buttonBlock = new class ($spanBlock) extends Block {            protected $spanBlock;            public function __construct($spanBlock)            {                parent::__construct();                $this->spanBlock = $spanBlock;            }        };        $formBlock   = new class ($buttonBlock) extends Block {            protected $buttonBlock;            public function __construct($buttonBlock)            {                parent::__construct();                $this->buttonBlock = $buttonBlock;            }        };        $this->assertEquals(            [                Block::TEMPLATE_KEY_NAMESPACE => '',                Block::TEMPLATE_KEY_TEMPLATE  => '',                Block::TEMPLATE_KEY_IS_LOADED => false,                'buttonBlock'                 => [                    Block::TEMPLATE_KEY_NAMESPACE => '',                    Block::TEMPLATE_KEY_TEMPLATE  => '',                    Block::TEMPLATE_KEY_IS_LOADED => false,                    'spanBlock'                   => [                        Block::TEMPLATE_KEY_NAMESPACE => '',                        Block::TEMPLATE_KEY_TEMPLATE  => '',                        Block::TEMPLATE_KEY_IS_LOADED => false,                        'name'                        => 'span',                    ],                ],            ],            $formBlock->getTemplateArgs($settings)        );    }    public function testGetTemplateArgsWhenTemplateIsInParent()    {        $rootDirectory = $this->tester->getUniqueDirectory(__METHOD__);        $namespace     = $this->tester->getUniqueControllerNamespaceWithAutoloader(__METHOD__, $rootDirectory->url());        $blocksFolder  = vfsStream::create(            [                'ButtonBase'  => [                    'ButtonBase.php'  => $this->tester->getBlockClassFile(                        $namespace . '\ButtonBase',                        'ButtonBase',                        '\\' . Block::class                    ),                    'ButtonBase.twig' => '',                ],                'ButtonChild' => [                    'ButtonChild.php' => $this->tester->getBlockClassFile(                        $namespace . '\ButtonChild',                        'ButtonChild',                        '\\' . $namespace . '\ButtonBase\ButtonBase'                    ),                ],            ],            $rootDirectory        );        $settings = new Settings();        $settings->addBlocksFolder($namespace, $blocksFolder->url());        $buttonChildClass = $namespace . '\ButtonChild\ButtonChild';        $buttonChild      = new $buttonChildClass();        if (! $buttonChild instanceof Block) {            $this->fail("Class doesn't child to Block");        }        $this->assertEquals(            [                Block::TEMPLATE_KEY_NAMESPACE => $namespace,                Block::TEMPLATE_KEY_TEMPLATE  => 'ButtonBase/ButtonBase.twig',                Block::TEMPLATE_KEY_IS_LOADED => false,            ],            $buttonChild->getTemplateArgs($settings)        );    }}

BlocksLoader

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

BlocksLoader.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks;class BlocksLoader{    private array $loadedBlockClasses;    private Settings $settings;    public function __construct(Settings $settings)    {        $this->loadedBlockClasses = [];        $this->settings           = $settings;    }    final public function getLoadedBlockClasses(): array    {        return $this->loadedBlockClasses;    }    private function tryToLoadBlock(string $phpClass): bool    {        $isLoaded = false;        if (            ! class_exists($phpClass, true) ||            ! is_subclass_of($phpClass, Block::class)        ) {            // without any error, because php files can contain other things            return $isLoaded;        }        call_user_func([$phpClass, 'onLoad']);        return true;    }    private function loadBlocks(string $namespace, array $phpFileNames): void    {        foreach ($phpFileNames as $phpFileName) {            $phpClass = implode('\\', [$namespace, str_replace('.php', '', $phpFileName),]);            if (! $this->tryToLoadBlock($phpClass)) {                continue;            }            $this->loadedBlockClasses[] = $phpClass;        }    }    private function loadDirectory(string $directory, string $namespace): void    {        // exclude ., ..        $fs = array_diff(scandir($directory), ['.', '..']);        $phpFilePreg = '/.php$/';        $phpFileNames      = Helper::arrayFilter(            $fs,            function ($f) use ($phpFilePreg) {                return (1 === preg_match($phpFilePreg, $f));            },            false        );        $subDirectoryNames = Helper::arrayFilter(            $fs,            function ($f) {                return false === strpos($f, '.');            },            false        );        foreach ($subDirectoryNames as $subDirectoryName) {            $subDirectory = implode(DIRECTORY_SEPARATOR, [$directory, $subDirectoryName]);            $subNamespace = implode('\\', [$namespace, $subDirectoryName]);            $this->loadDirectory($subDirectory, $subNamespace);        }        $this->loadBlocks($namespace, $phpFileNames);    }    final public function loadAllBlocks(): void    {        $blockFoldersInfo = $this->settings->getBlockFoldersInfo();        foreach ($blockFoldersInfo as $namespace => $folder) {            $this->loadDirectory($folder, $namespace);        }    }}
BlocksLoaderTest.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocks\Block;use LightSource\FrontBlocks\BlocksLoader;use LightSource\FrontBlocks\Settings;use org\bovigo\vfs\vfsStream;use UnitTester;class BlocksLoaderTest extends Unit{    protected UnitTester $tester;    public function testLoadAllBlocksWhichChildToBlock()    {        $rootDirectory = $this->tester->getUniqueDirectory(__METHOD__);        $namespace     = $this->tester->getUniqueControllerNamespaceWithAutoloader(__METHOD__, $rootDirectory->url());        $blocksFolder  = vfsStream::create(            [                'ButtonBase'  => [                    'ButtonBase.php' => $this->tester->getBlockClassFile(                        $namespace . '\ButtonBase',                        'ButtonBase',                        '\\' . Block::class                    ),                ],                'ButtonChild' => [                    'ButtonChild.php' => $this->tester->getBlockClassFile(                        $namespace . '\ButtonChild',                        'ButtonChild',                        '\\' . $namespace . '\ButtonBase\ButtonBase'                    ),                ],            ],            $rootDirectory        );        $settings = new Settings();        $settings->addBlocksFolder($namespace, $blocksFolder->url());        $blocksLoader = new BlocksLoader($settings);        $blocksLoader->loadAllBlocks();        $this->assertEquals(            [                $namespace . '\ButtonBase\ButtonBase',                $namespace . '\ButtonChild\ButtonChild',            ],            $blocksLoader->getLoadedBlockClasses()        );    }    public function testLoadAllBlocksIgnoreNonChild()    {        $rootDirectory = $this->tester->getUniqueDirectory(__METHOD__);        $namespace     = $this->tester->getUniqueControllerNamespaceWithAutoloader(__METHOD__, $rootDirectory->url());        $blocksFolder  = vfsStream::create(            [                'ButtonBase' => [                    'ButtonBase.php' => '<?php use ' . $namespace . '; class ButtonBase{}',                ],            ],            $rootDirectory        );        $settings = new Settings();        $settings->addBlocksFolder($namespace, $blocksFolder->url());        $blocksLoader = new BlocksLoader($settings);        $blocksLoader->loadAllBlocks();        $this->assertEmpty($blocksLoader->getLoadedBlockClasses());    }    public function testLoadAllBlocksInSeveralFolders()    {        $rootDirectory   = $this->tester->getUniqueDirectory(__METHOD__);        $firstFolderUrl  = $rootDirectory->url() . '/First';        $secondFolderUrl = $rootDirectory->url() . '/Second';        $firstNamespace  = $this->tester->getUniqueControllerNamespaceWithAutoloader(            __METHOD__ . '_first',            $firstFolderUrl,        );        $secondNamespace = $this->tester->getUniqueControllerNamespaceWithAutoloader(            __METHOD__ . '_second',            $secondFolderUrl,        );        vfsStream::create(            [                'First'  => [                    'ButtonBase' => [                        'ButtonBase.php' => $this->tester->getBlockClassFile(                            $firstNamespace . '\ButtonBase',                            'ButtonBase',                            '\\' . Block::class                        ),                    ],                ],                'Second' => [                    'ButtonBase' => [                        'ButtonBase.php' => $this->tester->getBlockClassFile(                            $secondNamespace . '\ButtonBase',                            'ButtonBase',                            '\\' . Block::class                        ),                    ],                ],            ],            $rootDirectory        );        $settings = new Settings();        $settings->addBlocksFolder($firstNamespace, $firstFolderUrl);        $settings->addBlocksFolder($secondNamespace, $secondFolderUrl);        $blocksLoader = new BlocksLoader($settings);        $blocksLoader->loadAllBlocks();        $this->assertEquals(            [                $firstNamespace . '\ButtonBase\ButtonBase',                $secondNamespace . '\ButtonBase\ButtonBase',            ],            $blocksLoader->getLoadedBlockClasses()        );    }}

Renderer

Связующий класс, объединяет вспомогательные классы, предоставляет функцию рендера блока, содержит список использованных блоков и их ресурсы (css, js)

Renderer.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks;class Renderer{    private Settings $settings;    private TwigWrapper $twigWrapper;    private BlocksLoader $blocksLoader;    private array $usedBlockClasses;    public function __construct(Settings $settings)    {        $this->settings         = $settings;        $this->twigWrapper             = new TwigWrapper($settings);        $this->blocksLoader     = new BlocksLoader($settings);        $this->usedBlockClasses = [];    }    final public function getSettings(): Settings    {        return $this->settings;    }    final public function getTwigWrapper(): TwigWrapper    {        return $this->twigWrapper;    }    final public function getBlocksLoader(): BlocksLoader    {        return $this->blocksLoader;    }    final public function getUsedBlockClasses(): array    {        return $this->usedBlockClasses;    }    final public function getUsedResources(string $extension, bool $isIncludeSource = false): string    {        $resourcesContent = '';        foreach ($this->usedBlockClasses as $usedBlockClass) {            $getResourcesInfoCallback = [$usedBlockClass, 'getResourceInfo'];            if (! is_callable($getResourcesInfoCallback)) {                $this->settings->callErrorCallback(                    [                        'message' => "Block class doesn't exist",                        'class'   => $usedBlockClass,                    ]                );                continue;            }            $resourceInfo = call_user_func_array(                $getResourcesInfoCallback,                [                    $this->settings,                ]            );            $pathToResourceFile = $resourceInfo['folder'] .                                  DIRECTORY_SEPARATOR . $resourceInfo['relativeResourcePath'] . $extension;            if (! is_file($pathToResourceFile)) {                continue;            }            $resourcesContent .= $isIncludeSource ?                "\n/* " . $resourceInfo['resourceName'] . " */\n" :                '';            $resourcesContent .= file_get_contents($pathToResourceFile);        }        return $resourcesContent;    }    final public function render(Block $block, array $args = [], bool $isPrint = false): string    {        $dependencies           = array_merge($block->getDependencies(), [get_class($block),]);        $newDependencies        = array_diff($dependencies, $this->usedBlockClasses);        $this->usedBlockClasses = array_merge($this->usedBlockClasses, $newDependencies);        $templateArgs           = $block->getTemplateArgs($this->settings);        $templateArgs           = Helper::arrayMergeRecursive($templateArgs, $args);        $namespace              = $templateArgs[Block::TEMPLATE_KEY_NAMESPACE];        $relativePathToTemplate = $templateArgs[Block::TEMPLATE_KEY_TEMPLATE];        // log already exists        if (! $relativePathToTemplate) {            return '';        }        return $this->twigWrapper->render($namespace, $relativePathToTemplate, $templateArgs, $isPrint);    }}
RendererTest.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocks\Block;use LightSource\FrontBlocks\Renderer;use LightSource\FrontBlocks\Settings;use org\bovigo\vfs\vfsStream;use UnitTester;class RendererTest extends Unit{    protected UnitTester $tester;    public function testRenderAddsBlockToUsedList()    {        $settings = new Settings();        $renderer = new Renderer($settings);        $button = new class extends Block {        };        $renderer->render($button);        $this->assertEquals(            [                get_class($button),            ],            $renderer->getUsedBlockClasses()        );    }    public function testRenderAddsBlockDependenciesToUsedList()    {        $settings = new Settings();        $renderer = new Renderer($settings);        $button = new class extends Block {        };        $form   = new class ($button) extends Block {            protected $button;            public function __construct($button)            {                parent::__construct();                $this->button = $button;            }        };        $renderer->render($form);        $this->assertEquals(            [                get_class($button),                get_class($form),            ],            $renderer->getUsedBlockClasses()        );    }    public function testRenderAddsDependenciesBeforeBlockToUsedList()    {        $settings = new Settings();        $renderer = new Renderer($settings);        $button = new class extends Block {        };        $form   = new class ($button) extends Block {            protected $button;            public function __construct($button)            {                parent::__construct();                $this->button = $button;            }        };        $renderer->render($form);        $this->assertEquals(            [                get_class($button),                get_class($form),            ],            $renderer->getUsedBlockClasses()        );    }    public function testRenderAddsBlockToUsedListOnce()    {        $settings = new Settings();        $renderer = new Renderer($settings);        $button = new class extends Block {        };        $renderer->render($button);        $renderer->render($button);        $this->assertEquals(            [                get_class($button),            ],            $renderer->getUsedBlockClasses()        );    }    public function testRenderAddsBlockDependenciesToUsedListOnce()    {        $settings = new Settings();        $renderer = new Renderer($settings);        $button = new class extends Block {        };        $form   = new class ($button) extends Block {            protected $button;            public function __construct($button)            {                parent::__construct();                $this->button = $button;            }        };        $footer = new class ($button) extends Block {            protected $button;            public function __construct($button)            {                parent::__construct();                $this->button = $button;            }        };        $renderer->render($form);        $renderer->render($footer);        $this->assertEquals(            [                get_class($button),                get_class($form),                get_class($footer),            ],            $renderer->getUsedBlockClasses()        );    }    public function testGetUsedResources()    {        $rootDirectory = $this->tester->getUniqueDirectory(__METHOD__);        $namespace     = $this->tester->getUniqueControllerNamespaceWithAutoloader(__METHOD__, $rootDirectory->url());        $blocksFolder  = vfsStream::create(            [                'Button' => [                    'Button.php' => $this->tester->getBlockClassFile(                        $namespace . '\Button',                        'Button',                        '\\' . Block::class                    ),                    'Button.css' => '.button{}',                ],                'Form'   => [                    'Form.php' => $this->tester->getBlockClassFile(                        $namespace . '\Form',                        'Form',                        '\\' . Block::class                    ),                    'Form.css' => '.form{}',                ],            ],            $rootDirectory        );        $formClass   = $namespace . '\Form\Form';        $form        = new $formClass();        $buttonClass = $namespace . '\Button\Button';        $button      = new $buttonClass();        $settings = new Settings();        $settings->addBlocksFolder($namespace, $blocksFolder->url());        $renderer = new Renderer($settings);        $renderer->render($button);        $renderer->render($form);        $this->assertEquals('.button{}.form{}', $renderer->getUsedResources('.css'));    }    public function testGetUsedResourcesWithIncludedSource()    {        $rootDirectory = $this->tester->getUniqueDirectory(__METHOD__);        $namespace     = $this->tester->getUniqueControllerNamespaceWithAutoloader(__METHOD__, $rootDirectory->url());        $blocksFolder  = vfsStream::create(            [                'Button' => [                    'Button.php' => $this->tester->getBlockClassFile(                        $namespace . '\Button',                        'Button',                        '\\' . Block::class                    ),                    'Button.css' => '.button{}',                ],                'Form'   => [                    'Form.php' => $this->tester->getBlockClassFile(                        $namespace . '\Form',                        'Form',                        '\\' . Block::class                    ),                    'Form.css' => '.form{}',                ],            ],            $rootDirectory        );        $formClass   = $namespace . '\Form\Form';        $form        = new $formClass();        $buttonClass = $namespace . '\Button\Button';        $button      = new $buttonClass();        $settings = new Settings();        $settings->addBlocksFolder($namespace, $blocksFolder->url());        $renderer = new Renderer($settings);        $renderer->render($button);        $renderer->render($form);        $this->assertEquals(            "\n/* Button */\n.button{}\n/* Form */\n.form{}",            $renderer->getUsedResources('.css', true)        );    }}

Settings

Вспомогательный класс, основные данные это пути к блокам и их пространства имен

Settings.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks;class Settings{    private array $blockFoldersInfo;    private array $twigArgs;    private string $twigExtension;    private $errorCallback;    public function __construct()    {        $this->blockFoldersInfo = [];        $this->twigArgs         = [            // will generate exception if a var doesn't exist instead of replace to NULL            'strict_variables' => true,            // disable autoescape to prevent break data            'autoescape'       => false,        ];        $this->twigExtension    = '.twig';        $this->errorCallback    = null;    }    public function addBlocksFolder(string $namespace, string $folder): void    {        $this->blockFoldersInfo[$namespace] = $folder;    }    public function setTwigArgs(array $twigArgs): void    {        $this->twigArgs = array_merge($this->twigArgs, $twigArgs);    }    public function setErrorCallback(?callable $errorCallback): void    {        $this->errorCallback = $errorCallback;    }    public function setTwigExtension(string $twigExtension): void    {        $this->twigExtension = $twigExtension;    }    public function getBlockFoldersInfo(): array    {        return $this->blockFoldersInfo;    }    public function getBlockFolderInfoByBlockClass(string $blockClass): ?array    {        foreach ($this->blockFoldersInfo as $blockNamespace => $blockFolder) {            if (0 !== strpos($blockClass, $blockNamespace)) {                continue;            }            return [                'namespace' => $blockNamespace,                'folder'    => $blockFolder,            ];        }        return null;    }    public function getTwigArgs(): array    {        return $this->twigArgs;    }    public function getTwigExtension(): string    {        return $this->twigExtension;    }    public function callErrorCallback(array $errors): void    {        if (! is_callable($this->errorCallback)) {            return;        }        call_user_func_array($this->errorCallback, [$errors,]);    }}
SettingsTest.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocks\Settings;class SettingsTest extends Unit{    public function testGetBlockFolderInfoByBlockClass()    {        $settings = new Settings();        $settings->addBlocksFolder('TestNamespace', 'test-folder');        $this->assertEquals(            [                'namespace' => 'TestNamespace',                'folder'    => 'test-folder',            ],            $settings->getBlockFolderInfoByBlockClass('TestNamespace\Class')        );    }    public function testGetBlockFolderInfoByBlockClassWhenSeveral()    {        $settings = new Settings();        $settings->addBlocksFolder('FirstNamespace', 'first-namespace');        $settings->addBlocksFolder('SecondNamespace', 'second-namespace');        $this->assertEquals(            [                'namespace' => 'FirstNamespace',                'folder'    => 'first-namespace',            ],            $settings->getBlockFolderInfoByBlockClass('FirstNamespace\Class')        );    }    public function testGetBlockFolderInfoByBlockClassIgnoreWrong()    {        $settings = new Settings();        $settings->addBlocksFolder('TestNamespace', 'test-folder');        $this->assertEquals(            null,            $settings->getBlockFolderInfoByBlockClass('WrongNamespace\Class')        );    }}

TwigWrapper

Класс обертка для Twig пакета, обеспечиват работу с шаблонами. Также расширили twig своей функцией _include (которая является оберткой для встроенного include и использует наши поля _isLoaded и _template из метода Block->getTemplateArgs выше) и фильтром _merge (который отличается тем, что рекурсивно сливает массивы).

TwigWrapper.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks;use Exception;use Twig\Environment;use Twig\Loader\FilesystemLoader;use Twig\Loader\LoaderInterface;use Twig\TwigFilter;use Twig\TwigFunction;class TwigWrapper{    private ?LoaderInterface $twigLoader;    private ?Environment $twigEnvironment;    private Settings $settings;    public function __construct(Settings $settings, ?LoaderInterface $twigLoader = null)    {        $this->twigEnvironment = null;        $this->settings        = $settings;        $this->twigLoader      = $twigLoader;        $this->init();    }    private static function GetTwigNamespace(string $namespace)    {        return str_replace('\\', '_', $namespace);    }    // e.g for extend a twig with adding a new filter    public function getEnvironment(): ?Environment    {        return $this->twigEnvironment;    }    private function extendTwig(): void    {        $this->twigEnvironment->addFilter(            new TwigFilter(                '_merge',                function ($source, $additional) {                    return Helper::arrayMergeRecursive($source, $additional);                }            )        );        $this->twigEnvironment->addFunction(            new TwigFunction(                '_include',                function ($block, $args = []) {                    $block = Helper::arrayMergeRecursive($block, $args);                    return $block[Block::TEMPLATE_KEY_IS_LOADED] ?                        $this->render(                            $block[Block::TEMPLATE_KEY_NAMESPACE],                            $block[Block::TEMPLATE_KEY_TEMPLATE],                            $block                        ) :                        '';                }            )        );    }    private function init(): void    {        $blockFoldersInfo = $this->settings->getBlockFoldersInfo();        try {            // can be already init (in tests)            if (! $this->twigLoader) {                $this->twigLoader = new FilesystemLoader();                foreach ($blockFoldersInfo as $namespace => $folder) {                    $this->twigLoader->addPath($folder, self::GetTwigNamespace($namespace));                }            }            $this->twigEnvironment = new Environment($this->twigLoader, $this->settings->getTwigArgs());        } catch (Exception $ex) {            $this->twigEnvironment = null;            $this->settings->callErrorCallback(                [                    'message' => $ex->getMessage(),                    'file'    => $ex->getFile(),                    'line'    => $ex->getLine(),                    'trace'   => $ex->getTraceAsString(),                ]            );            return;        }        $this->extendTwig();    }    public function render(string $namespace, string $template, array $args = [], bool $isPrint = false): string    {        $html = '';        // twig isn't loaded        if (is_null($this->twigEnvironment)) {            return $html;        }        // can be empty, e.g. for tests        $twigNamespace = $namespace ?            '@' . self::GetTwigNamespace($namespace) . '/' :            '';        try {            // will generate ean exception if a template doesn't exist OR broken            // also if a var doesn't exist (if using a 'strict_variables' flag, see Twig_Environment->__construct)            $html .= $this->twigEnvironment->render($twigNamespace . $template, $args);        } catch (Exception $ex) {            $html = '';            $this->settings->callErrorCallback(                [                    'message'  => $ex->getMessage(),                    'file'     => $ex->getFile(),                    'line'     => $ex->getLine(),                    'trace'    => $ex->getTraceAsString(),                    'template' => $template,                ]            );        }        if ($isPrint) {            echo $html;        }        return $html;    }}
TwigWrapperTest.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocks\Block;use LightSource\FrontBlocks\Settings;use LightSource\FrontBlocks\TwigWrapper;use Twig\Loader\ArrayLoader;class TwigWrapperTest extends Unit{    private function renderBlock(array $blocks, string $template, array $renderArgs = []): string    {        $twigLoader = new ArrayLoader($blocks);        $settings   = new Settings();        $twig       = new TwigWrapper($settings, $twigLoader);        return $twig->render('', $template, $renderArgs);    }    public function testExtendTwigIncludeFunctionWhenBlockIsLoaded()    {        $blocks     = [            'form.twig'   => '{{ _include(button) }}',            'button.twig' => 'button content',        ];        $template   = 'form.twig';        $renderArgs = [            'button' => [                Block::TEMPLATE_KEY_NAMESPACE => '',                Block::TEMPLATE_KEY_TEMPLATE  => 'button.twig',                Block::TEMPLATE_KEY_IS_LOADED => true,            ],        ];        $this->assertEquals('button content', $this->renderBlock($blocks, $template, $renderArgs));    }    public function testExtendTwigIncludeFunctionWhenBlockNotLoaded()    {        $blocks     = [            'form.twig'   => '{{ _include(button) }}',            'button.twig' => 'button content',        ];        $template   = 'form.twig';        $renderArgs = [            'button' => [                Block::TEMPLATE_KEY_NAMESPACE => '',                Block::TEMPLATE_KEY_TEMPLATE  => 'button.twig',                Block::TEMPLATE_KEY_IS_LOADED => false,            ],        ];        $this->assertEquals('', $this->renderBlock($blocks, $template, $renderArgs));    }    public function testExtendTwigIncludeFunctionWhenArgsPassed()    {        $blocks     = [            'form.twig'   => '{{ _include(button,{classes:["test-class",],}) }}',            'button.twig' => '{{ classes|join(" ") }}',        ];        $template   = 'form.twig';        $renderArgs = [            'button' => [                Block::TEMPLATE_KEY_NAMESPACE => '',                Block::TEMPLATE_KEY_TEMPLATE  => 'button.twig',                Block::TEMPLATE_KEY_IS_LOADED => true,                'classes'                     => ['own-class',],            ],        ];        $this->assertEquals('own-class test-class', $this->renderBlock($blocks, $template, $renderArgs));    }    public function testExtendTwigMergeFilter()    {        $blocks     = [            'button.twig' => '{{ {"array":["first",],}|_merge({"array":["second",],}).array|join(" ") }}',        ];        $template   = 'button.twig';        $renderArgs = [];        $this->assertEquals('first second', $this->renderBlock($blocks, $template, $renderArgs));    }}

Helper

Вспомогательный класс, содержит лишь пару функций, оставлю без комментариев.

Helper.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks;abstract class Helper{    final public static function arrayFilter(array $array, callable $callback, bool $isSaveKeys): array    {        $arrayResult = array_filter($array, $callback);        return $isSaveKeys ?            $arrayResult :            array_values($arrayResult);    }    final public static function arrayMergeRecursive(array $args1, array $args2): array    {        foreach ($args2 as $key => $value) {            if (intval($key) === $key) {                $args1[] = $value;                continue;            }            // recursive sub-merge for internal arrays            if (                is_array($value) &&                key_exists($key, $args1) &&                is_array($args1[$key])            ) {                $value = self::arrayMergeRecursive($args1[$key], $value);            }            $args1[$key] = $value;        }        return $args1;    }}
HelperTest.php
<?phpdeclare(strict_types=1);namespace LightSource\FrontBlocks\Tests\unit;use Codeception\Test\Unit;use LightSource\FrontBlocks\Helper;class HelperTest extends Unit{    public function testArrayFilterWithoutSaveKeys()    {        $this->assertEquals(            [                0 => '2',            ],            Helper::ArrayFilter(                ['1', '2'],                function ($value) {                    return '1' !== $value;                },                false            )        );    }    public function testArrayFilterWithSaveKeys()    {        $this->assertEquals(            [                1 => '2',            ],            Helper::ArrayFilter(                ['1', '2'],                function ($value) {                    return '1' !== $value;                },                true            )        );    }    public function testArrayMergeRecursive()    {        $this->assertEquals(            [                'classes' => [                    'first',                    'second',                ],                'value'   => 2,            ],            Helper::arrayMergeRecursive(                [                    'classes' => [                        'first',                    ],                    'value'   => 1,                ],                [                    'classes' => [                        'second',                    ],                    'value'   => 2,                ]            )        );    }}

Это был последний класс, теперь можно переходить к демонстрационному примеру.

Демонстрационный пример

Ниже приведу демонстрационный пример использования пакета, с одним чистым css для наглядности, если кому-то интересен пример с scss/webpack смотрите ссылки в конце статьи.

Создаем блоки для теста, пусть это будут Header, Article и Button. Header и Button будут независимыми блоками, Article будет содержкать Button.

Header

Header.php
<?phpnamespace LightSource\FrontBlocksSample\Header;use LightSource\FrontBlocks\Block;class Header extends Block{    protected string $name;    public function loadByTest()    {        parent::load();        $this->name = 'I\'m Header';    }}
Header.twig
<div class="header">    {{ name }}</div>
Header.css
.header {    color: green;    border:1px solid green;    padding: 10px;}

Button

Button.php
<?phpnamespace LightSource\FrontBlocksSample\Button;use LightSource\FrontBlocks\Block;class Button extends Block{    protected string $name;    public function loadByTest()    {        parent::load();        $this->name = 'I\'m Button';    }}
Button.twig
<div class="button">    {{ name }}</div>
Button.css
.button {    color: black;    border: 1px solid black;    padding: 10px;}

Article

Article.php
<?phpnamespace LightSource\FrontBlocksSample\Article;use LightSource\FrontBlocks\Block;use LightSource\FrontBlocksSample\Button\Button;class Article extends Block{    protected string $name;    protected Button $button;    public function loadByTest()    {        parent::load();        $this->name = 'I\'m Article, I contain another block';        $this->button->loadByTest();    }}
Article.twig
<div class="article">    <p class="article__name">{{ name }}</p>    {{ _include(button) }}</div>
Article.css
.article {    color: orange;    border: 1px solid orange;    padding: 10px;}.article__name {    margin: 0 0 10px;    line-height: 1.5;}

Далее подключаем пакет и рендерим блоки, результат вставляем в html код страницы, в шапке выводим использованный css код

example.php
<?phpuse LightSource\FrontBlocks\{    Renderer,    Settings};use LightSource\FrontBlocksSample\{    Article\Article,    Header\Header};require_once __DIR__ . '/vendors/vendor/autoload.php';//// settingsini_set('display_errors', 1);$settings = new Settings();$settings->addBlocksFolder('LightSource\FrontBlocksSample', __DIR__ . '/Blocks');$settings->setErrorCallback(    function (array $errors) {        // todo log or any other actions        echo '<pre>' . print_r($errors, true) . '</pre>';    });$renderer = new Renderer($settings);//// usage$header = new Header();$header->loadByTest();$article = new Article();$article->loadByTest();$content = $renderer->render($header);$content .= $renderer->render($article);$css     = $renderer->getUsedResources('.css', true);//// html?><html><head>    <title>Example</title>    <style>        <?= $css ?>    </style>    <style>        .article {            margin-top: 10px;        }    </style></head><body><?= $content ?></body></html>

в результате вывод будет таким

example.png

Послесловие

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

Понравилась статья? Не забудь проголосовать.

Ссылки:

репозиторий с данным пакетом

репозиторий с демонстрационным примером

репозиторий с примером использования scss и js в блоках (webpack сборщик)

репозиторий с примером использования в WordPress теме (здесь вы также можете увидеть пример расширения класса блока и использования автозагрузки, что добавляет поддержку ajax запросов для блоков)

P.S. Благодарю @alexmixaylov, @bombe и @rpsv за конструктивные комментарии к первой части.

Подробнее..

HTML и CSS ошибки, ухудшающие UX

24.05.2021 10:16:39 | Автор: admin
В прошлом году я собрал несколько случаев, когда HTML и CSS ошибки негативно влияют на доступность интерфейсов. В этой статье я хочу продолжить и описать еще несколько случаев.


Не мучайте пользователей свойствами justify-content и align-items


Когда мы решаем задачи по позиционированию элементов, нам нравится использовать свойства justify-content и align-items. Но мало кто знает, что эти свойства могут провести к мукам пользователя. Особенно часто проблемы связаны с вертикальным позиционированием.

Это связано с особенностями работы свойств, а именно свойства justify-content и align-items не учитывают размеры flex-элементов. Соответственно в случае когда размеры flex-элементов больше размеров flex-контейнера, то flex-элементы будут выходить за его пределы, и могут отображаться некорректно.

Например, я часто не могу закрыть модальное окно, потому что кнопка-крестик вышла за пределы окна и не доступна. Вы можете увидеть этот эффект на изображении ниже.

Эта проблема легко решается с помощью свойства margin со значением auto. Внутри flex-контейнера значение auto будет рассчитано с учетом размера самого контейнера и размеров flex-элементов. Если размеры flex-элементов меньше, чем размеры flex-контейнера, то браузер рассчитает отступ автоматически. А если больше, то элементы прижмутся к границам flex-контейнера.

Не делайте так

<div class="modal">  <div class="modal__main"></div></div>


.modal {  display: flex;  justify-content: center;  align-items: center;}


Можно делать так

<div class="modal">  <div class="modal__main"></div></div>


.modal {  display: flex;}.modal__main {  margin: auto;}




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


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

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

Для это существует дескриптор font-display, который определяет то, как шрифт, подключённый через font face будет отображаться в зависимости от того, загрузился ли он и готов ли к использованию.

У дескриптора есть значение swap, подсказывающее браузеру, что он может использовать безопасный шрифт, пока кастомный полностью не загрузится. А когда он будет загружен, то текстом будет отображен им.

Этим трюком вы поможете вашим пользователям быстрее начать взаимодействовать с интерфейсом, а не ждать отображения текста.

Не делайте так

@font-face {  font-family: "Baloo Tamma";  src: url("balotamma.woff2") format("woff2"),       url("balotamma.woff") format("woff");}


Можно делать так

@font-face {  font-family: "Baloo Tamma";  src: url("balotamma.woff2") format("woff2"),       url("balotamma.woff") format("woff");  font-display: swap;}




Не ломайте SVG иконками интерфейсы


Когда вы используете SVG иконки внутри HTML, обратите внимание, что вам нужно уставить атрибуты width и height. Если вы это не сделаете и установите ширину и высоту через CSS, то ваш интерфейс будет сломан.

В эпоху фреймворков CSS может сработать не сразу. Я не понимаю как это получилось, но я видел приложения на React, в которых cначала отображались огромные иконки, а только потом они принимали нужный размер. Поэтому просто установите размеры через атрибуты width и height, и ваши интерфейсы будут пуленепробиваемыми.

Не делайте так

<svg   xmlns="http://personeltest.ru/away/www.w3.org/2000/svg"  viewBox="0 0 448 512">    <path fill="currentColor" d="..."></path></svg>


svg {  width: 0.875rem;  height: 1rem;}


Можно делать так

<svg   xmlns="http://personeltest.ru/away/www.w3.org/2000/svg"  viewBox="0 0 448 512"  width="0.875rem"  height="1rem">    <path fill="currentColor" d="..."></path></svg>


Не заставляйте ждать пользователей загрузки тяжелых изображения на всех типах устройств


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

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

Например, мы может создать 2 source элемента и определить через медиа-запросы, какое изображение загружать на планшетах, а какое на десктопах. Кроме того, для отображения изображения на мобильных устройствах мы будем использовать элемент img.

Не делайте так

<img   src="ferrari-1920x1080.jpg"  alt="yellow ferrari F8 spider on the background of the ocean">


Можно делать так

<picture>  <source     srcset="ferrari-1200x960.jpg"    media="(min-width: 641px) and (max-width: 1200px)">  <source     srcset="ferrari-1920x1080.jpg"    media="(min-width: 1201px)">   <img     src="ferrari-640x480.jpg"    alt="yellow ferrari F8 spider on the background of the ocean"></picture>

Также мы можем оптимизировать загрузку изображений для retina-экранов. Для этого мы будем использовать дескриптор плотности.

Например, если у смартфона плотность пикселя 2x, то браузер загрузит ferrari-640x480-2x.jpg, а если 1x, то ferrari-640x480-1x.jpg. Такой же механизм сработает для планшетов и десктопных экранов.

Не делайте так

<img   src="ferrari-1920x1080.jpg"  alt="yellow ferrari F8 spider on the background of the ocean">


Можно делать так

<img   src="ferrari-1x.jpg"  srcset="ferrari-2x.jpg 2x"  alt="yellow ferrari F8 spider on the background of the ocean"><!-- или  --><picture>  <source     srcset="ferrari-1200x960-1x.jpg, ferrari-1200x960-2x.jpg 2x"    media="(min-width: 641px) and (max-width: 1200px)">  <source     srcset="ferrari-1920x1080-1x.jpg, ferrari-1920x1080-2x.jpg 2x"    media="(min-width: 1201px)">   <img     src="ferrari-640x480-1x.jpg, ferrari-640x480-2x.jpg 2x"    alt="yellow ferrari F8 spider on the background of the ocean"></picture>


P.S: Если у вас есть вопросы по CSS/HTML, то, не стесняйтесь, пишите мне на мою почту. Она указана в моем профиле на Хабре.
Подробнее..
Категории: Html , Css , Usability , Accessibility , Ux , Front-end

Перевод Продуманный front-end. Правильная архитектура для быстрых сайтов

11.08.2020 10:19:42 | Автор: admin
Привет, Хабр!

Мы давно обходили вниманием темы браузеров, CSS и accessibility и решили вернуться к ней с переводом сегодняшнего обзорного материала (оригинал февраль 2020). Особенно интересует ваше мнение об упомянутой здесь технологии серверного рендеринга, а также о том, насколько назрела необходимость в полноценной книге по HTTP/2 впрочем, давайте обо всем по порядку.


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

Рассмотрим общую архитектуру фронтенда. Как добиться, чтобы в первую очередь загружались важнейшие ресурсы, и довести до максимума вероятность того, что эти ресурсы уже окажутся в кэше?

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

Обзор

Разобьем процесс загрузки приложения на три отдельных этапа:

  1. Первичный рендеринг сколько времени пройдет, прежде, чем пользователь что-то увидит?
  2. Загрузка приложения сколько времени пройдет, прежде, чем пользователь увидит приложение?
  3. Следующая страница сколько времени требуется для перехода на следующую страницу?


Первичный рендеринг

До этапа первичного рендеринга (отрисовки) пользователю попросту ничего не увидеть. Чтобы отобразить страницу, нужен, как минимум, HTML-документ, но в большинстве случаев приходится загружать и дополнительные ресурсы, в частности, файлы CSS и JavaScript. Если они доступны, то браузер может начинать отрисовку на экране.

В этом посте я буду использовать каскадные диаграммы WebPageTest. Каскад запросов для вашего сайта будет выглядеть примерно так.



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

Сокращаем количество запросов, блокирующих рендеринг

Таблицы стилей и (по умолчанию) скриптовые элементы не позволяют отобразить какой-либо контент, расположенный под ними.

Существует несколько способов, позволяющих это исправить:

  • Ставим скриптовые теги в самом низу тега body
  • Асинхронно загружать скрипты при помощи async
  • Внутристрочно записывать небольшие фрагменты JS или CSS, если их требуется загружать синхронно


Избегайте образования цепочек запросов, блокирующих рендеринг

Работа вашего сайта может замедляться не только из-за количества запросов, блокирующих рендеринг. Важнее размер каждого из тех ресурсов, которые нужно скачать, а также, когда именно браузер обнаруживает, что ресурс необходимо загрузить.

Если браузеру станет известно о необходимости загрузки файла только после того, как завершится другой запрос, то у вас может получиться цепочка синхронных запросов. Это может произойти по нескольким причинам:

  • Наличие правил @import в CSS
  • Использование веб-шрифтов, ссылки на которые стоят в CSS-файле
  • Ссылка для инъекции JavaScript или скриптовые теги


Рассмотрим такой пример:



В одном из CSS-файлов на этом сайте содержится правило @import, предназначенное для загрузки шрифта Google. Таким образом, браузер должен выполнять следующие запросы один за другим, в таком порядке:

  1. Document HTML
  2. Application CSS
  3. Google Fonts CSS
  4. Файл Google Font Woff (не показан в каскаде)


Чтобы это исправить, сначала переместим запрос к Google Fonts CSS из @import к ссылочному тегу в HTML-документе. Так мы укоротим цепочку на одно звено.

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

(Помните, что CSS-отклик от Google Fonts зависит от пользовательского агента. Если сделать запрос при помощи IE8, то CSS сошлется на файл EOT (внедряемый OpenType), IE11 получит woff-файл, а современные браузеры woff2. Но, если вас устраивает работать так, как со сравнительно старыми браузерами, использующими системные шрифты, то можно просто скопировать и вставить содержимое CSS-файла.)

Даже после того, как начнется рендеринг страницы, пользователь, возможно, ничего не сможет с ней поделать, так как не отобразится никакого текста до полной загрузки шрифта. Этого можно избежать при помощи свойства font-display swap, которое теперь используется в Google Fonts по умолчанию.

Иногда полностью избавиться от цепочки запросов не удается. В таких случаях попробуйте использовать тег preload или preconnect. Например, сайт, показанный выше, может подключиться к fonts.googleapis.com до того, как фактически будет сделан запрос к CSS.

Переиспользуем серверные соединения для ускорения запросов

Как правило, для установления нового серверного соединения требуется 3 прохода туда-обратно между браузером и сервером:

  1. Поиск DNS
  2. Установление TCP-соединения
  3. Установление SSL-соединения


Как только соединение установлено, требуется еще как минимум 1 проход туда и обратно: отправить запрос и загрузить отклик.

Как показано в нижеприведенном каскаде, соединения инициируются к четырем разным серверам: hostgator.com, optimizely.com, googletagmanager.com и googelapis.com.

Однако при последующих запросах к затронутому серверу можно заново пользоваться уже имеющимся соединением. Поэтому base.css или index1.css загружаются быстро, так как они тоже расположены на hostgator.com.



Уменьшаем размер файла и используем сети доставки контента (CDN)

На длительность запроса, наряду с размером файла, влияют еще два фактора, которые вы контролируете: это размер ресурса и расположение ваших серверов.

Отправляйте пользователю минимально необходимое количество данных, причем, позаботьтесь об их сжатии (напр., при помощи brotli или gzip).

Сети доставки контента (CDN) предоставляют сервера, расположенные в самых разных местах, поэтому велика вероятность, что какой-нибудь из них будет расположен поблизости от ваших пользователей. Вы можете подключать их не к вашему центральному серверу приложений, а к ближайшему серверу в сети CDN. Таким образом, путь данных на сервер и обратно значительно сократится. Это особенно удобно при работе со статическими ресурсами, например, CSS, JavaScript и изображениями, поскольку их легко распределять.

Минуем сеть при помощи сервис-воркеров

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



Разумеется, это работает лишь в случае, когда вам нужно, чтобы сеть просто отправила отклик. Этот отклик уже должен быть кэширован, тем самым вы только облегчите жизнь вашим пользователям, когда они загрузят ваше приложение повторно.

Сервис-воркер, показанный ниже, кэширует HTML и CSS, необходимые для рендеринга страницы. При повторной загрузке приложение пытается выдать кэшированные ресурсы, а если они недоступны обращается к сети в качестве резервного варианта.

self.addEventListener("install", async e => { caches.open("v1").then(function (cache) {   return cache.addAll(["/app", "/app.css"]); });});self.addEventListener("fetch", event => { event.respondWith(   caches.match(event.request).then(cachedResponse => {     return cachedResponse || fetch(event.request);   }) );});


О предзагрузке и кэшировании ресурсов при помощи сервис-воркеров подробнее рассказано в этом руководстве.

Загрузка приложения

Хорошо, наш пользователь уже что-то увидел. Что еще ему потребуется, чтобы он мог пользоваться нашим приложением?

  1. Загрузка приложения (JS и CSS)
  2. Загрузка наиболее важных данных для страницы
  3. Загрузка дополнительных данных и изображений




Обратите внимание: рендеринг может замедляться не только из-за загрузки данных по сети. Когда код будет загружен, браузеру потребуется разобрать, скомпилировать и выполнить его.

Разделение бандла: загружаем только необходимый код и доводим до максимума попадания в кэш

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

Как правило, код состоит из файлов трех различных типов:

  • Код, специфичный для данной страницы
  • Разделяемый код приложения
  • Сторонние модули, которые меняются редко (отлично подходят для кэширования!)


Webpack может автоматически разбивать разделяемый код, чтобы снизить общий вес загрузок, это делается при помощи optimization.splitChunks. Обязательно активируйте фрагмент, отвечающий за время исполнения (runtime chunk), так, чтобы хэши фрагментов оставались стабильными, и можно было с пользой применять долгосрочное кэширование. Иван Акулов написал подробное руководство о разделении и кэшировании кода Webpack.

Разделение кода, специфичного для определенной страницы, не может выполняться автоматически, поэтому вам придется выявить фрагменты, которые можно загружать отдельно. Зачастую это конкретный маршрут или набор страниц. Используйте динамические импорты для ленивой загрузки такого кода.

Разделение бандла приводит к тому, что для полноценной загрузки вашего приложения потребуется сделать больше запросов. Но, если запросы распараллелены, эта проблема невелика, особенно на сайтах, использующих HTTP/2. Обратите внимание на три первых запроса в этом каскаде:



Однако, в этом каскаде также показано 2 запроса, выполняемых последовательно. Эти фрагменты нужны только для данной страницы, и динамически загружаются при помощи вызова import().

Это можно исправить, вставив тег preload link, если вам известно, что эти фрагменты точно понадобятся.



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

Кроме того, использование предзагрузки иногда бывает контрпродуктивным и приводить к задержкам, когда загружаются другие, более важные файлы. Почитайте пост Энди Дэвиса о предзагрузке шрифтов и о том, как заблокировать первичный рендеринг, загрузив сначала шрифты, а потом уже CSS, препятствующие рендерингу.

Загрузка страничных данных

Вероятно, ваше приложение предназначено для показа тех или иных данных. Вот несколько советов о том, как загружать данные заранее и избегать задержек с рендерингом.

Не дожидайтесь бандлов, сразу начинайте загружать данные

Может возникать особый случай цепочки последовательных запросов: вы загружаете бандл приложения, и уже этот код запрашивает страничные данные.

Есть два способа этого избежать:

  1. Встраивать страничные данные в HTML-документ
  2. Начинать запрашивать данные через внутристрочный скрипт, находящийся внутри документа


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

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

В таком случае, либо при выдаче кэшированного HTML-документа при помощи сервис-воркера, можно встроить в HTML внутристрочный скрипт, который будет загружать эти данные. Его можно предоставить в виде глобального промиса, вот так:

window.userDataPromise = fetch("/me")


Затем, если данные уже готовы, ваше приложение может сразу приступать к рендерингу, либо дождаться, пока они будут готовы.

При применении обоих этих методов необходимо знать, какие именно данные должны отображаться на странице, причем, еще до того, как в приложении начнется рендеринг. Обычно это просто обеспечить для тех данных, что касаются пользователя (имя, уведомления) но нелегко при работе с контентом, специфичным для конкретной страницы. Попробуйте сами выделить самые важные страницы и написать для каждой из них собственную логику.

Не блокируйте рендеринг, пока дожидаетесь несущественных данных

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

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



Избегайте случаев, в которых возникают цепочки последовательных запросов к данным

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

Вместо того, чтобы сначала сделать запрос о том, что за пользователь вошел в систему, а потом запрашивать список групп, к которым относится этот пользователь, возвращайте список групп вместе с информацией о пользователе. Для этого можно использовать GraphQL, но собственная конечная точка user?includeTeams=true также отлично подойдет.

Рендеринг на стороне сервера

В данном случае имеется в виду заблаговременный рендеринг приложения на сервере, так, чтобы в качестве отклика на запрос, поступивший от документа, подавалась полноценная HTML-страница. Таким образом, клиент может увидеть всю страницу целиком, не дожидаясь загрузки дополнительного кода или данных!

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

Используйте серверный рендеринг, если видите, что неинтерактивный контент ценен сам по себе. Также такой подход помогает кэшировать на сервере тот HTML, который был там отображен, после чего передавать его всем пользователям без задержки при первичном запросе документа. Например, серверный рендеринг отлично подойдет в случае, когда вы отображаете блог при помощи React.

Почитайте эту статью Михала Янашека; в ней хорошо описано, как комбинировать сервис-воркеры с рендерингом на стороне сервера.

Следующая страница

В какой-то момент пользователю, работающему с вашим приложением, потребуется перейти на следующую страницу. Когда открыта первая страница, вы контролируете все, что происходит в браузере, поэтому можете подготовиться к следующему взаимодействию.

Предварительная выборка ресурсов

Если вы предзагрузите код, нужный для отображения следующей страницы, это поможет избежать задержек при пользовательской навигации. Используйте теги prefetch link или webpackPrefetch для динамических импортов:

import(    /* webpackPrefetch: true, webpackChunkName: "todo-list" */ "./TodoList")


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

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

Переиспользуйте те данные, что уже загружены

Локально кэшируйте данные Ajax в вашем приложении, чтобы впоследствии обойтись без лишних запросов. Если пользователь переходит к списку групп на странице Редактировать группу, такой переход можно сделать мгновенным, повторно использовав данные, уже выбранные ранее.

Обратите внимание: это не сработает, если ваш объект часто редактируют другие пользователи, а данные, которые вы загрузили, могут быстро устареть. В таких случаях попробуйте сначала показывать имеющиеся данные в режиме только для чтения, а тем временем выбирать обновленные данные.

Заключение

В этой статье мы рассмотрели ряд факторов, которые могут замедлять работу страницы на различных этапах процесса загрузки. Пользуйтесь такими инструментами как Chrome DevTools, WebPageTest и Lighthouse, чтобы определить, какие из советов актуальны в вашем приложении.

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

Работая над этой статьей, я осознал, что разделяю глубоко укоренившееся убеждение, будто множество отдельных запросов это плохо с точки зрения производительности. Это было актуально в прошлом, когда для каждого запроса требовалось отдельное соединение, а браузеры допускали лишь несколько соединений на домен. Но такая проблема исчезла с появлением HTTP/2 и современных браузеров.

Есть серьезные аргументы в пользу дробления запросов. Поступая так, можно загружать строго необходимые ресурсы и более рационально использовать кэшированный контент, поскольку перезагружать нужно лишь те файлы, которые изменились.
Подробнее..

Перевод Создание блога с помощью Nuxt Content(часть первая)

11.10.2020 18:05:36 | Автор: admin

Создание блога на Nuxt Content


От переводчика: Я собирался сделать собственную статью по Nuxt Content, но наткнулся на готовую статью, которая отлично раскрывает тему. Лучше у меня вряд ли получится, поэтому я решил перевести. Написал автору в твиттер и практически сразу получил согласие. Статья будет с моими дополнениями для лучшего понимания темы.



Модуль Content в Nuxt это headless CMS основанной на git файловой системе, которая предоставляет мощные функции для создания блогов, документации или просто добавления контента на обычный сайт. В этой статье мы разберем большинство преимуществ этого модуля и узнаем как создать блог с его помощью.


Видео обзор готового проекта:


Your browser does not support HTML5 video.

Посмотреть Демо /
Код проекта



Начало работы


Установка


Чтобы начать работу с модулем Content, нам сначала нужно установить модуль с помощью npm или yarn.


yarn add @nuxt/content

npm install @nuxt/content

Затем мы добавим его в сборку модулей в файле nuxt.config.


export default {  modules: ['@nuxt/content']}

Если вы создаете новый проект с помощью create-nuxt-app, можете выбрать опцию добавить модуль Content, и он будет установлен.

Создаем страницу


Модуль Content читает файлы в нашем каталоге content/.


mkdir content

Если вы создали свой проект с помощью create-nuxt-app, каталог content/ будет уже создан.

Давайте создадим директорию articles/, куда мы сможем добавлять статьи для нашего блога.


mkdir content/articles

Модуль Content может анализировать markdown, csv, yaml, json, json5 или xml файлы. Давайте создадим нашу первую статью в markdown файле:


touch content/articles/my-first-blog-post.md

Теперь добавим заголовок и текст для нашего сообщения в блоге:


# My first blog postWelcome to my first blog post using content module

В markdown мы создаем заголовок <h1> с помощью значка #. Убедитесь, что вы оставили пробел между ним и заголовком вашего блога. Для получения дополнительной информации о записи в markdown стиле смотрите Руководство по основному синтаксису.

Отображение контента


Чтобы отобразить контент на странице, мы используем динамическую страницу, добавив к странице знак подчеркивания (_). Создав компонент страницы с именем _slug.vue внутри папки blog, мы можем использовать переменную params.slug, предоставляемую vue router, для получения имени каждой статьи.


touch pages/blog/_slug.vue

Затем используем asyncData в компоненте страницы для получения содержимого статьи до того, как страница будет отрисована. Мы можем получить доступ к контенту через context, используя переменную $content. Поскольку мы хотим получить динамическую страницу, нам также необходимо знать, какую статью нужно получить с помощью params.slug, который доступен нам через context.


<script>  export default {    async asyncData({ $content, params }) {      // fetch our article here    }  }</script>

Внутри асинхронной функции asyncData мы создаем переменную с именем article, которая принимает контент, используя await, за которым следует $content. Нужно передать в $content параметры того, что мы хотим получить, в нашем случае это папка articles и slag, который мы получаем из params. По цепочке в конце добавляем метод fetch, который возвращает нужную статью.


<script>  export default {    async asyncData({ $content, params }) {      const article = await $content('articles', params.slug).fetch()      return { article }    }  }</script>

Чтобы отобразить контент, используем компонент <nuxt-content />, передав переменную в параметр document. В этом примере мы заключили его в HTML тег article, согласно правилам семантического синтаксиса, но вы можете использовать div или другой тег HTML, если хотите.


<template>  <article>    <nuxt-content :document="article" />  </article></template>

Теперь мы можем запустить сервер разработки и перейти по маршруту http://localhost:3000/blog/my-first-blog-post. Мы должны увидеть контент из .md файла.


статья из файла my-first-blog-post.md


Введенные переменные по умолчанию


Модуль Content Nuxt дает нам доступ к введенным переменным, которые мы можем показать в нашем шаблоне. Давайте посмотрим на переменные по умолчанию, которые вводятся в документ:


  • body: содержимое документа
  • dir: директория
  • extension: расширение файла (.md в этом примере)
  • path: путь к файлу
  • slug: имя файла
  • toc: массив, содержащий оглавление
  • createdAt: дата создания файла
  • updatedAt: дата последнего изменения файла

Мы можем получить доступ ко всем этим переменным, используя созданную ранее переменную article. Article это объект, который содержит все эти дополнительные введенные переменные, к которым у нас есть доступ. Давайте проверим их, распечатав с помощью тега <pre>.


<pre> {{ article }} </pre>

Теперь на нашей странице мы видим, что у нас есть объект с переменной, которая представляет собой пустой массив, и переменную содержимого(body), которая включает все наши теги h1 и p, а также некоторую другую информацию, которую мы рассмотрим позже. Если мы прокрутим вниз, вы увидите все остальные переменные, к которым есть доступ.


"dir": "/articles","path": "/articles/my-first-blog-post","extension": ".md","slug": "my-first-blog-post","createdAt": "2020-06-22T10:58:51.640Z","updatedAt": "2020-06-22T10:59:27.863Z"

Это означает, что мы можем получить доступ к этим свойствам, используя нашу переменную article, добавив точку и имя свойства, которую мы хотим использовать. Например, article.updatedAt даст нам дату последнего обновления поста.


<p>Post last updated: {{ article.updatedAt }}</p>

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


methods: {    formatDate(date) {      const options = { year: 'numeric', month: 'long', day: 'numeric' }      return new Date(date).toLocaleDateString('en', options)    } }

А затем в нашем шаблоне мы можем использовать метод formatDate, принимающий дату, которую мы получаем из контента, и возвращающий уже отформатированную дату.


<p>Article last updated: {{ formatDate(article.updatedAt) }}</p>

Пользовательские введенные переменные


Мы также можем добавить пользовательские введенные переменные, добавив блок YAML в наш md файл. Он должен находиться в верхней части файла, иметь допустимый формат YAML и находиться между двумя тройными пунктирными линиями. Это полезно для добавления переменных SEO, таких как заголовок, описание и изображение вашей статьи.


---title: My first Blog Postdescription: Learning how to use @nuxt/content to create a blogimg: first-blog-post.jpgalt: my first blog post---

Теперь у нас есть переменные title, description, img и alt, к которым у нас есть доступ из объекта article`.


<template>  <article>    <h1>{{ article.title }}</h1>    <p>{{ article.description }}</p>    <img      :src="article.image"      :alt="article.alt"    />    <p>Article last updated: {{ formatDate(article.updatedAt) }}</p>    <nuxt-content :document="article" />  </article></template>

Чтобы отрендерить изображения, включенные в YAML разделе файла, нам нужно либо поместить их в статическую папку, либо использовать синтаксис:
:src="require(`~/assets/images/${article.image}`)".
Изображения, включенные в содержимое статьи, всегда следует помещать в папку static, поскольку @nuxt/content не зависит от Webpack. Эта папка не пропускается через Webpack, в отличие от папки assets.

Стилизация markdown контента


Если мы посмотрим на код получившейся страницы, мы увидим, что все, что написано внутри нашего файла, заключено в div с классом nuxt-content. Это означает, что мы можем легко добавить стили ко всем нашим элементам из нашего markdown файла, заключив их в класс nuxt-content.


<style>  .nuxt-content h2 {    font-weight: bold;    font-size: 28px;  }  .nuxt-content h3 {    font-weight: bold;    font-size: 22px;  }  .nuxt-content p {    margin-bottom: 20px;  }</style>

Чтобы использовать стили с ограниченной областью видимости с классом nuxt-content, вам необходимо использовать deep селектор: /deep/, ::v-deep или >>>


Все остальные данные, которые поступают из YAML раздела, можно оформить как обычно: используя TailwindCSS или добавив в CSS в стиль тега.


Наши теги из md файла преобразуются в правильные теги, что означает, что теперь у нас есть два заголовка, два тега <h1>. Удалим один из md файла.


Добавление иконки к ссылке наших заголовков


Обратите внимание, что внутри тега <h2> есть тег <a> с href, который содержит id для ссылки на себя, и тег span внутри него с icon и icon-link классы. Это полезно для ссылки на этот раздел страницы. Ссылки в заголовках пусты и поэтому скрыты, поэтому давайте добавим им стиль. Используя классы значков, мы можем добавить svg-иконки в качестве фонового изображения для нашего значка. Сначала вам нужно будет добавить сами иконки в папку с ресурсами assets. В этом примере я добавила его в папку svg и взяла иконки Steve Schoger's Hero Icons.


.icon.icon-link {  background-image: url('~assets/svg/icon-hashtag.svg');  display: inline-block;  width: 20px;  height: 20px;  background-size: 20px 20px;}

Добавляем оглавление


Сгенерированная переменная toc позволяет нам добавить оглавление к нашему посту в блоге. Давайте добавим заголовки к нашему сообщению в блоге.


## This is a headingThis is some more info## This is another headingThis is some more info

Теперь мы можем видеть эти новые заголовки внутри массива toc с идентификатором, глубиной и текстом. Значение глубины является значением тега заголовка, поэтому значение глубины 2 приравнено тегу <h2> и равно 2, значение 3 тегу<h3> и т. д.


## This is a headingThis is some more info### This is a sub headingThis is some more info### This is another sub headingThis is some more info## This is another headingThis is some more info

Поскольку у нас есть доступ к toc и тексту, мы можем перебрать и отобразить их все, а в компоненте <NuxtLink> сделать ссылку на якорь раздела, на который мы хотим создать ссылку.


<nav>  <ul>    <li v-for="link of article.toc" :key="link.id">      <NuxtLink :to="`#${link.id}`">{{ link.text }}</NuxtLink>    </li>  </ul></nav>

Теперь ссылки ToC работают, и нажатие на любую из них приведет нас к нужной части документа. Модуль Content автоматически добавляет идентификатор и ссылку к каждому заголовку. Если мы проверим один из заголовков из нашего .md файла в инструментах разработки браузера, мы увидим, что у нашего тега <h2> есть идентификатор. Это тот же идентификатор, который находится в toc, который по сути из него и берется для ссылки на правильный заголовок.


Мы можем улучшить верстку дальше, используя динамические классы для стилизации классов заголовков в зависимости от глубины заголовка, которую мы можем добавить в наш тег nuxt-link. Если ссылка имеет глубину 2, добавьте отступ по оси y, а если глубина равна 3, добавьте поле слева и отступ внизу. Здесь мы используем классы TailwindCSS, но, конечно же, можно использовать собственные имена и стили классов.


:class="{ 'py-2': link.depth === 2, 'ml-2 pb-2': link.depth === 3 }"

Использование HTML в .md файлах


Иногда нам может понадобиться добавить HTML в наши файлы c разметкой. Давайте добавим div с некоторыми классами, чтобы он имел синий цвет фона с белым текстом, небольшим отступом и нижним краем.


<div class="bg-blue-500 text-white p-4 mb-4">  This is HTML inside markdown that has a class of note</div>

Добавление Vue компонента


А также мы можем добавлять Vue компоненты в .md файлы. Это означает, что если мы множество раз используем такие компоненты, как информационное окно или окно предупреждения, мы можем создать его с нужными нам стилями и передать текст в слот.


Теперь мы можем добавлять компоненты в наше приложение, установив для свойства components значение true в нашем файле nuxt.config. (начиная с v2.13)


export default {  components: true}

Автоматический импорт компонентов не будет работать для <nuxt-content>, если мы не зарегистрируем их глобально, добавив глобальную папку внутри папки компонентов.


mkdir components/global

А теперь можно создать наш компонент InfoBox внутри этой папки.


<template>  <div class="bg-blue-500 text-white p-4 mb-4">    <p><slot name="info-box">default</slot></p>  </div></template>

Теперь в нашей разметке эти компоненты будут доступны без необходимости их импорта.


<info-box>  <template #info-box>    This is a vue component inside markdown using slots  </template></info-box>

Глобальные компоненты будут доступны для всего нашего приложения, поэтому будьте осторожны при добавлении компонентов в эту папку. Это работает иначе, чем добавление компонентов в папку components, которые добавляются (наверное, имеется в виду импортируются прим. пер.) только в том случае, если они используются (начиная с Nuxt v2.13 компоненты в папке components импортируются автоматически, достаточно написать в Nuxt конфиге: components: true прим. пер.).



От переводчика: На этом первая часть статьи подошла к концу. Дебби познакомила нас с мощным инструментом от создателей Nuxt'а, который они, кстати, сами используют на своем сайте для документации фреймворка. В этом, в общем-то, основное его применение. Если вы думали, что наконец-то нашли простую CMS, на которой можно быстро шлепать проекты и отдавать заказчику, то это не так. Из-за git-подобной системы наполнения контента, проект должен всегда быть под контролем разработчиков. Она идеальна для документации инструментов разработки, и любого другого контента не требующего частого обновления. Если же контент должен динамически обновляться из-за действий пользователей, то тут без базы данных не обойтись.


В следующей части мы узнаем как стилизовать код в статьях, сортировать статьи по различным параметрам, работать с API Content и многое другое.


Продолжение следует...

Подробнее..

Vue 3 на Typescript

07.11.2020 14:06:38 | Автор: admin

vue3+ts


Популярность Typescript растет день ото дня. Javascript нетипизированный язык(или слабо типизированный, если точнее), и одна и та же переменная способна принимать и строку, и число, и даже объект. С одной стороны, это делает язык гибким, с другой, потенциально ведет к многочисленным ошибкам. Typescript создан, чтобы решить эту проблему. Vue старается не отставать от моды, и в новой версии фреймворка была значительно улучшена поддержка языка. Теперь переход на Typescript проще и приятнее, чем был раньше. Хороший повод научиться чему-то новому, тем более, что в требованиях к вакансиям он встречается все чаще и чаще.


В этой статье мы перепишем тестовое задание, которое я разбирал ранее, на Vue 3 и Typescript и вдобавок используем обновленные Vue-Router и Vuex(критики, вы были услышаны).


Для тех, кто забыл, что представляет из себя техническое задание: освежить память можно здесь и здесь.


Все вопросы связанные с тем, как приступить к этому заданию были разобраны в предыдущей статье, поэтому без лишних слов к делу!


Для начала обновите ваш Vue CLI. Я знаю, что вы давно его не обновляли, а там много полезных фич завезли, в том числе улучшенную поддержку Typescript.


npm update -g @vue/cli

Теперь откройте папочку, где вы храните свои проекты и наберите в консоли:


vue create tsvue-todo-app

Из появившихся вариантов установки выбираем третий вариант: Manually select features. Далее обязательно помечаем звездочками следующие варианты:


  • (*) TypeScript
  • (*) Router
  • (*) Vuex

Выбираем версию Vue для проекта: 3.x (Preview)


Все остальные опции как рекомендовано.


Ждем установки проекта


cd tsvue-todo-app

Открываем любимым редактором проект.


Удаляем все файлы из папок components, assets и views. Очищаем файл App.vue от кода.


Для начала займемся нашим хранилищем, которое находится в папке store. Его, как и все приложение, мы напишем на typescript. Моя статья, если это не было понятно из предисловия, рассчитана на тех, кто хорошо знаком с Vue и javascript, но почти ничего не знает о typescript.


Несколько слов о typescript. Его можно воспринимать как обертку или препроцессор для джаваскрипта, который проверяет код на соблюдение строгой типизации, то есть, что переменные, функции и объекты принимают только те типы данных, для которых и были задуманы, в остальном это обычный javascript. Соблюдена обратная совместимость, то есть вы можете писать на ts, как на js, и ваш код будет работать. Браузер, конечно же, не понимает typescript, для этого его нужно компилировать в javascript, который в итоге и идет в продакшен. К счастью, вам не нужно об этом беспокоиться, Vue CLI настроил все для вас. Достаточно запомнить особенности создания компонентов на typescript во Vue, для этого есть отличное руководство от самих создателей фреймворка(пока что доступно только на английском), и выучить типизацию данных в typescript, которая во Vue имеет свои особенности.

Существует множество способов создания типов в typescript. С полным спектром возможностей для наследования и инкапсуляции, которые так любят сторонники строго типизированных языков(таких как C# и Java). Мы пойдем по простому пути. Используем interface для создания модели объектов дел и списков дел. Создадим папку models в папке scr. Добавим файл ToDoModel.ts с кодом:


export default interface ToDo {  text: string;  completed: boolean;}

Теперь добавим файл NoteModel.ts:


import ToDo from './ToDoModel';export default interface Note {  title: string;  todos: Array<ToDo>;  id: number;}

Обратите внимание, что типы примитивов пишутся с маленькой буквы, они хоть и похожи, но отличаются от собратьев из js. Строка todos: Array<ToDo> означает массив, содержащий элементы типа ToDo, который мы импортировали из ToDoModel.ts


Теперь мы можем использовать эти модели в нашем приложении. Сначала займемся хранилищем. Оно должно находиться в src/store/index.ts. В первую очередь я советую установить строгий режим для хранилища. Дело в том, что данные в нем реактивные, как и во Vue, то есть, если вы просто используете их в компонентах, они будут автоматически меняться, в следствии локальных действий над ними. Это приведет к неразберихе в больших приложениях со множеством данных. Теперь мутации хранилища возможны только в самом хранилище, в противном случае будут вылетать ошибки. Импортируем наши модели. Создаем две переменные notes: [] as Note[],, будет содержать все заметки со списками задач(сразу определяем его тип с помощью typescript как массив записей), и currentNote, для создания/редактирования выбранной заметки. Создаем "небольшую" CRUD модель для них, и вот что должно получиться:


import { createStore } from 'vuex'import Note from '@/models/NoteModel'import ToDo from "@/models/ToDoModel"export default createStore({  state: {    notes: [] as Note[],    currentNote: {      title: "",      todos: [] as ToDo[],      id: 0    } as Note  },  mutations: {    addNote(state) {      state.notes.push(JSON.parse(JSON.stringify(state.currentNote)))    },    deleteNote(state) {      state.notes = state.notes.filter(note => note.id != state.currentNote.id)    },    updateNote(state) {      let note = state.notes.find(note => note.id === state.currentNote.id)      let index = state.notes.indexOf(note as Note)      state.notes[index] = JSON.parse(JSON.stringify(state.currentNote))    },    setCurrentNote(state, payload: Note) {      state.currentNote = JSON.parse(JSON.stringify(payload))    },    updateTitle(state, payload: string) {      state.currentNote.title = payload    },    updateTodos(state, payload: ToDo[]) {      state.currentNote.todos = payload    },    addNewTodo(state) {      state.currentNote.todos.push({        text: "",        completed: false      })    },    deleteTodo(state, index: number) {      state.currentNote.todos.splice(index, 1)    }  },  actions: {    saveNote({ commit }) {      const isOldNote: boolean = this.state.notes.some(el => el.id === this.state.currentNote.id)      if (isOldNote) {        commit('updateNote')      }      else {        commit('addNote');      }    },    fetchCurrentNote({ commit }, noteId: number) {      let note = this.state.notes.find(note => note.id === noteId)      commit('setCurrentNote', note)    },    updateCurrentNote({ commit }, note: Note) {      commit('setCurrentNote', note)    },  },  getters: {    getIdOfLastNote(state): number {      if (state.notes.length > 0) {        const index = state.notes.length - 1        return state.notes[index].id      } else {        return 0      }    }  },  strict: true})

Как видите, нет большой разницы с написанием кода, кроме того, что теперь всем переменным и входящим параметрам указывается тип, а для создания хранилища требуется импортировать функцию createStore. currentNote будет использоваться для хранения заметки, которую редактируют в настоящий момент, при создании новой она будет пустой, при редактировании старой будем получать нужную запись из массива записей notes.


А теперь перейдем к созданию первого компонента. Добавим файл ToDoItem.vue в папку components. Он нам пригодится для компонента создания/редактирования записи, и будет отвечать за отдельное дело.


<template>  <li>    <div>      <input v-model="checked" type="checkbox" />    </div>    <div>      <span        :class="{ completed: todo.completed }"        v-if="!editable"        @click="editable = !editable"      >        {{ todo.text ? todo.text : "Click to edit Todo" }}      </span>      <input        v-else        type="text"        :value="todo.text"        @input="onTextChange"        v-on:keyup.enter="editable = !editable"      />    </div>    <div>      <button @click="editable = !editable">        {{ editable ? "Save" : "Edit" }}      </button>      <button @click="$emit('remove-todo', todo)">Delete</button>    </div>  </li></template><script lang="ts">  import { defineComponent, PropType } from "vue"  import ToDo from "@/models/ToDoModel"  export default defineComponent({    name: "TodoItem",    props: {      todo: {        type: Object as PropType<ToDo>,        required: true      }    },    data() {      return {        editable: false      }    },    methods: {      onTextChange(e: { target: { value: string } }) {        this.$emit("update-todo", e.target.value)      }    },    computed: {      checked: {        get(): boolean {          return this.todo.completed        },        set(value: boolean) {          this.$emit("checkbox-click", value)        }      }    }  })</script><style scoped>  .completed {    text-decoration: line-through;  }</style>

Обратите внимание, что Vue компонент на typescript создается с помощью команды defineComponent. Всё внутреннее Options API должно быть вам знакомо. Единственно, что возникает путаница, при типизации props компилятор думает, что происходит операция присваивания. Нам пригодится объект PropType, который импортируется также из vue, он служит для указания типа объекта в typescript. В сложных ситуациях можно на месте указывать какое строение принимаемого объекта мы ожидаем onTextChange(e: { target: { value: string } }). Конечно, еще можно присваивать тип any, тогда переменная будет принимать все типы данных, но это убивает весь смысл typescript.


Строка this.$emit("update-todo", e.target.value) это просто вызов функции, которую передадут из внешнего компонента, и передача ей параметра e.target.value. Во внешней функции это будет выглядеть так:


      <TodoItem        <?--... -->        @update-todo="onUpdateTodo($event, index)"      />

Далее нам предстоит создание компонента Note.vue. Для интереса используем новый Composition API и функцию setup, все так же на typescript. Это усложняет работу, так как придется импортировать кучу вещей из vue, вот так:
import { defineComponent, computed, onMounted } from "vue". Наши $store и $router не будут доступны в функции setup, для доступа к ним их тоже придется импортировать. Мой обзор Composition API и работу с setup можно прочитать в моей прошлой статье. Напомню только, что функция setup вызывается до создания компонента и после получения props, хуки created и beforeCreate в ней не нужны, фактически это новый beforeCreate/created, все что вы писали в этих хуках, теперь нужно писать в setup. Что делает компонент Note.vue? Он используется для создания новой заметки и редактирования старых, для начала он связывает currentNote из хранилища с переменной note, потом он проверяет роута, если в нем присутствует :id, он просит хранилище найти нужную заметку в массиве заметок и записать её в currentNote, если нет, то устанавливает пустую заметку. А так же он содержит все логику для редактирования и сохранения заметок и отдельных дел.


<template>  <div>    <input :value="note.title" @input="updateTitle" />    <h2>{{ note.title }}</h2>    <hr />    <ul>      <TodoItem        v-for="(todo, index) in note.todos"        :todo="todo"        :key="index"        @remove-todo="onRemoveTodo(index)"        @update-todo="onUpdateTodo($event, index)"        @checkbox-click="onCheckboxClick($event, index)"      />    </ul>    <div class="new-todo">      <button @click="addNewTodo">        Add Todo      </button>      <span @click="addNewTodo">Add New Todo</span>    </div>    <hr />    <div>      <button @click="saveNote">        Save      </button>      <button @click="cancelEdit">        Cancel      </button>      <button @click="DeleteNote">        Delete      </button>    </div>    <hr />  </div></template><script lang="ts">  import TodoItem from "@/components/ToDoItem.vue"  import { defineComponent, computed, onMounted } from "vue"  import Note from "@/models/NoteModel"  import ToDo from "@/models/ToDoModel"  import store from "@/store"  import router from "@/router"  export default defineComponent({    name: "Note",    components: {      TodoItem    },    setup() {      const note = computed(() => store.state.currentNote)      const saveNote = () => {        store.dispatch("saveNote")        router.push("/")      }      const DeleteNote = () => {        store.commit("deleteNote", note)        router.push("/")      }      const { currentRoute } = router      const fetchNote = () => {        if (currentRoute.value.params.id) {          const routeId: number = +currentRoute.value.params.id          store.dispatch("fetchCurrentNote", routeId)        } else {          const id = store.getters.getIdOfLastNote + 1          store.commit("setCurrentNote", {            title: "",            todos: [] as ToDo[],            id: id          })        }      }      onMounted(fetchNote)      const updateTitle = (e: { target: { value: string } }) => {        store.commit("updateTitle", e.target.value)      }      const addNewTodo = () => {        store.commit("addNewTodo")      }      const onRemoveTodo = (index: number) => {        store.commit("deleteTodo", index)      }      const onUpdateTodo = (text: any, index: any) => {        let todos = JSON.parse(JSON.stringify(store.state.currentNote.todos))        todos[index].text = text        store.commit("updateTodos", todos)      }      const onCheckboxClick = (value: boolean, index: number) => {        let todos = JSON.parse(JSON.stringify(store.state.currentNote.todos))        todos[index].completed = value        store.commit("updateTodos", todos)      }      const cancelEdit = () => {        if (currentRoute.value.params.id) {          // undo changes        } else {        }        router.push("/")      }      const clearNote = () => {        const id = store.getters.getIdOfLastNote + 1        store.commit("setCurrentNote", {          title: "",          todos: [] as ToDo[],          id: id        } as Note)      }      return {        note,        saveNote,        addNewTodo,        cancelEdit,        onRemoveTodo,        onUpdateTodo,        DeleteNote,        clearNote,        updateTitle,        onCheckboxClick      }    },    beforeRouteLeave(to, from, next) {      this.clearNote()      next()    }  })</script><style>  .new-todo {    display: flex;    justify-content: flex-start;    background-color: #e2e2e2;    height: 36px;    margin: 5px 0px;    padding-top: 4px;    padding-left: 10px;    padding-right: 15px;    border-radius: 5px;  }</style>

Теперь логику компонента с помощью setup можно группировать для удобства чтения. Красота!


Вне функции setup вы заметили следующую функцию:


    beforeRouteLeave(to, from, next) {      this.clearNote()      next()    }

Это хук роутера, который вызывается каждый раз, когда юзер покидает страницу компонента. У нас он вызывает функцию "очистки" заметки.


Конечно, все это не будет работать без роутера. Вот какой код следует написать в \router\index.ts:


import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'import List from '@/views/List.vue'import Note from '@/views/Note.vue'const routes: Array<RouteRecordRaw> = [  {    path: '/',    name: 'List',    component: List  },  {    path: '/note',    name: 'Create',    component: Note  },  {    path: '/note/:id',    name: 'Edit',    component: Note  }]const router = createRouter({  history: createWebHistory(process.env.BASE_URL),  routes})export default router

А так же отредактируем файл App.vue:


<template>  <div id="app">    <nav>      <router-link class="router-link" to="/" exact>List Of Notes</router-link>      <router-link class="router-link" to="/note" exact        >Create Note</router-link      >    </nav>    <hr />    <router-view />  </div></template><script>  export default {    name: "App"  }</script>

И добавим файл List.vue в папку views:


<template>  <h2>List of Notes</h2>  <hr />  <div v-for="note in notes" :key="note.id" :to="`/note/${note.id}`">    <h2>{{ note.title }}</h2>    <h3>{{ note.id }}</h3>    <ul>      <li        :class="todo.completed ? 'completed' : ''"        v-for="(todo, index) in note.todos"        :key="index"      >        {{ todo.text }}      </li>    </ul>    <button @click="$router.push(`/note/${note.id}`)">Go to note</button>    <hr />  </div></template><script>  export default {    computed: {      notes() {        return this.$store.state.notes      }    }  }</script><style>  .completed {    text-decoration: line-through;  }</style>

Теперь все должно работать. Если вы запутались, то можно клонировать весь проект отсюда.




Это не все тонкости работы с typescript на vue. Рекомендую также прочитать официальную документацию.


Конечно, наш проект далек от завершения. Внешний вид убогий(отсутствуют стили) и необходимый функционал: кеширование записей, модальные окна-предупреждения, кнопки redo\undo для истории редактирования. К счастью, большая часть этих проблем решается плагинами. А настоящий фронтендер, познавший дзен разработки, знает, что для решения любой сложной задачи, есть свой npm-пакет. Этим мы и займемся в следующей статье.

Подробнее..

Перевод Brython заменяем JavaScript на Python на фронтенде

11.12.2020 10:08:16 | Автор: admin
Привет, Хабр!

К огромному удовлетворению нашей читательской аудитории, наша работа над обновлением книг по Python не прекращается. Но не прекращается и поиск в этом направлении и сегодня мы хотели бы упомянуть Brython Python для браузеров. Статья короткая, немного игривая и детективная, мы постарались сохранить авторский стиль.



В этой статье дается краткое введение в работу с Brython, реализацией Python для разработки на фронтенде (в браузере).

Весь проект выложен здесь.

Введение


Завидуя успеху программистов JavaScript, питонисты-заговорщики тайно встретились, чтобы обсудить будущее Python в этом апокалиптическом мире. Повсюду JavaScript, отжирающий поляну у Python. Вооружившись Node.js, язык JavaScript вторгся на территорию Python, и тот утратил свою доминирующую роль всеми любимого серверного языка, где ранее соперничал с Ruby (помните те времена?). Тогда пришло время сделать вылазку в самое сердце территории JavaScript: в браузер.

Не забывайте вашу историю (и помните о будущем)


Эта дилемма волновала не только вышеупомянутых заговорщиков. Был еще один рыцарь плаща и кинжала, автор Transcrypt. Он решил написать компилятор для Python, компилирующий код прямо в JavaScript Как хороший отравитель, он не оставлял после себя и следа Python. Выглядело это многообещающе.

Другие предпочитали воспользоваться уроками истории. Просто иммигрировать всей семьей. По крайней мере, именно так мыслили создатели Pyodide. Они собирались создать на стороне JavaScript анклав с полноценным интерпретатором Python, который мог бы выполнять код на Python. Соответственно, там можно было гонять любой код Python, в том числе, большую часть его стека для data science, где есть привязки к языку C (например, Numpy, Pandas).

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

Тогда заговорщики поступили именно в духе хороших заговорщиков: создали другой компилятор для преобразования Python в JavaScript, но на этот раз выполнять компиляцию в JavaScript при загрузке страницы (а не как Transcrypt, компилирующий код в JavaScript заранее). Так сформировалось Братство Brython. Одна змея, чтоб править ими всеми.

Hello World


Давайте напишем традиционный Hello World

А вот и десант Brython (это я о компиляторе).

<script type="text/javascript"       src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/brython@3.8.9/brython.min.js"></script>Активируем его при загрузке страницы<body onload="brython()">...</body>


В теге body, показанном выше, мы пишем код на Brython:

<script type="text/python">from browser import documentdocument <= "Hello World"</script>


Просто добавляем Hello World в элемент document. Хм. Очень легко.

В полном виде ниже.

<!doctype html><html><head>    <meta charset="utf-8">    <script type="text/javascript"        src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/brython@3.8.8/brython.min.js">    </script></head><body onload="brython()"><script type="text/python">from browser import documentdocument <= "Hello World"</script></body></html>


В таком случае на страницу будет просто выведено приветствие Hello World.

Калькулятор


Давайте напишем калькулятор на Brython. Весь его код выложен здесь.



Да, вы догадались, нам понадобится таблица. Давайте ее сделаем.

from browser import document, htmlcalc = html.TABLE()


Добавим только первый ряд. То есть отобразим поле для чисел (назовем его result) и клавишу C.

calc <= html.TR(html.TH(html.DIV("0", id="result"), colspan=3) +                html.TD("C"))


Да, я тоже не слишком уверен в этом синтаксисе с <=. Но, посудите сами, такая классная библиотека, так что я и на него согласен.

Теперь добавим клавиатуру

lines = ["789/", "456*", "123-", "0.=+"]calc <= (html.TR(html.TD(x) for x in line) for line in lines)


Наконец, добавим calc в document.

document <= calc


Итак, пока все хорошо. Как же нам добиться, чтобы все это работало? Сначала нужно захватить ссылку на элемент result, чтобы управлять им, когда будут нажиматься клавиши.

result = document["result"] # прямой доступ к элементу по его id


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

def action(event):    """обрабатывает событие "click" при нажатии на кнопку калькулятора."""    # Элементу, нажатому пользователем, соответствует атрибут "target"     # объекта event     element = event.target    # Текст, выводимый на кнопке, записывается в атрибуте "text" элемента    value = element.text    if value not in "=C":        # обновляем поле с результатом        if result.text in ["0", "error"]:            result.text = value        else:            result.text = result.text + value    elif value == "C":        # сброс        result.text = "0"    elif value == "=":        # выполняем формулу в поле с результатом        try:            result.text = eval(result.text)        except:            result.text = "error"


Наконец, связываем вышеописанный обработчик событий с событием click на всех кнопках.

for button in document.select("td"):    button.bind("click", action)


Видите как все было просто. Но, если серьезно, Brython кажется мне шедевром инженерной работы и, пожалуй, наилучшей иллюстрацией любви к языку Python. Пожалуйста, поддержите разработчиков и поставьте им звездочку в репозитории на Github!
Подробнее..

Перевод Фронтендеры герои. Yehuda Katz объясняет почему

11.01.2021 08:05:18 | Автор: admin

Идея что фронтенд это "для джунов", расстраивает меня тем, что никто не скажет так про другие специализации.

Кто-то может сказать, что неплохо, если б автор компилятора был более "фуллстековым".

Но они не скажут, что "писать компиляторы это для джунов".

Это перевод треда Yehuda Katz из твиттера. Под фронтедом здесь подразумеваются именно браузерные приложения на JS (и, отчасти, вся JS-экосистема).

По сути, когда люди говорят фронт для джунов, они делают несколько больших ошибок. Вот две из них:

  1. Они недооценивают сложность работы.

  2. Они переносят мемы про тулинг фронта на самих фронтендеров.

Фронтенд - это сложно в самой своей сути

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

На самом базовом уровне это просто трудно. Эквивалент пользователь закрыл свой ноутбук - это что, если случайные 5% моих SQL-запросов в Rails фейлятся.

Начинающие фронтендеры не думают об этом, но более опытные - точно учитывают такие кейсы.

Хотя бэкэнд в основном не имеет состояния (и это нам очень нравится), на фронте пользователь может запустить какие-то асинхронные запросы, а затем продолжить использовать приложение произвольным образом.

На бэкэнде вы получаете в значительной степени согласованное представление о мире, с которым можете работать (через ACID транзакции), на клиенте вы постепенно увеличиваете локальный кеш данных, который не имеет гарантий внутренней консистентности.

Опять же, если вы думаете, что на самом деле никто не работает над этим, лол, вы сильно ошибаетесь.

Опытные фронтендеры МНОГО думают о том, как структурировать свои приложения так, чтобы минимизировать эти проблемы, да ещё и так, чтобы начинающие смогли всё это понять.

Фронтенд сложен ещё и с другой стороны: это место, где покрышки касаются дороги при работе с пользователем.

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

Интерфейс - это то, о чем думают руководители вашей компании, когда придумывают новые идеи для приложения.

И очень часто у них есть особое мнение о том, как должен выглядеть этот интерфейс и как он должен работать.

Мелкие ошибки в коде интерфейса заметны не только руководителям и менеджерам компании, но и пользователям.

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

На бэкэнде нет ничего похожего.

Вкратце: на фронтендеров возлагается задача реализовать ту часть приложения, о которой ваши пользователи и руководители могут составить своё мнение и, исходя из него, находить какие-то ошибки.

Фронтендеры должны реализовывать идеи (и адаптироваться к мнению руководителей и пользователей) во враждебной среде (глючный браузер привет Safari, блокировщики рекламы, расширения для перевода и т.д.), в которой пользователь может создать странное состояние приложения в любое время, просто перейдя в комнату с плохим Wi-Fi.

И это важно: опытные фронтендеры постоянно работают над этими сложными проблемами и стараются сделать их решения понятными для менее опытных разработчиков.

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

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

Я видел это во многих, многих компаниях, включая Tilde. Опытные фронтендеры наставляют и направляют новых разработчиков.

Это все очень сложно и запросто может превратиться в отдельную специальность и карьеру. Бессмысленно выделять один только фронтенд и назвать все перечисленное чем-то только для джунов.

Но, я согласен с тем, что каждая техническая роль выиграет от большей "фуллстечности".

Лично я работаю над ядром Ember, бэкэнд-кодом Rails и бэкэнд-кодом Rust, когда работаю над Skylight.

Я считаю, что это хорошо.

Но никто никогда не скажет: Специализация на Rust-бэкэнде у нас имеет смысл только для джунов. По сути, сказать так про фронт - значит сильно недооценить сложность задач, которые стоят перед фронтендерами.

Итак, это была первая часть.

Мемы про node_modules

Вторая часть - это (откровенно комичное) высмеивание инструментов фронта.

Скажу как человек, который в свое время создал много тулингов (bundler, cargo, инспектор Ember): тулинг фронта часто на голову лучше, чем тулинги, которые кто-то может самодовольно превозносить.

Не только JavaScript может получать пользу от транспайлеров. Разве плохо было бы, если б большинство фич C++ 20 работали с более старыми (и все еще широко использующимися) версиями компиляторов через транспиляцию?

Сделать подобное непросто, но в экосистеме JS вы можете использовать совершенно новые фичи (включая экспериментальные), даже если вы ориентируетесь на гораздо более старые браузеры.

И дело не только в том, что вы можете транспилировать код: инструменты вроде eslint, IDE language servers, syntax highlighters и TypeScript обычно поддерживают большую часть самой последней версии ECMAScript задолго до того, как она будет завершена.

Кстати, о TypeScript. TypeScript - лучшая в своем классе система постепенной типизации для динамического языка. Ничто другое (может, за исключением Racket) даже близко к подобному уровню не подбирается, особенно если учесть интеграцию в экосистему и проработанность тулинга.

И всё это довольно легко работает с популярным расширением JS (JSX), которое даже не входит в спецификацию ECMAScript.

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

И для этого не надо ручками указывать набор фич, которые нужны от браузера.

Стандартный способ указать, какие фичи необходимо транслировать - это browserslist, который позволяет вам сказать последняя версия Chrome или браузеры с долей рынка в 1%.

Таким образом, вы просто используете ES2020 и TypeScript, указываете браузеры, которые вам нужно поддерживать (используя гибкое описание, основанное на данных, поддерживаемых caniuse.com), и легко получаете сборку, которая работает в поддерживаемых браузерах.

Если надо поддерживать Node - всё то же самое.

Вдобавок ко всему, девтулзы в браузере намного, намного лучше, чем любые отладчики, доступные для большинства языков - если, конечно, учесть, насколько легко их настроить и насколько они доступны для людей с любым опытом.

Насмехайтесь над npm сколько угодно, но отличия npm от Bundler сильно повлияли на мой дизайн Cargo, а экосистема Rust стала лучше из-за отсутствия ограничения только одна копия вашей зависимости, даже если она используется только внутри пакета.

Конечно, npm мог бы быть лучше, но было впечатляюще (и вдохновляюще) наблюдать за медленным и неуклонным улучшением npm на протяжении многих лет (приличное разрешение зависимостей, файлы блокировки, йуху!).

Workspaces из Yarn перенимаются в npm и pnpm, что ставит фронт в высшую лигу языков с хорошей поддержкой монорепозиториев (поддержка не идеальна, но точно в высшей лиге).

Вдобавок ко всему, бандлеры JS должны (просто для выживания) поддерживать разделение кода и удаление мертвого кода, при этом понятное для неопытных разрабов.

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

И ни одно из этих ограничений не является случайным или чрезмерно усложняющим проблему.

Фронтенд-разработка - сложная проблема по самой своей сути. Это расплата за то, что мы можем писать приложения, которые работают в любой операционной системе, на более старых машинах (и ОС), различных разрешениях (от маленьких телефонов до сверхшироких дисплеев), технологиях доступности и версиях всех этих вещей, которые даже не существует на момент написания кода (при этом ещё и продолжая постоянно работать вне зависимости от обновлений пользовательских устройств).

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

Но это не делает фронтенд специализацией, подходящей только для джунов.

По моему опыту, легче полностью погрузиться во фронтенд, нежели во что-то вроде Rails, и при этом всё ещё приносить огромную пользу по мере развития.

Это, кстати, вызвано (отчасти) тем фактом, что фронт достаточно прост для начинающих разработчиков.

Из-за этого опытные фронтендеры значимую часть времени обучают новых разработчиков.

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

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

Думайте о себе как о герое, который несет прямую ответственность перед пользователем и руководителями, отвечает за то, чтобы ваше приложение работало в чрезвычайно враждебной и неизвестной среде (сравнивая с запустить го в докере).

Вы также несете ответственность за создание абстракций, которые может использовать весь спектр разработчиков в вашей компании, потому что начинающим нужна ваша помощь.

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

Мои друзья-фронтендеры: мне нравится быть с вами в одном сообществе.

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

Мы круты! Мы зажигаем!

Подробнее..

Перевод Поддержка JavaScript-приложений в долгосрочной перспективе

20.02.2021 16:15:19 | Автор: admin
Публикуем перевод статьи, в которой подробно описана многолетняя работа команды по созданию и поддержанию большого портала данных на JavaScript.

В 2019 была написана статья о поддержке больших приложений на JavaScript (Maintaining large JavaScript applications). В продолжение этого материала, хотели бы поделиться клиентским проектом, который моя команда поддерживает с 2014 года.



Портал данных для Организации экономического сотрудничества и развития (ОЭСР)



Стартовая страница портала

Организация экономического сотрудничества и развития (Organisation for Economic Co-operation and Development) это межправительственный орган, который собирает данные и публикует исследования от имени государств-членов. На портале организации содержится информация из различных сфер: экономика, экология, образование и др.

Портал данных ОЭСР является главным хранилищем статистических данных. Он помогает исследователям, журналистам и политикам находить важную информацию и быстро визуализировать ее с помощью диаграмм. Портал ОЭСР объединяет также библиотеку с публикациями OECD iLibrary, и ресурс OECD.Stat, где хранятся все данные.

ОЭСР финансируется государствами-членами, то есть налогоплательщиками, как мы с вами. Одно из требований проекта использование экономически эффективных и надежных технологий, так как поддержка кода важна на долгое время.

Портал данных это совместная работа сотрудников ОЭСР и внешних разработчиков и дизайнеров. Первоначальный дизайн и прототип были созданы Морицем Штефанером и командой Raureif. А компания 9elements разработала front-end часть и до сих пор ее поддерживает.

Сложная кодовая база JavaScript


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



Мы начали работу над порталом данных в 2014 году. С тех пор его особо не переписывали, только добавляли новые функции, небольшие улучшения и рефакторили код. В декабре 2020 для отчета OECD Economic Outlook мы добавили несколько новых функций, в том числе еще четыре типа диаграмм. Кроме того, в тот раз мы значительно реорганизовали кодовую базу.

В этой статье я расскажу, как нам удается поддерживать код так долго и шаг за шагом улучшать его. А также раскрою и то, что не получилось.

Скучная мейнстримная технология


Проект стартовал в 2014 году, и тогда мы выбрали простой HTML, шаблоны XSLT, Sass для стилей таблиц и CoffeeScript как язык, который компилируется с JavaScript. В качестве библиотек JavaScript мы выбрали jQuery, D3, D3.chart и Backbone.

В 2014 году эти технологии были самыми безопасными и наиболее совместимыми, из всех них только выбор CoffeeScript был рискованной авантюрой. Благодаря CoffeeScript, мы смогли стать более продуктивными и написали надежный код. Но мы знали, что с этой новой технологией могут возникнуть трудности.

С 2015 года команда 9elements стала использовать React для большинства веб-приложений на JavaScript. Мы думали использовать React для новой версии движка диаграмм, но все не могли выбрать подходящий момент. В результате оказалось, что придерживаться исходного стека технологий было правильным решением.

Сейчас описанный стек JavaScript может показаться устаревшим, но, на самом деле, кодовая база выдержала испытание временем. Одна из причин: выбранные нами технологии до сих пор релевантны.

Разрушительное влияние времени


Множество JavaScript-библиотек появлялось и исчезало, но jQuery по-прежнему остается самой популярной. Она надежна, хорошо поддерживается и широко распространена. Согласно Web Almanac 2020, jQuery используется на 83% всех веб-сайтов. (Для сравнения, React обнаружили только на 4%.)

Конечно, jQuery утратила лидерские позиции в решении сложных задач с DOM. Как уже упоминалось, сейчас для создания подобного портала данных мы бы выбрали React или Preact.

Вторая библиотека, D3, остается стандартом в области визуализации данных в браузере. Она существует с 2010 года и до сих пор остается лидером. Хотя пара крупных релизов значительно изменили структуру и API, D3 это все еще выдающаяся инженерная работа.

Библиотека Backbone не так популярна, но имеет свои плюсы. Например, она относительно простая: вы можете прочитать исходный код за одно утро и переделать основные части за день. Кроме того, Backbone все еще поддерживается. Она полнофункциональна, что особенно важно.

С технологической точки зрения только CoffeeScript не является актуальной технологией в текущих реалиях. Данный язык был разработан из-за явных недостатков в ECMAScript 5. Позже многие идеи из CoffeeScript были включены в стандарты ECMAScript 6 (2015) и ECMAScript 7 (2016). С этого момента у нас уже не осталось причин его использовать.

Мы выбрали CoffeeScript в 2014 году из-за его философии: Это просто JavaScript. В отличие от других языков, компилируемых с JavaScript, CoffeeScript представляет собой прямую абстракцию. Код CoffeeScript без сюрпризов компилируется в чистый JavaScript.

Сегодня большинство компаний перенесли свои кодовые базы с CoffeeScript на современный JavaScript. И мы сделали то же самое.

От CoffeeScript до TypeScript


С помощью этого инструмента для декофеинизации, мы преобразовали код CoffeeScript в ECMAScript 6 (2015). Мы хотели продолжить поддерживать те же самые браузеры, поэтому теперь используем компилятор Babel для создания обратно совместимого ECMAScript 5.

В целом переход прошел гладко, но мы не хотели останавливаться на достигнутом.

В новых проектах разработчики из 9elements используют TypeScript. На мой взгляд, TypeScript лучшее, что произошло в мире JavaScript за последние пару лет. Как я упоминал в своей предыдущей статье, TypeScript заставляет задуматься о типах и приучает правильно их называть.

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

TypeScript это надмножество JavaScript. Компилятор хорошо понимает файлы .js. Поэтому мы постепенно добавляли аннотации типов с помощью технологии 20-летней давности JSDOC. В дополнение к этому написали несколько типов (typings) в файлах .ts, чтобы ссылаться на них в аннотациях JSDOC.

Таким образом, опыт разработки в Visual Studio Code значительно вырос без особых усилий. Хотя здесь нет строгой проверки типов, редактирование кода так же удобно, как и в обычном проекте на TypeScript.

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

Документация и комментарии к коду


На первый взгляд, кодирование это разговор между вами и компьютером: вы говорите компьютеру, что ему нужно сделать.

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

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

До перехода на JSDOC, мы сделали удобные для чтения аннотации типов, задокументированные параметры функций и возвращаемые значения. Также мы задокументировали основные типы данных и сложные структуры вложенных объектов.

Эти комментарии оказались действительно полезны шесть лет спустя. Мы перевели их на машиночитаемый JSDOC и в объявления типов для компилятора TypeScript.

Вещи ломаются всегда имейте под рукой набор тестов


В проекте есть только несколько автоматизированных юнит-тестов и более 50 (!) тестовых страниц, которые демонстрируют все страницы портала, компоненты, интерфейсы запроса данных, типы и настройки диаграмм. Они тестируют как оперативные, промежуточные, а также моковые (mock) данные.

Такие тестовые страницы нужны для того же, что и автоматизированные тесты: если мы исправляем ошибку, то сначала добавляем сценарий на соответствующую тестовую страницу. Если разрабатываем новую функцию, то одновременно создаем полную тестовую страницу.


Тестовая страница (test page)

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

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

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


В 2014 году наш портал должен был работать с Internet Explorer 9. Сейчас Internet Explorer не так важен, особенно при создании динамичного движка для построения графиков в браузере.

Однако мы решили сохранить совместимость со старыми браузерами. Портал данных это международная платформа, куда заходят пользователи со всего мира. Не у всех есть последние модели компьютеров и новые браузеры.


Портал в браузере Internet Explorer 9

Мы смогли сохранить базовый уровень поддержки браузеров, используя скучные стандартные технологии. Конечно, есть и несколько современных веб-функций, но их мы активируем, только если поддерживает браузер, В этом нам помогает подход Progressive Enhancement (прогрессивное улучшение). Также мы используем Babel и polyfills, чтобы современные функции JavaScript работали и в старых браузерах.

Ваши абстракции могут и укусить


За все эти годы нас ограничивал не стек технологий. Скорее, на пути встали собственные абстракции.

Мы разделили пользовательский интерфейс на визуальные части (views) и создали базовый класс, аналогичный Backbone.View. (Сегодня все большие библиотеки JavaScript используют термин component вместо view для частей пользовательского интерфейса.) Для хранения данных и состояния мы использовали Backbone.Model. Это отлично сработало, но мы решили придерживаться собственных лучших практик.

Идея разделения Backbone model-view состоит в том, чтобы эта модель была единственным источником истины. DOM должен просто отражать данные модели. Все изменения должны исходить также из модели. Современные фреймворки, такие как React, Vue и Angular, соблюдают соглашение о том, что пользовательский интерфейс является функцией состояния, то есть UI определенно является производным от состояния.

Мы нарушали этот принцип и иногда делали DOM источником истины. Это приводило к путанице с кодом, который рассматривал модель в качестве авторитетного источника.

Объектно-ориентированные диаграммы


Для графиков мы выбрали другой подход: создали классы диаграмм, отличающиеся от тех, которые описывали выше.

Сам D3 функционален. Диаграмма, как правило, создается и обновляется с помощью функции рендеринга, которая вызывает другие функции. Такие диаграммы являются вводными для этой большой функции. Больше состояния содержится в конкретных объектах.

Это делает D3 выразительным и гибким. Но код D3 трудно читать, так как есть небольшие соглашения (договоренности, которые не задокументированы) по работе со структурами диаграмм.

Ирен Рос и Майк Пенниси, разработчики из компании Bocoup, изобрели d3.chart небольшую библиотеку поверх D3, которая представляет OOП на основе классов. Ее основной целью было структурировать и повторно использовать код построения диаграмм. Эти диаграммы состоят из слоев, каждый из которых отображает и обновляет определенную часть модели DOM с помощью D3. Кроме того, к диаграммам могут быть прикреплены другие диаграммы.

Общее правило OOП Композиция преобладает над наследованием. К сожалению, мы выбрали странное сочетание композиции и наследования для поведения диаграммы.

Нам нужно было использовать функции или простые классы вместо сложных иерархий классов. Люди до сих пор оборачивают D3 в OOП на основе классов, но ни одно решение на основе классов не смогло превзойти функциональную структуру D3.

Подведем итоги


С того момента, как мы разработали front-end часть портала данных в 2014 году, появилось много крутых подходов для создания веб-интерфейсов на основе JavaScript.

Сейчас UI-компоненты декларативны, можно обойтись без рендеринга HTML-шаблонов и обновления DOM вручную. Вы просто обновляете состояние, и фреймворк обновляет DOM. Этот однонаправленный поток данных устраняет целый класс багов.

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

В компании 9elements мы всегда стараемся использовать современные технологии, в том числе оценивать экспериментальные front-end технологии, которые могут быть нерелевантны через 3-4 года. К сожалению, многие open-source JavaScript-проекты могут быть прогрессивными с технической точки зрения, но при этом оказаться нестабильными.

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

Exports в package.json

09.03.2021 22:22:04 | Автор: admin

Привет. Я работаю в команде, занимающейся улучшением пользовательского опыта
при работе с деньгами. Front-end мы поставляем npm-пакетами.

В какой-то момент я столкнулся с проблемами, которые привели меня к использованию
поля exports в package.json


Проблема #1


Пакеты могут экспортировать функции с одинаковыми названиями, но делающие разные вещи. Для примера возьмём 2 стейт-менеджера: Reatom и Effector.

Они экспортируют функцию createStore. Если попытаться экспортитировать их из одного пакета (назовём его vendors), то получится следующая картина:


// @some/vendors/index.tsexport { createStore } from '@reatom/core';export { createStore } from 'effector';

Налицо конфликт имён. Такой код просто не будет работать.


Этого можно избежать за счёт as:


// @some/vendors/index.tsexport { createStore as reatomCreateStore } from '@reatom/core';export { createStore as effectorCreateStore } from 'effector';

Выглядит, прямо скажем, паршиво. По-хорошему, каждый реэкспорт надо писать через as для поддержания консистентности. Как по мне, это ухудшает DX.

Я начал искать решение, которое позволит избежать и конфликта имён и необходимости писать as. Как бы оно могло выглядеть Например, вот так:


// @some/vendors/reatom.tsexport { createStore } from 'reatom';

// @some/vendors/effector.tsexport { createStore } from 'effector';

В двух разных файлах мы пишем обычные экспорты и импортируем нужную реализацию createStore:


// someFile.tsimport { createStore } from 'vendors/effector';

Проблема #2


Пакет vendors, скорее всего, содержит не только стейт-менеджер, а ещё какую-то библиотеку. Например, Runtypes.
Если не использовать exports, то импорт будет выглядеть вот так:


// someFile.tsimport { createStore, Dictionary, createEvent, Record } from 'vendors';

Получается какая-то мешанина. На мой взгляд, приятнее было бы читать что-то в стиле:


// someFile.tsimport { createStore, createEvent } from 'vendors/effector';import { Dictionary, Record } from 'vendors/runtypes';

А ещё хорошо бы скрыть имена библиотек. Это может быть полезно при последующих рефакторингах.


// someFile.tsimport { createStore, createEvent } from 'vendors/state';import { Dictionary, Record } from 'vendors/contract';

Решение


Чтобы добиться таких импортов, необходимо обратиться к полю exports в package.json


// package.json"exports": {  "./contract": "./build/contract.js",  "./state": "./build/state.js",  "./package.json": "./package.json"}

По сути, мы просто говорим сборщику как резолвить импорты. Если вы пишете на TypeScript, то это ещё не всё.

В package.json есть поле types, которое позволяет указать, где находятся типы пакета. В значении у него строка. Не получится указать типы и для contract, и для state. Так что же делать?

Тут на помощь приходит typesVersions в package.json


// package.json"typesVersions": {  "*": {    "contract": ["build/contract.d.ts"],    "state": ["build/state.d.ts"]  }}

Мы делаем то же самое, что и в exports, но для d.ts файлов, получая рабочие типы.


Заключение


Использование exports не ограничивается проблемой создания пакета vendors. Его можно использовать для улучшения DX.

Например, в Effector базовый импорт выглядит вот так:


import { createEvent } from 'effector';

А для поддержки старых браузеров вот так:


import { createEvent } from 'effector/compat';

Какие ещё проблемы решает exports можно ознакомиться тут.
Посмотреть на репозиторий с примером здесь.


Спасибо!

Подробнее..

I18n в Angular

27.08.2020 14:07:16 | Автор: admin

Angular i18n


Цель статьи это описать детальные шаги интернационализации вашего приложения на Angular с помощью родного функционала.


На входе мы имеем коммерческое приложение. Как традиционно положено сначала сделали, запустили, а потом решили что ой, у нас же есть другие рынки, нужно срочно перевод сделать. Обычная история, пальцев рук не хватит посчитать сколько раз такое было. Но так же все знают что решения по интернационализации необходимо закладывать в архитектуру приложения с самого начала, потому что потом очень тяжело добавить i18n в готовое приложение. Но все же...


Angular i18n
Illustration by Thomas Renon


Есть два подхода использовать встроенный в angular i18n: генерация под каждую локаль своего бандла приложения, либо использовать библиотеки вроде transloco, которые предлагают хранить переводы во внешних json файлах или разных других форматах и динамически подставлять/менять локаль по запросу пользователя. Не мало холиваров было о вопросе как удобнее, но однозначно ясно одно если у нас уже написаное приложение расставлять в нем токены дело не сильно приятное. В то время как родные средства Angular более подходят, для того чтобы взять и готовое приложение сделать многоязычным.


Тут вы найдете ответы на вопросы:


  • Как вынести текущий язык в токены
  • Как добавить новый язык переводов
  • Как модифицировать языки
  • Как деплоить и собирать приложение
  • Как быть если есть токены в ts файле или они приходят по API

Вступление


Как это работает?


Помимо черной магии в angular есть специальный атрибут i18n для поддержи интернационализации. Работает он совсем не так как обычные атрибутивные компоненты в angular (как ngClass к примеру). Потому что на самом деле это не компонента, Это фактически директива препроцессора. Да, для интернационализации Angular предлагает не использовать Angular, а использовать хитрый препроцессор во время сборки проекта. Именно такой подход отчасти и позволяет нам локализовать приложение которое уже написанно с минимальными вложениями в этот процесс (оставим за кадром RTL языки, поддержка которых хромает на обе ноги везде). Соответственно разметив все строки в шаблонах мы говорим angular-cli извлеки все строки в проекте и сделай мне файл для переводов.


Итого Для создания мультиязычных интерфейсов Angular предлагает использовать механизм разметки HTML шаблонов при помощи специального атрибута i18n который после компиляции удаляется из финального кода.


1. Вопрос: "Как токенизировать текущий язык и не создать путаницу токенов"


  • Советую всегда привязываться к id
  • Для себя выберите правила токенизации приложения которым должны следовать все на проекте

Теперь вопрос: "как создать сам id"?
Какие правила придумать?


В проекте следует по возможности для маркера указывать дополнительные параметры которые отображаются в специализированных редакторах использующихся для перевода и дополняют переводимый текст служебной информацией призванной помочь переводчику. Это параметры передаются в формате Значение|Описание или только Описание. Обязательно следует указывать @@id это будет токен для перевода. Идентификатор пишется своеобразным синтаксисом используя префикс @@.


<div i18n="форма логина | поле @@login.email">Email</div><button i18n="форма логина | кнопка @@login.post">save</button>

Правила для именования можно выбрать такие:
Название компонента или роут используются как префикс токена это позволит переводчику понять где искать токен, грубо говоря не явное описание контекста. Кроме того при удалении компонента из проекта легко вычленить и удалить все связанные с ним переводы.
Слова метки: кнопка, переключатель, поле, колонка для переводчиков как дополнительный контекст


Пример соглашения по наименованию токенов


Находясь в компоненте <info-statuses> токен следует называть таким образом:


<th i18n="Статусы покупателя | колонка @@(селектор компонента и поле)info-statuses.date">Дата добавления</th>

Различные варианты использования токенов


Иногда у нас есть необходимость задать перевод для текста который является фрагментом внутри большего блока или текст вне тегов. Используем ng-container, который не рендерится в финальный код.


<ng-container     i18n="Генерация архива | поле @@generate-archive.title">I don't output any element</ng-container>

Возможно так же делать перевод для атрибутов тегов. Указывается i18n-attrName


<img     [src]="logo"     i18n-title="картинка @@company.logo"    title="Angular logo" />

Вот мы заполнили все шаблоны тегами i18n и что теперь? Теперь нужно создать файл переводов, Angular приходит на помощь и говорит, просто вызываем команду i18n-extract и генерируем файл с переводами. Глянуть описание аргументов можно тут https://angular.io/cli/xi18n


В моём случае команда выглядит таким образом (я указываю исходную локаль файлов перевода. "uk")


"extract-i18n": "ng xi18n projectName --i18n-format xlf --output-path i18n --i18n-locale uk

Теперь вы знаете ответ как локализовать приложение под один язык.


2. Как добавить новый язык для переводов


Сейчас мы поговорим о инструментах, что помогают работать с дефолтным форматом выгрузки ключей в angular i18n это xliffmerge


Проблема возникает при генерации нового языка. Необходимо синхронизировать токены дефолтного языка и нового. Также есть процесс обновления локалей, после редактирования переводчиком или после добавления новых токенов в проект. Инструмент xliffmerge решает эти проблемы одним конфигом и одной командой.


Вывод для настройки и удобной генерации новых языков xliffmerge наше спасение.


https://www.npmjs.com/package/@ngx-i18nsupport/ngx-i18nsupport
https://github.com/martinroob/ngx-i18nsupport/wiki/Tutorial-for-using-xliffmerge-with-angular-cli


"xliffmerge": {  "builder": "@ngx-i18nsupport/tooling:xliffmerge",  "options": {    "xliffmergeOptions": {      "i18nFormat": "xlf",      "srcDir": "projects/my-test/i18n",      "genDir": "projects/my-test/i18n",      "verbose": true,      "defaultLanguage": "uk",      "languages": [        "uk",        "en"      ]    }  }}

В настройках angular.json мы добавляем новую конфигурацию.
Эта конфигурация при запуске, принимает дефолтный исходный файл. B зависимости от настроек генерирует производные или дополняет уже существующие файлы с переводом новыми ключами. Важно! При добавлении ключа в базовую локаль он будет добавлен всем производным локалям. Это все делается автоматически, не нужно править XML руками.


Таким образом. Когда мне нужно добавить новую локаль. Я добавляю поле в блок languages с именем языка, к примеру en и запускаю ng run my-test:xliffmerge чтобы на выходе получить новый файл xlf с локалью en.
Теперь команда генерации файлов переводов выглядит таким образом


"extract-i18n": "ng xi18n crm --i18n-format xlf --output-path i18n --i18n-locale ru && ng run my-test:xliffmerge",

Было бы классно ещё пропускать переводы через google translate, чтобы сэкономить на переводах и иметь какой-то черновой вариант подумал я. Как выяснилось xliffmerge имеет и такую опцию.


Дополняем конфиг xliffmerge а angular.json:


"autotranslate": ["en"],"apikey": "yourAPIkey",

Хорошо, теперь при изменениях в нашем html запуск команды extract-i18n будет обновлять все локали.


Осталось последнее, как собирать бандл для деплоя.


"build-prod:my-test:en": "ng build my-test --configuration=productionEN --base-href /en/ --resources-output-path ../""build-prod:my-test:uk": "ng build my-test --configuration=productionUK --base-href /uk/ --resources-output-path ../","build-prod:locales": "npm run build-prod:my-test:en && npm run build-prod:my-test:uk",

Под каждую локаль своя команда, к сожалению, в А8 на то время нельзя было ставить аргументы через запятую --configuration=production,en, поэтому пришлось дублировать конфиги в angular.json


"productionEN": {  "outputPath": "dist/my-test/en",  "fileReplacements": [    {      "replace": "projects/my-test/src/environments/environment.ts",      "with": "projects/my-test/src/environments/environment.en.prod.ts"    }  ],  ... like in production},

Мы настроили билд так, чтобы assets были общими (resources-output-path ../), вы можете убрать resources path и максимально отделить разные версии между собой. Для большинства приложений ресурсы в разных языковых версиях не будут отличаться, поэтому такой ход оправдан. В случае общих ресурсов перезагрузка бандла при смене языка будет происходить существенно быстрее, потому что часть ресурсов уже будет в кеше браузера.


Теперь запоминаем самое главное: файлы .xlf никогда руками не правим, через специальные инструменты (weblate к примеру OpenSource инструмент) можно записывать верные переводы и пушить в ветку, а там ваш билд это всё подхватит и всё супер.


На выходе в нашем проекте мы разметили все шаблоны, дополнили список команд для работы с переводами, сняли с себя задачу актуализации json между несколькими языками, наняли несколько переводчиков в штат, чтобы они изучили CMS Weblate и поседели раньше чем разработчики. На самом деле для небольших проектов экономия на снятии сопровождения переводов с разработчиков дает весьма значимый эффект.


Таким образом мы теперь должны доработать свой CI/CD папйплайн чтобы генерировать несколько языковых версий вместо одной.


Всё работает, но оказывается у в проекте есть текст который зашит в ts файлах и как его переводить, если подстановка i18n атрибута работает только в шаблонах.


Переводы в коде


Есть список с текстом, который никак не завязан на бэкенд, поэтому и переводов у нас с бэканда этой сущности нет. Что делать? Ответ один локализируй через шаблон.


Вот пример как это будет


  list = [    {      token: 'login-info-1',      value: 1,    },    {      token: 'login-info-2',      value: 2,    },  ];

 <div style="display: none"       #el       i18n-login-info-1="поле @@login-form.first"       login-info-1="первое условие это..."       i18n-login-info-2="поле @@login-form.second"       i18n-login-info-2="второе условие это..."  >  </div>   <div *ngFor="let item of list">      <label>        {{ item.token | customPipeI18n: el }}      </label>  </div>

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


Пример пайпа


@Pipe({ name: 'customPipeI18n', pure: true })export class TranslatePipe {  transform(key: string, value: HTMLElement): any {    const lowerKey = key.toLowerCase();    if (value && value.hasAttribute(lowerKey)) {      return value.getAttribute(lowerKey);    }    console.log('key: ', lowerKey);    return '*not found key*';  }}

Хорошо это работает. А что делать если у меня схожие тексты повторяются на многих страницах? Добавлять на каждую по скрытому элементу с идентичной логикой?


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


Для этого создана комбинация:
сервис(ElementRegistry) для хранения элемента;
директива(ElementDirective) для регистрации шаблона с атрибутами и сохранения его в сервис;
пайп(ElementPipe) для получения перевода из сервиса;


Пример использования:


Имеем модуль auth
в корневом компоненте создаём элемент с атрибутами объявляем директиву и регистрируем имя шаблона auth


<div  i18nElement="auth"  le="display: none"  i18n-login-info-1="поле @@login-form.first"  login-info-1="первое условие это..."  i18n-login-info-2="поле @@login-form.second"  i18n-login-info-2="второе условие это..."></div>

Для перевода вызывается pipe i18nElement туда передаётся название шаблона в котором объявлены атрибуты с токенами.


   <div *ngFor="let item of list">      <label>        {{ item.token | i18nElement: 'auth' }}      </label>  </div>

Это решает следующие проблемы:


  • Eсли мы используем текст который приходит из сторонего API, а локализовать необходимо ответ
  • У вас в ts файле просто почему-то оказался текст который нужно локализовать

Итог


Simple-Made-Easy нативные средства i18n в Angular не смотря на меньшую популярность чем классический подход с кучей json файлов тоже работают и весьма удобны\продуманны в практическом применнии.
xliff как формат хранения переводов помимо непригодности для редактирования руками имеет много удобных интсрументов для переводчиков, позволяющих анотировать и групировать переводы. Отказ от использования json и переход на xliff позволяет упростить работу с переводами для команды локализации, особенно вместе с инструментами вроде weblate или аналогами.
Некоторые сложности вызывает использование переводов вне шаблонов, но все они в целом решаемые при помощи подходв описанных в статье.


P.S.


в 9-10м Ангуляре есть изменения в работе с локализацией. Ставьте палец вверх и будет ещё одна статья про облегчение с 9м


Пригласить автора на хабр: skochkobo
Метода опробована на проектах: nodeart.io

Подробнее..

Из песочницы vuex typescript vuexok. Велосипед, который поехал и обогнал всех

07.11.2020 22:21:21 | Автор: admin
Доброго времени суток.

Как и многие разработчики, я в свободное от работы время пишу свойотносительнонебольшой проект. Раньше писал на react, а на работе используется vue. Ну и что бы прокачаться во vue начал пилить свой проект на нем. Сначала всё было хорошо, прямо-таки радужно, пока я не решил, что надо бы еще прокачаться и в typescript. Так в моем проекте появился typescript. И если с компонентами всё былонеплохо, то с vuex всё оказалось печально. Так мне пришлось пройти все 5 стадий принятия проблемы, ну почти все.

Отрицание


Основные требования для стора:

  1. В модулях должны работать типы typescript
  2. Модули должно быть легко использовать в компонентах, должны работать типы для стейта, экшенов, мутаций и геттеров
  3. Не придумывать новое api для vuex, надо сделать так,чтобы как-то типы typescript заработали с модулями vuex, чтобы не приходилось разом переписывать всё приложение
  4. Вызов мутаций и экшенов должен быть максимально простым и понятным
  5. Пакет должен быть как можно меньше
  6. Не хочу хранить константы с именами мутаций и экшенов
  7. Оно должно работать (А как же без этого)

Не может быть что у такого уже зрелого проекта как vuex не было нормальной поддержки typescript. Ну-с, открываемGoogleYandex и погнали. Я был уверен на 100500% что с typescript всё должно быть отлично (как же я ошибался). Есть куча разных попыток подружить vuex и typescript. Приведу несколько примеров, которые запомнились, без кода чтобы не раздувать статью. Всё есть в документации по ссылкам ниже.

vuex-smart-module

github.com/ktsn/vuex-smart-module
Добротно, даже очень. Всё при себе, но лично мне не понравилось то, что для экшенов, мутаций, стейта, геттеров надо создавать отдельные классы. Это, конечно, вкусовщина, но это я и мой проект) И в целом вопрос типизации решен не до конца (ветка комментариев с объяснением почему).

Vuex Typescript Support

Хорошая попытка, но что-то много переписывать, да и вообще не принялось сообществом.

vuex-module-decorators

Казалось, что это идеальный способ подружить vuex и typescript. Похоже наvue-property-decorator, который я использую в разработке, работать с модулем можно как с классом, в общем супер, но

Но наследования нет. Классы модулей не корректно наследуются и issue на эту проблему висят уже очень давно! А без наследования будет очень много дублирования кода. Блин

Гнев


Дальше было совсем уже не очень, ну или так же идеального решения нет. Это тот самый момент, когда говоришь себе: Ну зачем я начал писать проект на vue? Ну ты же знаешь react, ну писал бы на react, там бы таких проблем не было! На основной работе проект на vue и тебе надо в нем прокачаться зашибись аргумент. А оно стоит потраченных нервов и бессонных ночей? Сиди как все, пиши компонентики, нет, тебе больше всех надо! Бросай этот vue! Пиши на react, прокачивайся в нем, за него и платят больше!

В тот момент я был готов хейтить vue как никто другой, но это были эмоции, и интеллект всё же был выше этого. Vue имеет (на мой субъективный взгляд) много преимуществ над react, но совершенства не бывает, как и победителей на поле сражений. И vue, и react по-своему хороши, а так как уже значительная часть проекта написана на vue, то было бы максимально глупо сейчас переходить на react. Надо было решить, что же делать с vuex.

Торг


Ну что же, дела обстоят не очень хорошо. Может тогда vuex-smart-module? Этот пакет вроде хорош, да, надо создавать много классов, но работает отлично же. Или может попробовать прописывать типы для мутаций и экшенов руками в компонентах и использовать чистый vuex? Там и vue3 c vuex4 на подходе, может у них дела с typescript обстоят лучше. Так что давай попробуем чистый vuex. И вообще на работу проекта это не влияет, всё же работает, типов нет, но вы держитесь. И держимся же)

Сначала так и начал делать, но код получается монструозный

Депрессия


Надо было двигаться дальше. Но куда неизвестно. Это был совсем отчаянный шаг. Решил сделатьконтейнер состояния с нуля. Код был набросан за пару часов. И получилось даже хорошо. Типы работают, состояние реактивно и даже наследование есть. Но вскоре агония отчаяния стала отступать, а здравый смысл возвращаться. В общем, эта идея отправилась на свалку. По большому счету это был паттерн глобальной шины событий. А он хорош только для не больших приложений. И вообще писать свой vuex всё же совсем перебор (по крайней мерев моей ситуации). Тут я уже догадывался, что совсем загнался. Но отступать было уже поздно.

Но если кому интересно, то код тут: (Наверное зря добавил этот фрагмент, но путь будет)

Слабонервным не смотреть
const getModule = <T>(name:string, module:T) => {  const $$state = {}  const computed: Record<string, () => any> = {}  Object.keys(module).forEach(key => {    const descriptor = Object.getOwnPropertyDescriptor(      module,      key,    );    if (!descriptor) {      return    }    if (descriptor.get) {      const get = descriptor.get      computed[key] = () => {        return get.call(module)      }    } else if (typeof descriptor.value === 'function') {      // @ts-ignore      module[key] = module[key].bind(module)    } else {      // @ts-ignore      $$state[key] = module[key]    }  })  const _vm = new Vue({    data: {      $$state,    },    computed  })  Object.keys(computed).forEach((computedName) => {    var propDescription = Object.getOwnPropertyDescriptor(_vm, computedName);    if (!propDescription) {      throw new Error()    }    propDescription.enumerable = true    Object.defineProperty(module, computedName, {      get() { return _vm[computedName as keyof typeof _vm]},      // @ts-ignore      set(val) { _vm[computedName] = val}    })  })  Object.keys($$state).forEach(name => {    var propDescription = Object.getOwnPropertyDescriptor($$state,name);    if (!propDescription) {      throw new Error()    }    Object.defineProperty(module, name, propDescription)  })  return module}function createModule<  S extends {[key:string]: any},  M,  P extends Chain<M, S>>(state:S, name:string, payload:P) {  Object.getOwnPropertyNames(payload).forEach(function(prop) {    const descriptor = Object.getOwnPropertyDescriptor(payload, prop)    if (!descriptor) {      throw new Error()    }    Object.defineProperty(      state,      prop,      descriptor,    );  });  const module = state as S & P  return {    module,    getModule() {      return getModule(name, module)    },    extends<E>(payload:Chain<E, typeof module>) {      return createModule(module, name, payload)    }  }}export default function SimpleStore<T>(name:string, payload:T) {  return createModule({}, name, payload)}type NonUndefined<A> = A extends undefined ? never : A;type Chain<T extends {[key:string]: any}, THIS extends {[key:string]: any}> = {  [K in keyof T]: (    NonUndefined<T[K]> extends Function       ? (this:THIS & T, ...p:Parameters<T[K]>) => ReturnType<T[K]>      : T[K]  )}


ПринятиеРождение велосипеда который обогнал всех. vuexok


Для нетерпеливых код тут, краткая документация тут.

В конце концов, написал крохотную библиотечку, которая закрывает все хотелки и даже чуть-чуть больше чем от нее требовалось. Но обо всём по порядку.

Простейший модуль с vuexok выглядит так:

import { createModule } from 'vuexok'import store from '@/store'export const counterModule = createModule(store, 'counterModule', {  state: {    count: 0,  },  actions: {    async increment() {      counterModule.mutations.plus(1)    },  },  mutations: {    plus(state, payload:number) {      state.count += payload    },    setNumber(state, payload:number) {      state.count = payload    },  },  getters: {    x2(state) {      return state.count * 2    },  },})

Ну вроде почти как vuex, хотя что там на 10й строке?

counterModule.mutations.plus(1)

Воу! А это легально? Ну с vuexok да, легально) Метод createModule возвращает объект, который в точности повторяет структуру объекта модуля vuex, только без свойства namespaced, и мы можем использовать его для вызова мутаций и экшенов или для получения стейта и геттеров, причем все типы сохраняются. Причем из любого места, где его можно импортировать.

А что там с компонентами?

А с ними все отлично, так как фактически это vuex, то в принципе ничего не поменялось, commit, dispatch, mapState и т.д. работают как и раньше.

Но теперь можно сделать так, чтобы типы из модуля работали в компонентах:

import Vue from 'vue'import { counterModule } from '@/store/modules/counterModule'import Component from 'vue-class-component'@Component({  template: '<div>{{ count }}</div>'})export default class MyComponent extends Vue {  private get count() {    return counterModule.state.count // type number  }}

Свойство state в модуле реактивно, как и в store.state, так что чтобы использовать состояние модуля в компонентах Vue достаточно просто вернуть часть состояния модуля в вычисляемом свойстве. Есть только одна оговорка. Я намеренно сделал стейт Readonly типом, не хорошо так стейт vuex изменять.

Вызов экшенов и мутаций прост до безобразия и тоже сохраняются типы входных параметров

 private async doSomething() {   counterModule.mutations.setNumber(10)   // Аналогично вызову this.$store.commit('counterModule/setNumber', 10)   await counterModule.actions.increment()   // Аналогично вызову await this.$store.dispatch('counterModule/increment') }

Вот такая красота получилась. Чуть позже понадобилось еще реагировать на изменение jwt, который тоже хранится в сторе. И тогда появился у модулей метод watch. Вотчеры модулей работают также как store.watch. Единственная разница заключается в том, что в качестве параметров функции-гетера передаются стейт и гетеры модуля

const unwatch = jwtModule.watch(  (state) => state.jwt,  (jwt) => console.log(`New token: ${jwt}`),  { immediate: true },)

Итак, что мы имеем:

  1. типизированный стор есть
  2. типы работают в компонентах есть
  3. апи как у vuex и всё что было до этого на чистом vuex не ломается есть
  4. декларативная работа со стором есть
  5. маленький размер пакета (~400 байт gzip) есть
  6. не иметь необходимости хранить в константах названия экшенов и мутаций есть
  7. оно должно работать есть

Вообще странно что такой прекрасной фичи нет во vuex из коробки, это же офигеть как удобно!
Что касается поддержки vuex4 и vue3 не проверял, но судя по докам должно быть совместимо.

Так же решены проблемы представленные в этих статьях:

Vuex решаем старый спор новыми методами
Vuex нарушает инкапсуляцию

Влажные мечты:


Было бы здорово сделать так что бы в контексте экшенов были доступны мутации и другие экшены.

Как это сделать в контексте типов typescript хер его знает. Но если бы можно было делать так:

{  actions: {    one(injectee) {       injectee.actions.two()    },    two() {      console.log('tada!')    }}

То радости моей не было бы предела. Но жизнь, впрочем как и typescript, суровая штука.

Вот такое приключение с vuex и typescript. Ну, вроде выговорился. Спасибо за внимание.
Подробнее..

Цветовая палитра как часть дизайн-системы

07.06.2021 20:11:26 | Автор: admin

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

Возможно, гипотеза, описанная в данной статье, станет хорошим подспорьем для обсуждения и решения одной из проблем, которые возникают при разработке дизайн-системы, а именно правильной настройки цветовой схемы.

Характеристики цвета

Цвета разделяют на две категории: хроматические и ахроматические. Ахроматические это не спектральные цвета. Черный, белый, оттенки серого. Они имеют только одну характеристику светлоту.

Цветовые системы

Классификация цветовых систем:

  1. По свету (RGB). Смесь. Красный, зеленый, синий

  2. По краске (CMYK). Вычитание. Голубой, пурпурный, желтый, черный

На мониторах мы имеем дело со светом, поэтому разберем более подробно, что такое цветовая система света.

В 1676 Исаак Ньютон провел следующий эксперимент: пропустил луч света через трёхгранную призму и получил цветовой спектр от красного к фиолетовому, кроме пурпурного. После этого спектр был пропущен через собирательную линзу, в результате чего был получен белый цвет.

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

Цвет света

До этого эксперимента все считали, что предметы обладают цветом. Однако, Ньютон определил что предмет при попадании на него луча света часть световых волн поглощает, а часть отражает. И в зависимости от того, с каким углом происходит преломление, таким мы и считаем цвет предмета.

Красный сосуд выглядит красным потому, что он поглощает все остальные цвета светового луча и отражает только красный.

Цветовой круг НьютонаЦветовой круг Ньютона

Иоганн Гёте смешал фиолетовый и красный, таким образом, получив пурпурный цвет и появился новый цветовой круг.

Цветовой круг Иоганна ГётеЦветовой круг Иоганна Гёте

Филипп Отто Рунге расширил ряд первичных цветов, добавив в него черный и белый. Рунге строил свои выводы на опытах с пигментами, что делало его учение более близким живописи.

Цветовой шар Отто РунгеЦветовой шар Отто Рунге

Иоханнес Иттен. Его 12-частный цветовой круг показывает наиболее распространенную в мире систему расположения цветов и их взаимодействие между собой.

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

Цветовой круг ИттенаЦветовой круг Иттена

Мишель Шеврёль был первым, кто разработал цветовой атлас. В его основе 6 основных цветов в двенадцати модификациях.

Цветовая система ШеврёляЦветовая система Шеврёля

Цвет краски (Субтрактивный цвет или Subtractive color)

Эта система предсказывает спектр мощности света после прохождения света через последовательно поглощающие слои разных сред. Создателем цветового колеса при вычитаемом смешении является Моисей Харрис.

Если красную бумагу (её поверхность поглощает все лучи кроме красного) освещать зеленым цветом, то она покажется нам черной, потому что зеленый цвет не содержит в себе лучей красного цвета, которые могли быть отражены нашей красной бумагой. Все краски являются вещественными (впитывающими), поэтому при их смешивании нужно руководствоваться принципами вычитания.

Цветовой круг Моисея ХаррисаЦветовой круг Моисея Харриса

Какой формат использует большинство?

Еще со времен CSS 2.1 принято использовать либо HEX, либо RGB-цвета. Недостатками использования такой формы представления цвета являются:

  • Система не понятна интуитивно. Мы не разделяем цвет отдельно на красный, зеленый и синий и не приводим цвет в шестнадцатеричную систему счисления, да и не говорим, например, Кремль цвета #ff0000.

  • Отсутствует поддержка. Дизайнерам может понадобиться 10 типов одного цвета, а в HEX и RGB нет никакой привязки к оттенкам.

HSL (hue, saturation, lightness)

Цвет в HSL представлении определяется тремя значениями:

  • тоном (оттенком, hue);

  • значением (светлотой, яркостью, value);

  • хромой (цветностью, насыщенностью, chroma, saturation).

Существует трехмерная колометрическая система Манселла. В ней цвет определяется с помощью трех координат.

Колометрическая система МанселлаКолометрическая система Манселла

Если составляющие цвета переименовать, мы получим следующую картину:

Выбор цветовой схемы

Темизация, white label, редизайн вот перечень основных пунктов, с которыми может столкнуться команда разработки. И чтобы эти пункты не превратились в проблемы, нужно с самого начала разработки предпринять ряд шагов.

1. Выбор типа цветовой схемы

Итак, какие же бывают цветовые схемы, согласно теории цвета:

  • Монохромные (используется один основной цвет и его оттенки)

https://codepen.io/gevara2015/pen/xxVdooe

  • Комплиментарная схема берутся цвета, расположенные на противоположных сторонах цветового круга. Сочетание таких цветов выглядит очень живо и энергично. Такую схему лучше не брать для создания текстовых композиций. Лучше всего использовать ее, когда необходимо что-то выделить или подчеркнуть.

Всего же существует 5 цветовых схем:

  1. Монохроматическая схема (один основной цвет).

  2. Добавочная схема (два основных цвета).

  3. Триадная схема (три основных цвета).

  4. Тетраэдная схема (четыре основных цвета).

  5. Примыкающая схема (два или три основных цвета).

Не будем тратить время на подробный разбор каждой схемы, это сделано вот здесь, перейдём ближе к коду.

2. Именование переменных

Большинство именований переменных цвета, которые есть сейчас: accent, base, high, button-contrast-alpha, positive, negative, faint, success, warning... множество их. И почти к каждому обозначению есть вопросы.

  1. Возьмём, к примеру, high относительно чего он high, есть ли low, насколько high больше и по какому параметру low?

  2. Button-contrast-alpha. Если следовать логике этого именования, должен быть ряд alpha, beta, gamma. Опять же вопрос: чем они будут отличаться и в какой степени? Какой цвет будет иметь средние значения?

  3. Низкий уровень абстракции: button, text, link и т. д.

По именованию, мне кажется, нужно придерживаться ряда правил:

  • Вместо alpha, beta, gamma использовать strong, base, weak.

  • Primary (основной цвет бренда).

Учитывая множество абстракций в именовании (в имени переменной нельзя указывать конкретный параметр элемента разметки или сам элемент), я подумал о том, что было бы неплохо придерживаться именования по слоям (мало кто вспомнит аддон Tilt в Firefox). Чтобы отличить один элемент от другого, достаточно представить, что он находится просто уровнем выше. А чтобы он выделялся, нужно ему задать этот самый цвет подложки, что-то наподобие background и foreground.

Mozilla tilt addonMozilla tilt addon

И тут я наткнулся на объяснение принципов именования переменных для Material Design.

  1. Background (0dp elevation surface overlay)

  2. Surface (with 1dp elevation surface overlay)

  3. Primary

  4. Secondary

  5. On Background

  6. On Surface

  7. On Primary

  8. On Secondary

2.1 Разделение именований

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

https://codepen.io/gevara2015/pen/poywzLj

То есть для описания свойств элементов нам будет достаточно следующего набора переменных:

/* elements variables */--global-background: var(--bg);--surface: var(--bg-weak);--on-color: #fff;--on-background: var(--contrast-weak);--on-primary: var(--on-color);--on-secondary: var(--on-color);--on-error: var(--on-color);--on-surface-prime: var(--contrast-weak);--on-surface-second: var(--contrast-strong);--input-background: var(--bg-weak);--input-outline: inset 0 0 0 1px var(--secondary-strong);--component-outline: none;

Также стоит отметить, что для описания границ элемента лучше использовать не border, а box-shadow, так как он является более комплексным (можем применить сразу хоть три значения тени):

--component-outline: 15px 17px 26px -4px rgba(34, 60, 80, 0.6), 15px 17px 19px -11px rgba(20, 117, 191, 0.6), -22px -36px 19px -11px rgba(56, 167, 103, 0.6);

Плюс он не меняет размер всей кнопки или, к примеру, инпутау (если мы берем за исходные данные box-sizing: border-box). Мне кажется, что это является хорошим решением, так как разработчики описывают для каждого компонента набор переменных, а дизайнер в дальнейшем может играться с цветами или цветовыми схемами, с дизайнерскими подходами (skeuomorphism, neumorphism) или даже использовать LCH цвета, все на усмотрение дизайнера.

Lea Verou в своей статье указывает на вариант конвертации цветовой схемы для темной темы с помощью L (светлота) параметра в HSL формате. В итоге для светлой темы получается лесенка:

 --l-0: 0%;--l-30: 30%;--l-40: 40%;--l-50: 50%;--l-90: 90%;--l-100: 100%;

а для тёмной обратная лесенка переменных:

@media (prefers-color-scheme: dark) {:root {--l-0: 100%;--l-30: 70%;--l-40: 60%;--l-90: 10%;--l-100: 0%;}}

И казалось бы, все логично, можно просто определить текущий параметр светимости как формула 100% - lightness, и он будет средним как для темной, так и для светлой темы. Однако у hsl есть недостаток. В светлой теме будет недостаточно контраста для ряда элементов. Решение, предложенное Lea Verou c использованием LCH цветов, как мне кажется, еще слишком сырое, это все еще драфт в спецификациях w3c.

Выводы

  1. Для того, чтобы грамотно использовать цветовые схемы, в которых цвета не будут сливаться (или будет недостаточная контрастность) нужно разделить переменные "для дизайнеров" и переменные "для разработчиков".

  2. Настройка цветовой схемы лежит полностью на дизайнере, и он отдает всего лишь итоговый файл конфиг.

  3. Компания Stripe сделала инструмент для внутреннего использования в своих продуктах, но для разработки подобного инструмента нужны ресурсы.

Автор: Александр Танцюра, Frontend Developer в Space307.

Подробнее..

Как стать Senior Front-End разработчиком. Советы из личного опыта

07.05.2021 18:23:01 | Автор: admin

Всем привет. Меня зовут Олег, я Senior Front-End разработчик в компании Genesis. Хочу начать с утверждения, что карьера front-end разработчика может достаточно динамично развиваться, если прикладывать к этому определенные усилия. В профессии я 5 с лишним лет. В этой статье я хочу поделиться своим опытом, который будет полезен как начинающим разработчикам, так и тем, кто уже имеет определенный опыт в front-end разработке.

Мотивация

В моем жизненном опыте существуют два уровня мотивации относительно карьерных перспектив на старте пути.

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

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

Мой первый профессиональный опыт

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

Изначально я, как среднестатистический подросток 19 лет, попросту прожигал свое время, не понимая, чем хочу заняться в будущем. Во мне зрела идея того, что нужно добиться большего и начать применять знания, которые я получил благодаря нашей образовательной системе. Собравшись с духом, я решил обратиться к своему старому другу за помощью и советом. Он подсказал мне, что нужно начать с выбора языка, а также очертил набор базовых знаний, необходимых для прохождения собеседования. Также, он выступил как мой первый, хоть и условный, заказчик, который писал мне ТЗ для домашних проектов. С его помощью, мне удалось подготовиться для собеседования на должность full-stack разработчика. После нескольких неудачных попыток, мне все же удалось найти ту компанию, которая поверила в меня и дала старт в IT-мире.

Это был важный период в моей карьере, во время которого мне довелось пообщаться с большим количеством разноплановых заказчиков и научиться находить общий язык с коллегами разного уровня и возраста. Также, у меня появилась возможность самостоятельно разрабатывать проекты; впрочем, не без набивания шишек и исправления собственных ошибок по выходным.

Со временем я начал чувствовать в себе больше уверенности и сил совладать с более сложными задачами. Переосмысливая свою текущую деятельность и общие предпочтения, я принял решение, что создавать интерфейсы мне ближе и приятнее. В итоге я получил замечательный опыт, выпустил в свет несколько небольших проектов и познакомился с большим количеством интересных людей с разными взглядами на работу и жизнь. Самое главное я начал чувствовать себя уже не студентом, а полноценным программистом, который готов покорять новые front-end вершины.

Топ-3 урока, которые я вынес из своего первого опыта

  1. Поначалу, все боятся. Первое время у меня было ощущение, что меня уволят, что я ничего не смогу или делаю что-то не так. Эти чувства испытывают многие начинающие разработчики, и это нормально. Главное это научиться работать с ними (концентрироваться на результате, доверять себе и своим решениям) и не давать им себя поглотить, стойко выдерживая все сложности.

  2. Пообщаться с людьми иногда полезнее прежде, чем писать код. Когда я получал свои первые задачи, я сразу рвался писать код и думать, как это реализовать, не осознавая, что дьявол кроется в деталях. Проводя больше времени на уточнение всех особенностей задачи и поиске подводных камней, я стал учитывать больше особенностей на ранних этапах и совершать меньше ошибок, которые пришлось бы переделывать. Например, уточнив все особенности фичи, я понял, что она имеет коллизию с другим функционалом, что не позволило бы пользователю полноценно использовать нужный ему интерфейс.

  3. Нужно давать себе отдыхать. Это может быть достаточно банальный совет, но для меня на тот момент он не был очевиден. Чем больше вы выматываетесь, тем меньше пользы вы принесете как для проекта, так и для себя.

Векторы развития Front-End разработчика

Первый профессиональный опыт развил мое понимание того, что проекты могут быть структурно поделены в таких направлениях как B2C, e-commerce, fintech и т.д. Это улучшило мое мышление как разработчика о необходимости анализа требований проекта и восприятия того, что ты делаешь со стороны пользователя. Соответственно, для себя я смог определить несколько дальнейших направлений в развитии.

Одно из направлений техническое, с такими профессиями как: Tech-lead, Principal Engineer, Head of Engineering и т. д. Особенностями данной ветки развития являются глубокие знания технических спецификаций разработки, создание архитектуры для разных приложений, а также умение выбирать и адаптировать нужные технологии под тип поставленной задачи.

Другое направление менеджерское, с такими профессиями как: Team-lead, Dev-manager, Project manager и т.д. Данное направление отличается от технического более углубленным общением с людьми. Необходимо уметь строить команду, раскрывать потенциал сотрудников, а также анализировать и понимать потребности бизнес части проекта.

Проблемы, которые могут возникнуть

Адаптация на проекте. Это проблема возникает у всех независимо от опыта работы. Поначалу вы всегда привыкаете к проекту, стилю написания кода, построению бизнес-процессов и к людям, вовлеченным в этот проект. Очень круто, если вам повезло, и вы попали на проект с детальной документацией. В этом случае в первую очередь стоит уделить как можно больше времени на ее прочтение так у вас в будущем возникнет меньше вопросов. К сожалению, в моем опыте это случалось не часто. В основном, документация либо вообще отсутствовала, либо присутствовала в минимальном объеме. Тем не менее, в подобных ситуациях не стоит расстраиваться, поскольку при правильном подходе можно быстро сформировать общее понимание о проекте. Вначале, необходимо провести встречи с менеджером, который руководит проектом, уточнив:

а) процессы разработки;

б) формирование команды;

в) текущее состояние проекта.

Также, будет полезно пообщаться с программистом, который построил архитектуру данного приложения или работает на проекте дольше всех. У него вы сможете узнать:

а) основные принципы построения архитектуры проекта;

б) базовый набор используемых библиотек;

в) стилистические особенности при написании кода и т.д.

На начальных этапах не нужно стесняться брать самые простые задачи (например, исправление мелких багов или внесение незначительных улучшений), которые уже долгое время находятся в backlog. Также, отличной идеей может стать написание тестов. Это поможет вам начать разбираться в проекте с минимальными рисками даже при отсутствии детальной документации.

Период адаптации в среднем длится от одного до трех месяцев. Тем не менее, если проект объективно большой либо содержит сложную бизнес-логику, его изучение может затянуться на более длительный срок. В данном случае, очень важно построить план и поэтапно следовать ему. Более того, не нужно стесняться задавать глупые вопросы коллегам; возможно, так вам будет легче изучить сложные аспекты проекта.

Переработка. Это достаточно сложная проблема, которая может испугать начинающих разработчиков. Для себя я определил несколько причин из-за которых происходит переработка:

а) переоценка возможностей;

б) необходимость проекта;

в) эксплуатация сотрудников.

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

Иногда бывают ситуации, когда переработок требует определенный проектный период. Особенностью данных случаев является то, что переработка не должна длиться долго. Обычно, это исчисляется днями или неделями, но в редких случаях несколькими месяцами. Часто бывают ситуации, когда перед крупным релизом или выходом на продакшн, особенно в небольших компаниях, необходимо работать больше и интенсивнее обычного, чтобы проект получился идеальным. К таким ситуациям стоит относиться лояльно и стараться достичь поставленных целей вовремя. В моем опыте осознание необходимости проекта сильно помогало расти как специалисту, приносило опыт работы в стрессовых ситуациях и поднимало ценность в глазах работодателя.

Третья причина переработки это эксплуатация. К сожалению, на рынке ИТ данная проблема до сих пор встречается. При эксплуатации работодатель может накладывать бесконечный груз ответственности на сотрудника; давать объем работы, который объективно не соответствует выделенному времени; обесценивать ваш вклад в развитие продукта, и постоянно обещать улучшение рабочей обстановки в ближайшем будущем. Если положительной динамики не наблюдается, я бы рекомендовал перейти в другую компанию. Более спокойная ситуация в другой обстановке и на другом проекте будет полезнее для развития и сохранит ваши нервы.

В общем и целом, в процессе развития могут возникать разнообразные проблемы. Главное, на мой взгляд, не поддаваться панике, а принимать их как вызов. Проработанные планы решения проблемы, а также правильно сделанные выводы помогут вам стать высококвалифицированным специалистом.

Роль обучения

Основное образование и литература. Ключевую базу знаний я приобрел из колледжа и университета. Но я однозначно уверен в том, что будущий front-end разработчик не должен фокусироваться на книгах по программированию образца 1994 года, поскольку это сфера достаточно динамично развивается и меняется. Но при этом, некоторые технические сведения все равно можно почерпнуть из ранних публикаций таких как: Design Patterns: Elements of Reusable Object-Oriented Software, Clean Code: A Handbook of Agile Software Craftsmanship и других.

Их прочтение необходимо для общего понимания того, как организован процесс разработки и как строить сложные, устойчивые системы. При этом, необходимо искать доступ к более современным ресурсам, например egghead, frontendmasters.com и множеству других. Однако, большинство ресурсов достаточно редко адаптированы на русский и, еще реже, украинский языки.

Дополнительные знания и социальные сети. Для приобретения современных знаний, я так же подписался на несколько социальных каналов тематического направления. К примеру, это несколько каналов в Twitter, таких как JavaScriptDaily или CSS-Tricks и блоги людей, например, Dan Abramov / Kent C. Dodds, связанных с обновлением или улучшением репозиториев библиотек для программных разработок, которые мы, как команда, сейчас используем. Но при этом, важно помнить, что на более высоких уровнях должностей, очень важно правильно и доступно презентовать идеи своей деятельности как уверенного программиста. Поэтому, в последнее время я обращаюсь к тем самым ранним публикациям, которые связаны с определенными паттернами для разработки, нежели исследую техническую составляющую. Также, несколько каналов на YouTube, которые я сейчас использую:

  1. webDev современный русскоязычный канал, в котором все о современных трендах, и простое описание сложных принципов в программировании.

  2. DesignCourse прекрасный англоязычный канал, который показывает проекты со стороны дизайнера. Для front-end разработчика это очень ценная информация, так как профессия предполагает тесное сотрудничество с дизайнерами.

  3. Fireship условно развлекательный контент на английском, который вкратце рассказывает об front-end новостях.

  4. JSConf кладезь всего что есть сейчас в JS-мире, лекции из самых популярных JavaScript конференций.

Требования к знаниям

Требования к junior позициям. Возвращаясь к требованиям для начинающих программистов, я отмечу, что багаж знаний даже для начинающей должности должен быть значительно выше того, чем был в начале моей карьеры. Люди, которые собеседуют потенциальных соискателей, стали более требовательны, так же, как и динамика рынка стала более высокой благодаря возрастающему интересу к программной разработке в целом. Понимание структуры и логики языка программирования тоже очень важна, поскольку для человека, который не владеет нею, займет очень много времени для дальнейшего изучения. При этом, проект сам по себе может требовать более интенсивной вовлеченности и расширенной базы знаний. Также, понимание причинно-следственных связей, например, осознание роли динамических библиотек и их изменений для программного кода, стали более весомы даже для уровня тех, кто начинает карьеру во front-end.

Разница между middle и senior позициями. Если говорить о разнице между требованиями к знаниям, то junior-разработчики должны владеть отличным пониманием теоретической части, например, как работает язык, как работает браузер, как это все взаимосвязано и т.д. Также, у младших специалистов может отсутствовать понимание того, как взаимодействовать с командой, или как строить сложные системы, которые будут легко масштабироваться и будут гибкими для изменения.

В то же время, специалисты уровня middle это своего рода эксперты в использовании методов разработки и успешном применении их к определенной бизнес-проблеме. Для специалистов этого уровня важно понимать принципы работы в команде и пользу взаимодействия друг с другом.

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

Тем не менее, это видение может измениться в будущем и многие вещи идут к усложнениям, поскольку JavaScript, как основная концепция для front-end разработки остается прежней, но количество библиотек, приложений, и методов их применения динамично растет.

Роль английского

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

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

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

Управление людьми и лидерство

Одна из главных проблем у разработчиков, как в моем, так и в случае других людей, которых я знаю это в достаточно молодом возрасте освоить soft skills. Они предполагают управление командой, общение с требовательными заказчиками, и решение рабочих конфликтов.

Из личного опыта, мое управление конфликтами отчасти строилось на примере общения с опытным разработчиком, работающим в данной сфере более 15 лет и имеющим определенное влияние на другого сотрудника, который также имел свою личную, местами неконструктивную точку зрения на ход событий. Для разработчиков это типичный пример стагнации, где лидерство теряет смысл, если человек сфокусирован на своей личности, нежели чем на необходимости командного духа и ориентации на желания клиента. Дополнительно, на senior уровне я начал читать литературу по психологии и групповой терапии, что позволило мне мыслить более открыто и конструктивно.

Мои советы Front-End разработчикам

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

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

  3. Ищите точки соприкосновения с людьми разных поколений, поскольку коммуникация так же важна, как и техническая экспертиза.

  4. Ну и, конечно, работайте над своими знаниями и разговорной речью мы хоть и работаем в технической сфере, но гуманитарная, или soft skills часть тоже важна для быстрого карьерного роста.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru