Создание собственного плагина

Архитектура Moment.js построена вокруг расширяемого объекта moment, который одновременно выступает фабрикой для создания экземпляров и контейнером для статических методов. Такой подход позволяет подключать плагины без изменения исходного кода библиотеки, добавляя функциональность через прототипы и расширение пространства имён.

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

(function (moment) {
  // расширение moment
})(moment);

При использовании CommonJS или ES Modules структура остаётся аналогичной:

// CommonJS
const moment = require("moment");
require("./myPlugin")(moment);
// ES Modules
import moment from "moment";
import myPlugin from "./myPlugin.js";
myPlugin(moment);

Расширение экземпляров через moment.fn

moment.fn — это прототип всех экземпляров даты. Любой метод, добавленный в moment.fn, становится доступным для каждого объекта moment().

Базовый принцип:

moment.fn.isWeekend = function () {
  const day = this.day();
  return day === 0 || day === 6;
};

Использование:

moment().isWeekend();

Особенности работы с контекстом

Внутри методов плагина this всегда указывает на экземпляр Moment. Это позволяет безопасно использовать встроенные методы:

moment.fn.addBusinessDays = function (days) {
  let date = this.clone();
  while (days > 0) {
    date = date.add(1, "day");
    if (date.day() !== 0 && date.day() !== 6) {
      days--;
    }
  }
  return date;
};

Ключевой момент — обязательное использование clone(), чтобы не мутировать исходный объект.


Добавление статических методов в moment

Статические методы расширяют сам объект moment, а не его экземпляры. Это полезно для утилитарных функций:

moment.isMomentStrict = function (obj) {
  return moment.isMoment(obj) && obj.isValid();
};

Использование:

moment.isMomentStrict(moment());

Также статические методы часто используются для фабрик:

moment.fromUnixSeconds = function (sec) {
  return moment(sec * 1000);
};

Расширение форматов парсинга

Плагины могут добавлять новые способы интерпретации строковых дат через обёртку над moment.parseZone, moment.utc или кастомные функции.

Пример простого парсера:

moment.fn.fromCustomFormat = function (str) {
  const [day, month, year] = str.split("-");
  return moment(`${year}-${month}-${day}`);
};

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

Механизм длительностей также расширяем:

moment.duration.fn.toBusinessHours = function () {
  const hours = this.asHours();
  return Math.floor(hours * 0.75);
};

Здесь moment.duration.fn аналогичен moment.fn, но применяется к объектам длительности.


Шаблон полноценного плагина

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

(function (moment) {
  function addWeekOffset(m, weeks) {
    return m.clone().add(weeks * 7, "days");
  }

  moment.fn.addWeeksStrict = function (weeks) {
    return addWeekOffset(this, weeks);
  };

  moment.isMomentEmpty = function (m) {
    return !m || !moment.isMoment(m) || !m.isValid();
  };

})(moment);

Защита от повторной установки

Плагины часто включают проверку, предотвращающую повторную регистрацию:

(function (moment) {
  if (moment.__myPluginInstalled) return;
  moment.__myPluginInstalled = true;

  moment.fn.example = function () {
    return this.format();
  };
})(moment);

Интеграция с форматированием

Плагины могут расширять форматирование через moment.fn.format-подобные обёртки:

moment.fn.formatWithPrefix = function (prefix, formatStr) {
  return prefix + this.format(formatStr);
};

Совместимость с цепочками вызовов

Одним из ключевых требований является сохранение цепочечной модели:

moment.fn.startOfWeek = function () {
  return this.clone().startOf("week");
};

moment.fn.nextBusinessDay = function () {
  return this.clone().add(1, "day");
};

Каждый метод должен возвращать новый экземпляр moment, если предполагается дальнейшее продолжение цепочки.


Работа с immutable-подходом

Несмотря на то, что Moment.js не является полностью immutable-библиотекой, плагины часто реализуют иммутабельное поведение вручную:

moment.fn.setImmutable = function (unit, value) {
  const clone = this.clone();
  clone.set(unit, value);
  return clone;
};

Плагин с конфигурацией

Расширения могут принимать параметры:

function businessDaysPlugin(moment, options) {
  const weekendDays = options?.weekends || [0, 6];

  moment.fn.isBusinessDay = function () {
    return !weekendDays.includes(this.day());
  };
}

businessDaysPlugin(moment, { weekends: [0, 6] });

Обработка локалей внутри плагина

Плагины могут учитывать локализацию через moment.localeData():

moment.fn.localizedDayName = function () {
  return this.localeData().weekdays(this);
};

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

  • изменение this вместо возврата нового объекта
  • отсутствие clone() при модификации даты
  • загрязнение глобального пространства moment
  • перезапись встроенных методов без необходимости
  • отсутствие проверки на существование метода перед добавлением

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

if (!moment.fn.myMethod) {
  moment.fn.myMethod = function () {
    return this;
  };
}

Такой подход снижает риск конфликтов между плагинами и сторонними расширениями.