Архитектура расширений определяет, как сторонний код встраивается в поведение timeago.js. Поскольку библиотека не предоставляет встроенных хуков, расширения реализуются через обёртки, прокси и паттерны дизайна.
┌─────────────────────────────────────────────┐
│ Приложение (компоненты, страницы) │
├─────────────────────────────────────────────┤
│ Расширения (кеш, логи, события, валидация) │
├─────────────────────────────────────────────┤
│ Обёртка (TimeagoCore / TimeagoService) │
├─────────────────────────────────────────────┤
│ timeago.js (format, render, cancel, register)│
└─────────────────────────────────────────────┘
Приложение никогда не обращается к timeago.js напрямую — только через обёртку. Расширения регистрируются в обёртке.
import { format as baseFormat } from 'timeago.js';
type DateInput = Date | string | number;
interface Context {
date: DateInput;
locale: string;
opts?: { relativeDate?: Date | number };
result: string;
}
type Middleware = (ctx: Context, next: () => void) => void;
class TimeagoMiddleware {
private stack: Middleware[] = [];
use(fn: Middleware): this {
this.stack.push(fn);
return this;
}
format(date: DateInput, locale = 'ru'): string {
const ctx: Context = { date, locale, result: '' };
let index = 0;
const next = () => {
if (index < this.stack.length) {
this.stack[index++](ctx, next);
} else {
ctx.result = baseFormat(ctx.date, ctx.locale, ctx.opts);
}
};
next();
return ctx.result;
}
}
const timeago = new TimeagoMiddleware();
// Логирование
timeago.use((ctx, next) => {
console.log(`[before] formatting ${ctx.date}`);
next();
console.log(`[after] result: ${ctx.result}`);
});
// Валидация
timeago.use((ctx, next) => {
const d = new Date(ctx.date as any);
if (isNaN(d.getTime())) {
ctx.result = 'давно';
return; // Не вызывать next — прервать цепочку
}
next();
});
// Кеширование
const cache = new Map<string, string>();
timeago.use((ctx, next) => {
const key = `${ctx.date}:${ctx.locale}`;
if (cache.has(key)) {
ctx.result = cache.get(key)!;
return;
}
next();
cache.set(key, ctx.result);
});
const result = timeago.format('2025-01-01', 'ru');
import * as timeagoLib from 'timeago.js';
type TimeagoModule = typeof timeagoLib;
function createExtendedTimeago(extensions: Partial<TimeagoModule>): TimeagoModule {
return new Proxy(timeagoLib, {
get(target, prop: keyof TimeagoModule) {
if (prop in extensions) {
return extensions[prop];
}
return target[prop];
},
});
}
// Расширенная версия с кастомным format
const extended = createExtendedTimeago({
format: (date, locale, opts) => {
if (!date) return 'N/A';
return timeagoLib.format(date, locale, opts);
},
});
extended.format(null, 'ru'); // 'N/A'
extended.render(el, 'ru'); // оригинальный render
import { format, render, cancel } from 'timeago.js';
abstract class TimeagoDecorator {
abstract format(date: Date | string | number, locale?: string): string;
abstract render(nodes: Element | Element[], locale?: string): void;
abstract cancel(nodes?: Element | Element[]): void;
}
class BaseTimeago extends TimeagoDecorator {
format(date: Date | string | number, locale = 'ru') { return format(date, locale); }
render(nodes: Element | Element[], locale = 'ru') { render(nodes, locale); }
cancel(nodes?: Element | Element[]) { cancel(nodes); }
}
class LoggingTimeago extends TimeagoDecorator {
constructor(private wrapped: TimeagoDecorator) { super(); }
format(date: Date | string | number, locale = 'ru') {
const result = this.wrapped.format(date, locale);
console.log(`format → ${result}`);
return result;
}
render(nodes: Element | Element[], locale = 'ru') {
console.log('render called');
this.wrapped.render(nodes, locale);
}
cancel(nodes?: Element | Element[]) {
console.log('cancel called');
this.wrapped.cancel(nodes);
}
}
const base = new BaseTimeago();
const logging = new LoggingTimeago(base);
import { format } from 'timeago.js';
type EventName = 'before:format' | 'after:format' | 'error:format';
class TimeagoEventBus {
private handlers = new Map<EventName, Set<Function>>();
on(event: EventName, handler: Function): () => void {
if (!this.handlers.has(event)) this.handlers.set(event, new Set());
this.handlers.get(event)!.add(handler);
return () => this.handlers.get(event)!.delete(handler);
}
emit(event: EventName, data: unknown): void {
(this.handlers.get(event) ?? new Set()).forEach(h => h(data));
}
format(date: Date | string | number, locale = 'ru'): string {
this.emit('before:format', { date, locale });
try {
const result = format(date, locale);
this.emit('after:format', { date, locale, result });
return result;
} catch (err) {
this.emit('error:format', { date, locale, err });
throw err;
}
}
}
export const bus = new TimeagoEventBus();
// Подключить расширение через события
bus.on('before:format', ({ date }) => {
if (!date) throw new TypeError('date is required');
});
| Подход | Сложность | Гибкость | Порядок выполнения | Когда использовать |
|---|---|---|---|---|
| Middleware | Средняя | Высокая | Явный (стек) | Несколько независимых расширений |
| Proxy | Низкая | Средняя | Произвольный | Быстрое добавление одного слоя |
| Decorator | Высокая | Высокая | Цепочка | OOP стиль, тестируемость |
| Event Bus | Средняя | Средняя | Параллельный | Слабосвязанные расширения |