Pull to refresh

Создание пользовательских компонент для Bootstrap 4

Reading time10 min
Views14K


Общественное мнение перевело Bootstrap в категорию легендарных фреймворков прошлого, но следить за ним все ещё стоит. Bootstrap 4 — отличный навигатор по безопасной верстке, и главное, образец HTML over JS подхода к созданию веб-приложений, в полной мере раскрывает существующие возможности HTML для декларативного описания интерфейса пользователя.


И о том как развивается JavaScript код фреймворка тоже полезно иметь представление. Архитектура jQuery плагинов все еще используется но с 4ой версии это завернутые Rollup'ом в пакет классы ES6 транспиленные при помощи Babel6. jQuery вероятно скоро и не будет вовсе — об этом позже — а пока, на примере создания собственного плагина BsMultiSelect, на том же стёке что и Boostrap 4, будут раскрыты особенности развития фреймворка.


Почему "каждый программист должен написать свой ..."


"Улучшать хорошее только портить". Победила потребность понять, что нового получил Bootstrap переведя свои компоненты на новый стёк, и насколько легче теперь создавать компоненты высокого порядка. Казалось должно быть всё просто: скомбинируй .form-control, .badge и .dropdown-menu и легко получишь мультиселект…



Конфликт архитектур с порога


Получившийся плагин BsMultiSelect напрямую JS код bootstrap.js не использует, а использует его зависимость — popper.js (ванильный фреймворк всплывающих элементов). Это потому что dropdown компоненте из bootstrap.js нужно при инициализации в DOM найти так называемый toggle элемент (в частном случае это кнопка открывающая меню), если не находит — падает с ошибкой (раз я создаю напрямую из js то и "кнопки" у меня конечно нет). Вывод: компоненты заточенные под "HTML over JS" невозможно (или невыгодно) создавать из JS напрямую. Вывод номер два: программировать пользовательские компоненты надо так, чтобы под "HTML over JS" компонентой, всегда была более легкая компонента "чистого JS". У меня это MulitSelect.js полностью очищенная не только от кода но и от стилей Bootstrap'а. MulitSelect.js играет туже роль для BsMulitSelect.js, что и popper.js для dropdown.js.


Два слова о popper.js


Отличная библиотека — одна функция. DropDown.js повторяет но предоставляет совсем ограниченный интерфейс к ней (опять же потому что в "HTML over JS" весь API и не нужен). У popper'а больше открытых issue чем хотелось бы. Меня огорчила тем, что под IE11 публикует события не так как под Chrome.


О переходе от IIFE к class'ам


Стандартный бойлерплейт JQuery Pluginа — это определение функции конструктора внутри IIFE. Теперь же это class внутри IIFE. Открытие: старые функциональные способы делать private методы конфликтуют с лямбдой ES6 поэтому от них в Bootstrap "отказались". Все методы публичные, а "псевдо-приватные" помечают префиксом _ т.е. underscrore. Понимаю что не прав, но следую своим соглашениям — "публичные методы с большой буквы". Во всем прочем BsMultiSelect создавался перенимая соглашения "стандартного" DropDown.js, т.е. я бы хотел его представлять этаким boilerplate'ом с мясом.


впрочем, еще одно отличие, раз есть rollup, бью на файлы..
import $ from 'jquery'
import AddToJQueryPrototype from './AddToJQueryPrototype'
import MultiSelect from './MultiSelect'
// ...

(
    (window, $) => {
        AddToJQueryPrototype('BsMultiSelect',
            (element, optionsObject, onDispose) => {
                // ...
                return new MultiSelect(element, optionsObject, onDispose, facade, window, $);
            }, $);
    }
)(window, $)

Новое соглашение: плагин должен публиковать Constructor — вот чтобы не потерять конструктор внутри IIFE. Да, одно IIFE должно остаться — задачу стандартно определять зависимости window, $ и Popper (для тех кто "не может в модули") никто не снимал.


Конфигурации линтеров Eslint и Stylelint


Я себя не почувствовал обязанным следовать драконовским правилам стилизации кода Bootstrap’а. Наверняка они пишут в среде которая им помогает в деле расстановки пробелов и для source control строгости очень хорошо, но мне тяжело ежесекундно выслушивать скорбь линтера в VS Code. При написании собственного плагина правила можно и нужно сильно ослабить.


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


Организация кода и rollup


Оттранспиленный ES6 код Bootstrap’а пакетируется rollup’ом в две версии: standalone bootstrap.js и bundle bootstrap.bundle.js — последний включает в себя popper.js.


Чуждыми мне инстинктами обладает автор rollup.config.js. Вся эта трансформация конфигурации через push и pop, в зависимости от требуемой версии standalone или bundle, выглядит вычурной и пугающей, к счастью для написания плагина нужен только один тип бандла "standalone" и мне не придется доказывать что я могу лучше.


Standalone версия Bootstrap'а указана и в main field пакета т.е. npm зависимости разрешаются в тот же самый файл который грузят пользователи через <script>. Это не такое уж и однозначное решение, но мой плагин BsMultiSelect последовал такому же пути. В плагин в таком случае могут быть включены babel helpers, несмотря на то что в bootsrap.js они тоже включены. Дублирование это как-то не правильно…


Что интересно у Bootstrap параллельно есть конфигурация под jspm — native browser ES module loader и его main field это уже чистый оттранспиленный код, 12 компонент, не объединенные в бандл, а сохраненные отдельно в каталог js/dist. Здесь каждый файл включает babel helper’ы. Jspm сейчас на распутье так что создавая BsMultiselect его игнорирую.


И все же интересно спросить у знатоков, неужели jspm собирается все 12 раз продублированные хелперы бабеля поднимать в браузер?

Обнаружен большой плюс rollup’a — его пакеты читабельны в отличии от пакетов webpack’а


Bootsrap 4 использует Babel 6 а плагин BsMultiSelect Babel 7


BsMultiSelect легко мигрировал на бету Babel 7 при помощи обновленного rollup-babel-plugin’а (тоже в бетте). На Babel 7 стоит переезжать уже сейчас из за нового @babel/preset-env (конфигурации транспилера "на каких броузерах будет запускаться код"). Разобраться при помощи только интернета как работают в ES6 preset’ы стало довольно сложно, они прошли длинную эволюцию скопилось множество устаревших конфигураций. Проще сразу ставить бету Babel 7 + @babel/preset-env


Все же, ошибок беты babel’я долго ждать не пришлось: [...nodeList] было танспилено в nodeList.concat(). Долго не разбирался, перевод кода в вызовы jquery (это все равно обязательно, но об этом далее) решило проблемы. Спросите: "Зачем тогда вообще Babel если с ним не разбираться?", ответ: "импорты и лямбда".


Практические аспекты


У Bootstrap 4 и его плагина (как и у любого js кода) есть два варианта загрузки в браузер: через script/style (создаем "страничку"), и через сборку node/webpack (создаем "приложение", так я буду далее эти две в принципе очень разные ситуации и обозначать: “страничка” и "приложение").
Для странички и для приложения будет использоваться один и тот же "файл-дистрибутив" Bootstrap. Это не очевидное и не единственное решение (как пример, для jspm и для тестов mocha используется другой код), но, создавая собственный плагин, ничего другого не остается, как следовать этому соглашению, надеясь на его оптимальность.


Поиск баланса между удовлетворением двух потребностей использовать один и тот же код для двух разных вариантов загрузки: "странички" и "приложения" — и является основной архитектурной задачей которая стоит перед разработчиком плагина.


Плагин к Bootstrap'у ведь можно создавать и ad hoc, прямо в вашем приложении. В таком случае можно писать код плагина со всеми удовольствиями: полифилами и доступу к SASS переменным схемы. Однако когда в расчете на переиспользование надо будет произвести перенос плагина в его собственный npm пакет, перечисленные удовольствия переходят в разряд проблем: теряется доступ к переменным SASS и надо иметь в виду создателей "страничек" подключающих плагин через script (где мои полифилы?).


Доступ к SASS переменным стиля Bootstrаp 4


Хотелось бы чтобы для написания плагина хватало css классов/селекторов Bootstrap'а. В таком случае при кастомизации темы Bootstrаp'а — можно ожидать что плагин будет адаптироваться без дополнительных усилий.


Для самостоятельно публикуемого плагина доступа к переменным SASS нет.


Существующее css классы/селекторы — не покрывают все необходимые стили для BsMutliSelect, например, нужен min-height для ul.form-control, когда в bootstrap.css существует только css селектор c height для input.form-control — ну никак не применишь его для ul. Другие существующие классы/селекторы приносят много лишних стилей: что если вы хотите например color от .form-control назначить элементу без назначения класса .form-control? Это не получится так как нет такого класса как .input-color в котором переменная $input-color была бы опубликована “отдельно” от всего.


Далее следовать решениям команды Bootstrap'а не возможно: они такой проблемы не имеют, если им нужно — они создают себе СSS класс/селектор с нужной переменной и оперируют им. При желании подискутировать — объясняться вам придется с Mark Otto, в комманде BS — абусрдно высокий уровень принятия решений даже по косметическим изменениям в селекторах CSS, если запросы попадают в категорию "предложение".


Я предлагаю следующее решение.


Для странички (поднимающей плагин через script) c недоступностью переменных приходится смириться. Но и предлагать разбираться с CSS не обязательно — недоступные переменные можно предложить кастомизировать через параметры js, благо их не много (и не все они меняются всякой схемой). Для BsMultiSelect проблематичным были: цвет disabled, цвет input, focused control shadow (и т.д. всего 10 стилей, большинство из них редко меняются схемами)...


За одно это декларация всех стилей от которых зависит плагин...
$("select[multiple='multiple']").bsMultiSelect({
              selectedPanelDefMinHeight: 'calc(2.25rem + 2px)',  // default size
              selectedPanelLgMinHeight: 'calc(2.875rem + 2px)',  // LG size
              selectedPanelSmMinHeight: 'calc(1.8125rem + 2px)', // SM size
              selectedPanelDisabledBackgroundColor: '#e9ecef',   // disabled background
              selectedPanelFocusBorderColor: '#80bdff',          // focus border
              selectedPanelFocusBoxShadow: '0 0 0 0.2rem rgba(0, 123, 255, 0.25)',  // foxus shadow
              selectedPanelFocusValidBoxShadow: '0 0 0 0.2rem rgba(40, 167, 69, 0.25)',  // valid foxus shadow
              selectedPanelFocusInvalidBoxShadow: '0 0 0 0.2rem rgba(220, 53, 69, 0.25)',  // invalid foxus shadow
              inputColor: '#495057', // color of keyboard entered text
              selectedItemContentDisabledOpacity: '.65' // btn disabled opacity used
          });

А вот для приложения использующего SASS и сборщик можно предложить скопировать BsMultiSelect.scss к себе, и подправить в нем путь к вашему _variables.scss. Таким образом плагин "получит" переменные схемы.


Все через CSS...
          $("select[multiple='multiple']").bsMultiSelect({
                         useCss: true
                     });

Эти два способа записаны в моем JS коде явно, через два “адаптера”. BsMultiSelect это собственно два плагина в одном. Один который работает со стилями через переменные js, другой делегирует эту работу css.


При этом надо сказать прямо, что работа с CSS может понадобится даже если использовалась передача кастомизированных стилей через JS параметры, просто потому что авторы схем обычно делают свою работу очень грубо. Или странно. Вот например схема Skethy почему-то решила что меню не нужен hover effect. Фичи приходится обрабатывать напильником.


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


адаптация под схему через js
$("select[multiple='multiple']").bsMultiSelect({
            selectedPanelFocusBoxShadow:"0px 0px 0px 0.2rem rgba(51,51,51,0.25)",
            selectedPanelFocusBorderColor:"#333",
            selectedPanelDisabledBackgroundColor: "#f7f7f9"
});


Это и есть максимум того что можно достигнуть используя "стандартные" CSS классы .bage, .form-control, .close и т.д. Почти всё адаптировалось под схему, но не всё.


Жаль конечно, что от этих трех параметров не уйти, не все переменные стилей схемы доступны через селекторы/классы css (вру, уйти-то можно если есть возможность скопировать BsMultiSelect.scss к себе и подвести ему переменные схемы, но это не до конца удовлетворяет, как и все решения, что начинаются со слов "скопируйте к себе").


Покликать можно здесь: https://dashboardcode.github.io/BsMultiSelect/indexsketchy.html


Polyfill’ы


В сценарии “пишу ad hoc плагин внутри приложения” — вы будете без проблем использовать @babel/polyfill, а DOM пропатчите polyfill.io потому что внутри вашего приложения это все уже есть.


При выносе плагина во внешний npm module — вся радость исчезает потому что пользователей Bootstrap пока не принято радовать длинными списками требуемых полифиллов в документации.


Решение: все что babel не будет транспилить а будет оставлять полифилам, не дать ему это сделать, скрипя зубами, переделывать в вызовы jQuery (например Node.closest в $.closest). Такой подход принят в самом Bootstrap 4 и очевидно это борьба с разбуханием. Сюрприз. jQuery — это полифил.


Соглашусь, программирование Babelем с оглядкой на CanIUse.com и MDN.com удовольствия не доставляет. Да и код получается неловким (у меня так).


Тут надо осветить ситуацию сверху еще раз. Bootstrap использует две библиотеки jQuery и popper.js. Popper.js — это дважды ванильный фреймворк, в нем нет ни babel’я, ни jQuery, ни внешних polyfillов, всё сам. JQuery пакетируется gruntом уже используя babel, но каким-то волшебным образом без его polyfill’ов и helper’ов. Сторонний плагин Bootstrap’а (как BsMultiSelct) использует Babel 7. Ваш продукт может использовать еще что-то. Единой платформы полифилов нет, ожидаемо множественное дублирование кода, штуки четыре реализации того же closest загруженных в броузер. Но поделать с этим ничего нельзя.


Если вы все же использовали полифилы при создании собственного плагина, надо помнить, что если в приложении есть инлайн скрипты ( т.е. <script>$( function() {/*..*/}) </script>) — то загружать полифилы придется только синхронно (иначе инлайн код может начать исполнение ранее того момента когда плагин загружен). Т.е. например webpack-polyfill-injector (самый богатый по возможностям из мне известных injector полифилов) надо контролировать так чтобы он не решил грузить полифилы асинхронно (поведение по умолчанию).


No jQuery


jQuery ощущается чем-то токсичным, конкурирующим с Babel/ES6 и затеняющем его, но это не навсегда. Есть branch v4-whithout-jquery, он уже в pool request, в планах выпустить bootstrap-nojquery c версии 4.3. Ранее самой большой проблемой отказа от jquery назывались namespace событий, хотя и очевидно что можно писать без них (BsMultiSelect обошелся). Видимо решились.


Также объявлено что с версии 4.3 оригинальные компоненты будет возможно грузить в броузер по отдельности (только для сценария "приложение"). Станут ли тогда оригинальные компоненты "как BsMultiSelect"? Нет. Если команде BS будут нужны какие-то переменные SASS — понаделают под себя классы/селекторы CSS и будут оперировать ими из JS ("функциональный CSS"). Создателям пользовательских компонент такие возможности не доступны.


Возможный переход с sass на post-css https://github.com/twbs/bootstrap/projects/11 в 5ой версии принципиально ничего не меняет. Или меняет всё: "JS over HTML".


Bootstrap 5 придется установить рекомендации как интегрироваться в полифилы Babel, и как патчить DOM. Тут прогноз более-менее ясен: создателям страничек и создателям приложений будут предложены совершенно различные сборки.


Итоги


Пока пишешь компоненты Bootstrap 4 на Babel методом ad hoc не задумываясь о переиспользовании, а значит о полифилах, и о том где взять переменные стилей — горя не знаешь.


Но если делать плагин отдельным npm пакетом — и с желанием сделать решение в духе самого Bootstrap 4 удовольствия становится меньше, задумываешься чаще и дольше. Вероятно придется сразу определиться — кто ваши пользователи, и предлагать различные сборки, одну для тех, кто будет собирать приложение в бандл, другую для пишущих странички, т.е. загружающих плагин script'ом.

Tags:
Hubs:
+7
Comments0

Articles