API для разработки плагинов

Day.js реализует модульную архитектуру расширений, основанную на функции extend. Вся функциональность библиотеки изначально минимальна и расширяется через подключаемые плагины, которые модифицируют прототип объекта Day.js.

Плагин представляет собой функцию, которая получает два аргумента:

  • объект конструктора Day.js
  • объект настроек (в некоторых плагинах не используется)

Основная задача плагина — добавить новые методы, расширить прототип, внедрить утилиты или изменить поведение парсинга и форматирования.

Базовый механизм подключения выглядит следующим образом:

import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'

dayjs.extend(customParseFormat)

Функция extend выполняет регистрацию плагина в глобальном контексте библиотеки и гарантирует, что расширения будут доступны всем создаваемым экземплярам.


Контракт плагина

Плагин в Day.js представляет собой чистую функцию без состояния. Стандартная сигнатура:

export default (option, Dayjs, dayjs) => {
  // расширение прототипа
}

Где:

  • option — конфигурация, переданная при регистрации
  • Dayjs — конструктор
  • dayjs — глобальный экземпляр API

Расширения обычно добавляются через:

Dayjs.prototype.newMethod = function () {
  return this
}

или через утилитарные функции:

dayjs.newUtility = () => {}

Расширение прототипа

Основной способ работы плагинов заключается в добавлении методов к Dayjs.prototype. Это позволяет каждому экземпляру получать новые цепочечные вызовы.

Пример плагина, добавляющего метод форматирования относительного времени:

export default (option, Dayjs, dayjs) => {
  Dayjs.prototype.toRelative = function () {
    const now = dayjs()
    const diff = this.diff(now, 'minute')

    return diff > 0 ? `${diff} minutes ago` : `${-diff} minutes later`
  }
}

После регистрации:

dayjs.extend(relativePlugin)

dayjs().toRelative()

Важное свойство архитектуры — отсутствие копирования методов на каждый экземпляр. Все экземпляры используют один прототип, что обеспечивает низкие накладные расходы.


Внутренний механизм extend

Функция extend в Day.js выполняет несколько шагов:

  1. Проверка, не был ли плагин уже зарегистрирован
  2. Вызов функции плагина
  3. Передача контекста Day.js
  4. Обновление внутреннего списка подключённых расширений

Псевдологика:

function extend(plugin, option) {
  if (plugin.isPluginInstalled) return

  plugin(option, Dayjs, dayjs)
  plugin.isPluginInstalled = true
}

Такой механизм предотвращает дублирование расширений и потенциальные конфликты методов.


Плагины, изменяющие парсинг

Часть плагинов в Day.js влияет на этап разбора даты. Это более сложный тип расширений, так как они вмешиваются в цепочку создания объекта.

Пример: плагин customParseFormat

Dayjs.prototype.parse = function (date, format) {
  // кастомная логика разбора строки
}

Подобные плагины должны аккуратно работать с внутренними структурами, чтобы не нарушить совместимость с базовыми форматами ISO.

Ключевая особенность: парсер должен быть обратимо совместимым с другими плагинами.


Композиция плагинов

Day.js поддерживает композицию, при которой несколько плагинов могут модифицировать один и тот же метод.

Порядок подключения имеет значение:

dayjs.extend(pluginA)
dayjs.extend(pluginB)

Если оба плагина модифицируют format, последний подключённый плагин получает приоритет.

Для управления конфликтами применяются техники:

  • сохранение оригинального метода
  • обёртка (wrapper pattern)
  • вызов предыдущей реализации через ссылку

Пример обёртки:

const originalFormat = Dayjs.prototype.format

Dayjs.prototype.format = function (args) {
  const base = originalFormat.call(this, args)
  return `[${base}]`
}

Глобальные расширения API

Плагины могут добавлять не только методы экземпляра, но и статические методы:

dayjs.isBusinessDay = (date) => {
  return [0, 6].includes(dayjs(date).day())
}

Это расширяет функциональность без изменения структуры экземпляров.

Также допустимо добавление утилит:

dayjs.MINUTES_IN_HOUR = 60

Однако такие расширения должны использоваться аккуратно, чтобы не загрязнять глобальный API.


Плагины и иммутабельность

Одной из ключевых концепций Day.js является неизменяемость объектов. Каждый вызов метода возвращает новый экземпляр.

Плагины должны учитывать это правило:

Dayjs.prototype.addBusinessDays = function (n) {
  let date = this

  while (n > 0) {
    date = date.add(1, 'day')
    if (date.day() !== 0 && date.day() !== 6) {
      n--
    }
  }

  return date
}

Метод всегда возвращает новый объект, не изменяя исходный.


Порядок инициализации расширений

Плагины подключаются строго до использования экземпляров, если они влияют на базовое поведение.

Правильный порядок:

dayjs.extend(pluginA)
dayjs.extend(pluginB)

const d = dayjs()

Неправильный порядок может привести к тому, что экземпляры будут созданы без нужных методов.

Некоторые плагины допускают динамическую установку, но это зависит от внутренней реализации.


Изоляция и безопасность расширений

В Day.js отсутствует жёсткая изоляция плагинов, поэтому конфликт имён является потенциальной проблемой.

Рекомендации при разработке:

  • избегать общих имён методов (format, parse)
  • использовать префиксы (toX, isX, getX)
  • не перезаписывать внутренние методы без сохранения оригинала

Пример безопасного расширения:

Dayjs.prototype.isLeapYearCustom = function () {
  const year = this.year()
  return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0
}

Условные плагины и конфигурация

Некоторые плагины поддерживают параметры конфигурации:

dayjs.extend(plugin, { locale: 'ru', strict: true })

Плагин получает опции и адаптирует поведение:

export default (option, Dayjs) => {
  const locale = option.locale

  Dayjs.prototype.localeAwareFormat = function () {
    return locale === 'ru' ? 'формат' : 'format'
  }
}

Это позволяет создавать универсальные расширения, адаптирующиеся под окружение.


Работа с TypeScript в плагинах

Day.js предоставляет поддержку типизации через расширение интерфейсов.

Пример декларации:

declare module 'dayjs' {
  interface Dayjs {
    toRelative(): string
  }
}

Реализация:

dayjs.extend((option, Dayjs) => {
  Dayjs.prototype.toRelative = function () {
    return 'custom'
  }
})

Типизация позволяет сохранять корректный автокомплит и предотвращает ошибки при использовании расширений.


Удаление и замена плагинов

Механизм удаления плагинов отсутствует как стандартная функция. Это архитектурное решение связано с тем, что расширения модифицируют глобальный прототип.

Возможные стратегии обхода:

  • создание обёрток вместо прямого расширения
  • использование отдельных экземпляров библиотеки
  • изоляция через фабрики

Пример фабричного подхода:

function createDayjsWithPlugin() {
  const localDayjs = dayjs
  localDayjs.extend(plugin)
  return localDayjs
}

Паттерны разработки плагинов

В экосистеме Day.js часто используются следующие паттерны:

Wrapper pattern

Используется для расширения существующих методов без их замены.

Factory pattern

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

Middleware pattern

Применяется для последовательного изменения входных данных (например, парсинга даты).

Decorator pattern

Используется для модификации поведения методов без изменения исходной реализации.


Ограничения системы плагинов

Система расширений имеет ряд ограничений:

  • отсутствует нативная система зависимостей между плагинами
  • невозможность частичной деактивации расширений
  • глобальное влияние на все экземпляры
  • потенциальные конфликты при переопределении методов

Эти ограничения компенсируются простотой и предсказуемостью модели расширений.