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

timeago.js не имеет встроенной системы плагинов, но её можно построить поверх публичного API. Плагины расширяют поведение без модификации исходного кода библиотеки.


Что можно расширить

timeago.js предоставляет четыре точки расширения:

  • register — добавить новую локаль;
  • format — обернуть для добавления логики до/после;
  • render — обернуть для управления жизненным циклом;
  • DOM — навесить обработчики событий на элементы.

Паттерн плагина: функция-расширитель

import { format as _format, render as _render, cancel as _cancel } from 'timeago.js';

type Plugin = {
  name:    string;
  install: (api: TimeagoAPI) => void;
};

interface TimeagoAPI {
  format: typeof _format;
  render: typeof _render;
  cancel: typeof _cancel;
  on:     (event: string, handler: (...args: any[]) => void) => void;
}

Базовая реализация системы плагинов

import { format as _format, render as _render, cancel as _cancel, register } from 'timeago.js';

type FormatFn   = typeof _format;
type RenderFn   = typeof _render;
type CancelFn   = typeof _cancel;
type EventMap   = Record<string, ((...args: any[]) => void)[]>;

class Timeago {
  private formatFn:  FormatFn  = _format;
  private renderFn:  RenderFn  = _render;
  private cancelFn:  CancelFn  = _cancel;
  private events:    EventMap  = {};

  use(plugin: { install: (t: this) => void }): this {
    plugin.install(this);
    return this;
  }

  format(...args: Parameters<FormatFn>): ReturnType<FormatFn> {
    return this.formatFn(...args);
  }

  render(...args: Parameters<RenderFn>): void {
    this.renderFn(...args);
  }

  cancel(...args: Parameters<CancelFn>): void {
    this.cancelFn(...args);
  }

  wrapFormat(wrapper: (original: FormatFn) => FormatFn): void {
    this.formatFn = wrapper(this.formatFn);
  }

  wrapRender(wrapper: (original: RenderFn) => RenderFn): void {
    this.renderFn = wrapper(this.renderFn);
  }

  emit(event: string, ...args: any[]): void {
    (this.events[event] ?? []).forEach(h => h(...args));
  }

  on(event: string, handler: (...args: any[]) => void): void {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(handler);
  }
}

export const timeago = new Timeago();

Плагин кеширования

const CachePlugin = {
  install(t: Timeago) {
    const cache = new Map<string, { value: string; ts: number }>();
    const TTL   = 30_000; // 30 секунд

    t.wrapFormat((original) => (date, locale, opts) => {
      const key     = `${date}:${locale}`;
      const cached  = cache.get(key);
      const now     = Date.now();

      if (cached && now - cached.ts < TTL) {
        return cached.value;
      }

      const value = original(date, locale, opts);
      cache.set(key, { value, ts: now });
      return value;
    });
  },
};

timeago.use(CachePlugin);

Плагин логирования

const LogPlugin = {
  install(t: Timeago) {
    t.wrapFormat((original) => (date, locale, opts) => {
      const result = original(date, locale, opts);
      console.debug(`[timeago.format] "${result}" | locale: ${locale ?? 'default'}`);
      return result;
    });

    t.wrapRender((original) => (nodes, locale) => {
      const count = nodes instanceof NodeList ? nodes.length :
                    Array.isArray(nodes)      ? nodes.length : 1;
      console.debug(`[timeago.render] ${count} element(s) | locale: ${locale ?? 'default'}`);
      original(nodes, locale);
    });
  },
};

Плагин события

const EventPlugin = {
  install(t: Timeago) {
    t.wrapFormat((original) => (date, locale, opts) => {
      const result = original(date, locale, opts);
      t.emit('format', { date, locale, result });
      return result;
    });

    t.wrapRender((original) => (nodes, locale) => {
      original(nodes, locale);
      t.emit('render', { nodes, locale });
    });
  },
};

// Подписка на события
timeago.on('format', ({ date, locale, result }) => {
  analytics.track('timeago_format', { locale, result });
});

Плагин автоматической регистрации локалей

const LocaleAutoloadPlugin = {
  install(t: Timeago) {
    const loaded = new Set<string>();

    t.wrapFormat((original) => async (date, locale, opts) => {
      if (locale && !loaded.has(locale)) {
        try {
          const mod = await import(`timeago.js/esm/lang/${locale}.js`);
          register(locale, mod.default);
          loaded.add(locale);
        } catch {
          console.warn(`Locale "${locale}" not found, falling back`);
        }
      }
      return original(date, locale, opts);
    });
  },
};

Плагин ограничения количества элементов

const MaxElementsPlugin = {
  install(t: Timeago, { max = 100 }: { max?: number } = {}) {
    t.wrapRender((original) => (nodes, locale) => {
      const arr = Array.isArray(nodes) ? nodes :
                  nodes instanceof NodeList ? Array.from(nodes) : [nodes];
      const limited = arr.slice(0, max) as Element[];
      if (arr.length > max) {
        console.warn(`[timeago] Limiting render to ${max} elements (${arr.length} provided)`);
      }
      original(limited, locale);
    });
  },
};

timeago.use({ install: (t) => MaxElementsPlugin.install(t, { max: 50 }) });

Цепочка плагинов

timeago
  .use(CachePlugin)
  .use(LogPlugin)
  .use(EventPlugin);

// Теперь каждый вызов timeago.format проходит через все плагины
const result = timeago.format(new Date(), 'ru');

Таблица распространённых плагинов

Плагин Назначение
Кеш Избегать повторного вычисления за TTL
Логирование Отладка вызовов format/render
Аналитика Отслеживать использование локалей
Ограничение элементов Защита от переполнения таймерами
Автозагрузка локалей Загружать локали по требованию
Fallback Возвращать дефолтное значение при ошибке