Создание пользовательских компонент для Bootstrap 4
- пятница, 6 июля 2018 г. в 06:49:30
Общественное мнение перевело Bootstrap в категорию легендарных фреймворков прошлого, но следить за ним все ещё стоит. Bootstrap 4 — отличный компас в безопасной верстке, и главное, остается образцом HTML over JS декларативного подхода к созданию веб-приложений, раскрывает существующие возможности HTML для описания интерфейса пользователя.
И о том как развивается JavaScript код фреймворка тоже полезно иметь представление. Архитектура jQuery плагинов все еще используется но с 4ой версии это завернутые Rollup'ом в пакет классы ES6 транспиленные при помощи Babel6. jQuery вероятно скоро и не будет вовсе — об этом позже — а пока, на примере создания собственного плагина BsMultiSelect, на том же стеке что и Boostrap 4 будут раскрыты особенности развития фреймворка.
Существующие мультиселекты (имитирующие input) не устраивали, можно было пытаться их улучшать, но была потребность понять, что нового получил Bootstrap переведя свои компоненты на новый стёк, и насколько легче теперь создавать компоненты более высокого порядка. Казалось должно быть всё просто: скомбинируй .form-control
, .badge
и .dropdown-menu
…
Получившийся плагин BsMultiSelect напрямую JS код bootstrap.js не использует, а использует его зависимость — popper.js (ванильный фреймворк всплывающих элементов). Это потому что dropdown компоненте из bootstrap.js нужно при инициализации в DOM найти так называемый toggle элемент (в частном случае это кнопка открывающая меню), если не находит — падает с ошибкой (раз я создаю напрямую из js то и "кнопки" у меня конечно нет). Вывод: компоненты заточенные под "HTML over JS" невозможно создавать из JS напрямую. Ладно, popper.js так popper.js.
Отличная библиотека. Однако у нее больше открытых issue чем хотелось бы для такой узкоспециализированной функции. Меня огорчил тем, что под IE11 он публикует события не так как под Chrome.
Стандартный бойлерплейт JQuery Pluginа — это определение функции конструктора внути IIFE. Теперь же это class внутри IIFE. Открытие: старые функциональные способы делать private методы конфликтуют с лямбдой ES6 поэтому от них в Bootstrap "отказались". Все методы публичные, а "псевдо-приватные" помечают префиксом _ т.е. underscrore. Понимаю что не прав, но следую своим соглашениям — "публичные методы с большой буквы". Во всем прочем BsMultiSelect создавался перенимая соглашения "стандартного" DropDown.js, т.е. я бы хотел его представить "этаким" boilerplate'ом.
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 никто не снимал.
Я себя не почувствовал обязанным следовать драконовским правилам стилизации кода Bootstrap’а. Наверняка они пишут в среде которая им помогает в деле расстановки пробелов и для source control строгости очень хорошо, но мне тяжело ежесекундно выслушивать скорбь линтера в VS Code. При написании собственного плагина правила можно и нужно сильно ослабить.
Eslint, stylelint выходят может и не слишком часто, но просматривать новые правила — нет никакого желания. Во всех аспектах меня это достижение больше раздражает.
Оттранспиленный ES6 код bootstrap’а пакетируется rollup’ом в две версии: standalone bootstrap.js и bundle bootstrap.bundle.js — последний включает в себя popper.js.
Чуждыми мне инстинктами обладает автор rollup.config.js. Вся эта трансформация конфигурации через push и pop, в зависимости от требуемой версии standalone или bundle, выглядит вычурной и пугающей, к счастью для написания плагина нужен только один тип бандла "standalone" и мне не придется доказывать что я могу лучше.
Standalone версия Bootstrap'а указана и в main field пакета т.е. зависимости разрешаются в тот же самый файл который грузят пользователи. Это не такое уж и однозначное решение, но мой плагин 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’а
BsMultiSelect легко мигрировал на бету Babel 7 при помощи обновленного rollup-babel-plugin’а (тоже в бетте). На Babel 7 стоит переезжать уже сейчас из за нового babel/preset-env (конфигурации транспилера "на каких броузерах будет запускаться код"). Разобраться при помощи только интернета как работают в ES6 preset’ы стало довольно сложно, они прошли длинную эволюцию скопилось множество устаревших конфигураций. Проще сразу брать последний babel/preset-env.
Все же, ошибок беты babel’я долго ждать не пришлось: [...nodeList] было танспилено в nodeList.concat(). Долго не разбирался, перевод кода в вызовы jquery (это все равно обязательно, но об этом далее) решило проблемы. Спросите: "Зачем тогда Babel если с ним не разбираться?", я уже ответил: "Импорты и лямбда".
У bootstrap 4 и его плагина (точнее у любого js кода) есть два варианта загрузки в броузер: через script/style (создаем "страничку"), и через сборку node/webpack (создаем "приложение", так я буду далее эти две в принципе очень разные ситуации и обозначать: “страничка” и "приложение").
Для странички и для приложения будет использоваться один и тот же файл "дистрибутив" Bootstrap. Это не очевидное и не единственное решение (как пример, для jspm и для тестов используется другой код), но, создавая собственный плагин, ничего другого не остается следовать этому соглашению, надеясь на его оптимальность.
Поиск баланса между удовлетворением двух потребностей использовать один и тот же код для двух разных вариантов загрузки: "странички" и "приложения" — и является основной архитектурной задачей которая стоит перед разработчиком плагина.
Плагин к Bootstrap'у ведь можно создавать и ad hoc, прямо в вашем приложении. В таком случае можно писать код плагина со всеми удовольствиями: полифилами и доступу к SASS переменным схемы. Однако когда в расчете на переиспользование надо будет произвести перенос плагина в его собственный npm пакет, перечисленные удовольствия переходят в разряд проблем: теряется доступ к переменным SASS и надо иметь в виду создателей "страничек" подключающих плагин через script (где мои полифилы?).
Хотелось бы чтобы для написания плагина хватало css классов Bootstrap'а. В таком случае при кастомизации темы Bootstrаp'а — можно ожидать что плагин будет адаптироваться без дополнительных усилий.
Но для самостоятельно публикуемого плагина доступа к переменным SASS нет, а что если вы хотите например color от .form-control назначить элементу без назначения класса .form-control? Это не получится так как нет такого класса как .input-color в котором переменная из Bootstrap схемы SASS $input-color была бы опубликована “отдельно” от всего.
Далее следовать решениям команды Bootstrap'а не возможно: они такой проблемы не имеют, если им нужно — они создают себе класс с нужной переменной и оперируют этим классом.
Я предлагаю следующее решение.
Для странички (поднимающей плагин через script
) c недоступностью переменных приходится смириться. Но и предлагать разбираться с css не обязательно — недоступные переменные можно предложить кастомизировать через параметры js, благо их не много (и не все они меняются всякой схемой). Для BsMultiSelect проблематичным были: цвет disabled, цвет input, focused control shadow...
$("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. Таким образом плагин "получит" переменные схемы.
$("select[multiple='multiple']").bsMultiSelect({
useCss: true
});
Эти два способа записаны в моем JS коде явно, через два “адаптера”. BsMultiSelect это собственно два плагина в одном. Один который работает со стилями через переменные js, другой делегирует эту работу css.
В сценарии “пишу ad hoc плагин внутри приложения” — вы будете без проблем использовать babel/polyfill, а Element пропатчите 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. Но поделать с этим ничего нельзя.
Если вы все же использовали полифилы при создании собственного плагина, надо помнить, что если в приложении есть инлайн скрипты — то загружать полифилы придется только синхронно (иначе инлайн код может начать исполнение ранее того момента когда плагин загружен). Т.е. например webpack-polyfill-injector (мне кажется самый богатый по возможностям injector полифилов) надо контролировать так чтобы он не решил грузить полифилы асинхронно.
jQuery не навсегда, есть branch v4-whithout-jquery, он уже в pool request, в планах выпустить bootstrap-nojquery
c версии 4.3. Ранее самой большой проблемой отказа от jquery назывались namespace событий, хотя и очевидно что можно писать без них. Видимо решились.
Также объявлено что есть хотелка чтобы в версии 4.3 каждая компонента имела бы собственный npm пакет. Т.е. станут все как BsMultiSelect? Нет — если им будут нужны какие-то переменные схемы — понаделют под себя классы CSS. Создателям custom plugin такие возможности не доступны.
Возможный переход с sass на post-css https://github.com/twbs/bootstrap/projects/11 в 5ой версии принципиально ничего не меняет. Или меняет всё: "JS over HTML".
Bootstrap 5 придется установить рекомендации как интегрироваться в полифилы Babel, и как патчить DOM node. Тут решение более-менее очевидно: создателям страничек и создателям приложений будут предложены совершенно различные сборки.
Пока пишешь компоненты Bootstrap 4 на Babel методом ad hoc не задумываясь о переиспользовании, а значит о полифилах, и о том где взять переменные стилей — горя не знаешь.
Но если делать плагин отдельным npm пакетом — и с желанием сделать решение в духе самого Bootstrap 4 удовольствия становится меньше, задумываешься чаще и дольше. Вероятно придется сразу определиться — кто ваши пользователи, и предлагать различные сборки для тех, кто будет собирать приложение в бандл и для пишущих странички, загружая плагин script'ом.