habrahabr

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

  • пятница, 6 июля 2018 г. в 06:49:30
https://habr.com/post/416235/
  • JavaScript
  • HTML
  • CSS



Общественное мнение перевело 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.


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


Отличная библиотека. Однако у нее больше открытых 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 пакета т.е. зависимости разрешаются в тот же самый файл который грузят пользователи. Это не такое уж и однозначное решение, но мой плагин 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/preset-env.


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


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


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


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


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


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


Хотелось бы чтобы для написания плагина хватало 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. Таким образом плагин "получит" переменные схемы.


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

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


Polyfill’ы


В сценарии “пишу 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 полифилов) надо контролировать так чтобы он не решил грузить полифилы асинхронно.


No jQuery


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'ом.