Хуки и middleware позволяют встраивать логику до и после вызовов timeago.js без изменения исходного кода. Это полезно для логирования, кеширования, телеметрии и валидации.
Хук — функция, вызываемая в определённый момент. Для timeago.js имеет смысл четыре момента:
beforeFormat — перед вызовом format;afterFormat — после получения результата;beforeRender — перед вызовом render;afterCancel — после вызова cancel.import { format as baseFormat, render as baseRender, cancel as baseCancel } from 'timeago.js';
type HookName = 'beforeFormat' | 'afterFormat' | 'beforeRender' | 'afterCancel';
type HookPayload = {
beforeFormat: { date: unknown; locale?: string };
afterFormat: { date: unknown; locale?: string; result: string };
beforeRender: { nodes: unknown; locale?: string };
afterCancel: { nodes?: unknown };
};
type HookFn<K extends HookName> = (payload: HookPayload[K]) => void;
class TimeagoHooks {
private hooks: { [K in HookName]?: HookFn<K>[] } = {};
hook<K extends HookName>(name: K, fn: HookFn<K>): void {
if (!this.hooks[name]) this.hooks[name] = [];
(this.hooks[name] as HookFn<K>[]).push(fn);
}
private fire<K extends HookName>(name: K, payload: HookPayload[K]): void {
((this.hooks[name] ?? []) as HookFn<K>[]).forEach(fn => fn(payload));
}
format(date: unknown, locale = 'ru'): string {
this.fire('beforeFormat', { date, locale });
const d = date instanceof Date ? date : new Date(date as any);
const result = baseFormat(d, locale);
this.fire('afterFormat', { date, locale, result });
return result;
}
render(nodes: Element | Element[], locale = 'ru'): void {
this.fire('beforeRender', { nodes, locale });
baseRender(nodes, locale);
}
cancel(nodes?: Element | Element[]): void {
baseCancel(nodes);
this.fire('afterCancel', { nodes });
}
}
export const timeago = new TimeagoHooks();
// Логирование перед форматированием
timeago.hook('beforeFormat', ({ date, locale }) => {
console.log(`[format] date=${date}, locale=${locale}`);
});
// Телеметрия после форматирования
timeago.hook('afterFormat', ({ locale, result }) => {
metrics.increment('timeago.format', { locale });
});
// Предупреждение о рендере большого количества элементов
timeago.hook('beforeRender', ({ nodes }) => {
const arr = Array.isArray(nodes) ? nodes : [nodes];
if (arr.length > 100) {
console.warn(`[timeago] Rendering ${arr.length} elements — consider batching`);
}
});
В отличие от хуков, middleware может прервать цепочку и вернуть собственный результат:
type NextFn = () => string;
type FormatMiddleware = (date: unknown, locale: string, next: NextFn) => string;
class TimeagoMiddlewareChain {
private middlewares: FormatMiddleware[] = [];
use(mw: FormatMiddleware): this {
this.middlewares.push(mw);
return this;
}
format(date: unknown, locale = 'ru'): string {
let index = 0;
const next: NextFn = () => {
if (index < this.middlewares.length) {
return this.middlewares[index++](date, locale, next);
}
return baseFormat(date as any, locale);
};
return next();
}
}
const validationMiddleware: FormatMiddleware = (date, locale, next) => {
if (date == null) {
return '';
}
const d = new Date(date as any);
if (isNaN(d.getTime())) {
console.error(`[timeago] Invalid date: ${date}`);
return '';
}
return next();
};
const CACHE_TTL = 30_000;
const cache = new Map<string, { value: string; ts: number }>();
const cacheMiddleware: FormatMiddleware = (date, locale, next) => {
const key = `${date}:${locale}`;
const cached = cache.get(key);
const now = Date.now();
if (cached && now - cached.ts < CACHE_TTL) {
return cached.value;
}
const result = next();
cache.set(key, { value: result, ts: now });
return result;
};
const fallbackMiddleware = (fallback: string): FormatMiddleware =>
(date, locale, next) => {
try {
return next();
} catch {
return fallback;
}
};
const chain = new TimeagoMiddlewareChain()
.use(validationMiddleware)
.use(fallbackMiddleware('давно'))
.use(cacheMiddleware);
// Вызовы проходят через все три middleware по очереди
chain.format(null, 'ru'); // '' — прерван validationMiddleware
chain.format('invalid', 'ru'); // '' — прерван validationMiddleware
chain.format('2025-01-01', 'ru'); // Кешируется после первого вызова
| Характеристика | Хук | Middleware |
|---|---|---|
| Порядок | Все вызываются | Может прервать цепочку |
| Влияние на результат | Только наблюдение | Может изменить результат |
| Сложность | Простые функции-слушатели | Требует передачи next |
| Типичное применение | Логи, метрики, side effects | Кеш, валидация, fallback |
// Хуки — для побочных эффектов (логи, метрики)
// Middleware — для изменения поведения (кеш, валидация)
class TimeagoExtensible {
private chain = new TimeagoMiddlewareChain();
private hookSys = new TimeagoHooks();
useMiddleware(mw: FormatMiddleware): this {
this.chain.use(mw);
return this;
}
useHook<K extends HookName>(name: K, fn: HookFn<K>): this {
this.hookSys.hook(name, fn);
return this;
}
format(date: unknown, locale = 'ru'): string {
this.hookSys['fire']('beforeFormat', { date, locale });
const result = this.chain.format(date, locale);
this.hookSys['fire']('afterFormat', { date, locale, result });
return result;
}
}