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

в 14:05, , рубрики: Angular плагин, imaskjs, javascript, js-плагин, React плагин, Vue плагин, наболело, Разработка веб-сайтов

Всем привет!

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

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

Введение

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

Мой пример:

mask.update(options)

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

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

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

React

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

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

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

<IMaskInput
  // свойства маски
  mask=”00-00”
  value="123"
  unmask={true}
  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>

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

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

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

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

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

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

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

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

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 в рантайме. Поэтому пришлось всю инициализацию переносить в конструктор. Пожалуйста, поделитесь, если кто-то знает как бороть.

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

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

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

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

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

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

В заключении

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

Автор: burfee

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js