timeago.js не имеет встроенной системы плагинов, но её можно построить поверх публичного API. Плагины расширяют поведение без модификации исходного кода библиотеки.
timeago.js предоставляет четыре точки расширения:
register — добавить новую локаль;format — обернуть для добавления логики до/после;render — обернуть для управления жизненным циклом;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 | Возвращать дефолтное значение при ошибке |