Хабрахабр

Как я писал плагины для React, Vue и Angular

Всем привет!

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

Я опишу некоторые нестандартные моменты и свои эмоции, возникшие в ходе разработки.

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

Введение

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

Мой пример:

mask.update(options)

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

Например в моем случае нужно было не забыть про всякие datepicker, number-spinner, StyledComponents и пр. И пожалуй, самое главное, что надо учитывать при разработке плагинов — это возможность их использования с существующими компонентами. При этом для простых случаев также желательно иметь готовый к использованию компонент маскированного ввода.

Также при разработке плагинов для указания зависимостей от фреймворков в npm-пакете используем peerDependencies вместо dependencies/devDependencies.

React

Первый плагин react-imask был для React. С ним все прошло довольно гладко: у React хорошая документация и много живых примеров.

Тем не менее, есть несколько моментов, о которых следует помнить.

Например в моем случае с маской: В React принято передавать параметры в виде отдельных свойств.

<IMaskInput // свойства маски mask=”00-00” value="123" unmask= onAccept={(value, mask) => console.log(value)} // свойства обычного input placeholder='Enter code here'
/>

В этом случае происходит смешение свойств маски со свойствами вложенного HTML-элемента. Задача состоит в том, чтобы выцепить свойства маски, а остальные передать дальше. Это неплохо решается в совокупности с объявлением propTypes и итерации по его ключам. Важно не забыть про некоторые особенные свойства, например в моем случае value, которые должны быть обработаны специальным образом. Отдельный случай представляют колбеки, хотя в React они передаются также как и обычные свойства. Я обернул внутренние колбеки библиотеки в методы компонента, и принудительно подключил при инициализации. Таким образом они будут вызываться всегда, а там уже если что-то передано в props, то будет вызвано дальше. Это позволяет избежать явного управления подключения/отключения колбеков.

Для расширения компонентов в React используется идея High Order Components (HOC), что на практике реализуется как обертка-декоратор. Мы хотим чтобы наши компоненты можно было расширять и использовать совместно с другими. Нет времени объяснять что это, посмотрите гайд и пример.

Желательно не забыть объявить Component.displayName, чтобы ваши компоненты были узнаваемы при отладке.

Vue

Следующим был плагин на Angular, но это долгая история, сначала про vue-imask. До написания плагина я практически не был знаком с Vue, и учился в процессе работы. Несмотря на то, что Vue показался хипстерским фреймворком, сообщество оставило очень приятные впечатления — люди открыты и небезразличны.

Компоненты — предпочитаемый способ, я реализовал оба — выбирайте сами. Итак, в Vue есть два способа реализации плагина: через директиву и через компонент.

Мне показалось, директива — это наиболее подходящий подход: есть все нужные методы bind, unbind, update. Начнем с более простой директивы. А нужен state, которого у директивы нет. Что же еще надо? — прямо в html-element, а колбеки реализуются через эмуляцию DOM-событий руками. Куда же класть свои данные? И вы еще спрашиваете причём тут хипстеры.

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

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

<template> <input :value="value" v-imask="mask" @accept="onAccept" @complete="onComplete">
</template> <script> import {IMaskDirective} from 'vue-imask'; export default { data () { return { value: '', mask: { mask: '{8}000000', lazy: false }, onAccept (e) { const maskRef = e.detail; console.log('accept', maskRef.value); }, onComplete (e) { const maskRef = e.detail; console.log('complete', maskRef.unmaskedValue); } } }, directives: { imask: IMaskDirective } }
</script>

Давайте попробуем через компонент.

Поэтому чтобы отследить изменения, подписываемся на изменения всех $props, выковыриваем наши и обновляемся.
Важный момент, который надо учитывать — это модели Vue, которые реагируют на событие input. Здесь, как и в React, параметры принято передавать через свойства. Моя библиотека живет себе тихо-мирно и не глушит стандартные HTML-события, а добавляет пару своих — accept и complete, кому что надо сам выбирает. И тут такая история. Но это неверно, ожидается такое поведение: маска подключена — реагируем на accept, маска отключена — на input. И получается, что модель обновляется на событие input, когда значение еще не было обработано маской. Кто знает способ получше? Решил вопрос ручным управлением событиями, что не очень удобно.

— как-то так. Поскольку в результате получился полноценный компонент с HTML-input под капотом, возникает вопрос: а как же его расширять или использовать параллельно с другими компонентами?

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

<template> <imask-input v-model="numberModel" :mask="Number" radix="." :unmask="true" @accept="onAccept" // first argument will be `value` or `unmaskedValue` depending on prop above // ...and more mask props in a guide // other input props placeholder='Enter number here' />
</template> <script> import {IMaskComponent} from 'vue-imask'; export default { data () { return { numberModel: '', onAccept (value) { console.log(value); } } }, components: { 'imask-input': IMaskComponent } }
</script>

Angular

С Angular было больно, хотя с ним было больше всего опыта и самые лучшие ожидания.
Многие критикуют Angular за отсутствие толковой документации, и видимо небезосновательно. У ангуляровцев грандиозные планы и много пиара, но у меня сложилось чувство, что они позиционируют свой фреймворк исключительно для пользователей, но не для разработчиков. Для использования в приложениях средней сложности документация вполне сносная, но на момент разработки плагина не было практически никакой официальной информации о том, как разрабатывать плагины/библиотеки для Angular. У Angular-cli есть небольшая заметка, которую еще найти надо, а в остальном — догадывайтесь сами. При этом особенностей сборки очень много, чего только стоит один AOT. А Angular-cli видимо еще не скоро поможет. Но решение есть.
Итак, нам нужна директива и модуль, которые можно использовать с Angular>=4 с JIT и AOT компиляцией. Я нашел несколько шаблонов проектов (1, 2, 3, 4, 5, 6), примеров библиотек (1, 2, 3, 4, 5), статей (1, 2, 3, 4, 5), но честно говоря, мало помогло. Мне для плагина, как и во всей остальной работе, нужен понятный, минимально рабочий и максимально простой вариант.

В общем виде процесс сборки выглядит следующим образом:

Минимальная сборка обычно включает umd версию, а также esm5 версию (модули es6, но код es5). На входе имеем исходный код библиотеки на typescript, а на выходе получаем различные javascript-сборки. В большинстве случаев достаточно umd-версии, ей и ограничимся. Самый полный комплект сборки, который я видел, называется Angular Package Format и включает много других дополнительных вариантов скомпилированного кода.

В моем случае метаданные ограничиваются только файлами .metadata.json, т.к. Помимо скомпилированного кода в придачу также публикуются тайпинги (.d.ts файлы) и пачка метаданных. Иначе также залетают фабрики ngfactory.ts и файлы .ngsummary.json. не используются стили и шаблоны.

Если коротко, AOT в Angular — дополнительная оптимизация во время компиляции с примесью json-магии, пакующая шаблоны и стили. Вся эта макулатура обязательна в основном ради фичи Angular под названием AOT. Но фактически AOT принудили использовать, и множество ранее написанных библиотек оказались несовместимыми. Для разработчиков плагинов все было бы замечательно, если бы AOT остался опциональным вариантом. Ситуация объясняется в красивой сказке о том, какой теперь Angular крутой и быстрый. В результате, в репозитории Angular десятки, если не сотни багов на эту тему, закрытых без объяснения, много недовольных, но по-прежнему нет внятной документации как же нужно делать. — да, но доверие потеряно. Стало ли реально быстрее? Что ж, давайте делать компиляцию ради компиляции.

Angular Compiler используется для упрощения генерации макулатуры, и с документацией у него совсем все плохо. Angular использует свой компилятор Angular Compiler (ngc), который использует компилятор typescript (tsc). Покажу основные моменты при настройке сборки:

package.json

{ ... "main": "dist/angular-imask.umd.js", "module": "dist/index.js", // esm5 "typings": "dist/index.d.ts", // обяз. поле, иначе AOT умрет ...
}

tsconfig.json

{ ... "target": "es5", // версия для кода "module": "es2015", // версия для импортов "moduleResolution": "node", // мы используем npm-зависимости "experimentalDecorators": true, // декораторы по-прежнему экспериментальные "stripInternal": true, // если используете @internal "declaration": true, // генерировать описания типов .d.ts "emitDecoratorMetadata": true, // какая-то магия чтобы сгенерировать метаданные // еще немного непонятных букв для укрощения беса, чтобы оставил в покое ваши peer-зависимости "baseUrl": ".", "paths": { "@angular/*": ["node_modules/@angular/*"], "rxjs/*": ["node_modules/rxjs/*"] }, // дальше тайная магия "angularCompilerOptions": { "skipTemplateCodegen": true, // ставим true, если не используем шаблоны/стили, выкинет много лишнего "skipMetadataEmit": false, // мне нужны метаданные, напомню ему на всякий случай, бывает забывал "strictMetadataEmit" : true, // скажи мне сразу про ошибки "annotationsAs": "decorators", // особенное тайное заклинание для укрощения Angular 5, который иначе просто вырежет ваши декораторы. Оптимизация, сэр. По хорошему надо использовать только для JIT версии, для aot не нужно. После включения этого флага tsc для каждого файла закинет свои хелперы. Почему их нельзя было положить в одно место? Зачем подсовывать для каждого файла? Адепты предлагают либо использовать флаг noEmitHelpers, и тогда будьте добры руками их подключить, либо используйте флаг importHelpers и получаете tslib в рантайме. Выбирайте сами как будете страдать. ...
}

С ngc есть еще одна проблема. На примере кода:

export class A { myVar = 1;
}

tsc корректно скомпилирует и добавит присвоение переменной в конструктор, но ngc просто вырежет присвоение, и неожиданно имеем undefined в рантайме. Поэтому пришлось всю инициализацию переносить в конструктор. Пожалуйста, поделитесь, если кто-то знает как бороть.

Я ограничился umd-сборкой. После преодоления компиляции ts → js можно колбасить js как хотите.

Если вдруг все развалилось, есть еще много бесполезных советов:

  • использовать export {… явное перечисление} from … вместо export * from ...
  • вместо import IMask from 'imask'; использовать import * as IMask from 'imask'; Не делайте так. При использовании в Аngular-cli приложении вас ждет сюрприз в рантайме.
  • удалять вложенный node_modules
  • вручную испортировать полифилы
  • использовать --preserve-symlinks для сборки

И не пытайтесь заставить работать плагин, скомпилированный с Angular 5, на Angular 4.

Главное — побороть сборку. Итого для Angular получили рабочую директиву на выходе, про которую писать особо нечего.

Любителям магической Angular-капусты подойдет ng-packagr — очередная обертка над оберткой над оберткой, чтобы вы никогда в жизни не пытались больше понять суть происходящего, но таки стали JSON-экспертом. Вообще всех этих ребят из Angular-тусовки отличает любовь к оберткам вокруг своей тайной магии, которая играет злую шутку в самые неподходящие моменты. Из плюсов — выдает модный Angular Package Format, из минусов — инлайнит зависимости, не нашел как отключить, настроек практически нет.

Управление пакетами

Плагинов стало много, и возник вопрос с обновлением версий. По хорошему, при обновлении core-библиотеки, должны обновляться и версии плагинов, чтобы вы узнали об обновлении. Но при обновлении framework-плагина имеет смысл обновлять только его версию, а саму библиотеку и другие плагины не трогать. На практике я не встречал таких схем, либо они выполняются вручную и требуют к себе слишком много внимания. Я остановился на варианте “одна версия на все”, а плагины, поскольку они уже достаточно стабильны, обновляться будут редко.

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

В заключении

Я потратил много времени на разработку библиотеки, плагинов, документации, статей и пр. И что же? Меня поблагодарили несколько человек, один сделал пожертвование (ему вообще респект) и еще пара ребят поддержали кодом и советом. За это им большое спасибо. Благодаря вам разработка продолжалась, и я чувствовал, что делаю что-то полезное. В целом Open-Source принес мне интересный опыт и стало понятно, что от него ждать на практике. Но на этом наверно все.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть