Como construir um sistema de notificações in-app sem biblioteca externa
1. Fundamentos do sistema de notificações in-app
Antes de escrever qualquer código, é essencial definir a arquitetura base. Um sistema de notificações in-app pode ser construído com dois modelos principais: event-driven (orientado a eventos) ou polling (verificação periódica). Para um sistema sem dependências externas, o modelo event-driven é mais eficiente, pois reage a mudanças de estado em tempo real sem consumir recursos desnecessários.
A estrutura de dados de cada notificação deve conter:
{
id: string (UUID ou timestamp único),
type: string ('info' | 'warning' | 'error' | 'success'),
title: string,
message: string,
timestamp: number (Date.now()),
priority: number (1-5, onde 5 é mais urgente),
duration: number (ms para auto-dismiss, 0 = permanente),
action: { label: string, callback: Function } | null
}
O ciclo de vida de uma notificação segue quatro estágios: criação (evento disparado), exibição (renderização no DOM), interação (clique, fechamento, ação) e descarte (remoção da fila ativa e arquivamento).
2. Implementação do gerenciador central de notificações
O coração do sistema é uma classe singleton NotificationManager que implementa o padrão Observer para reatividade:
class NotificationManager {
static instance = null;
constructor() {
if (NotificationManager.instance) return NotificationManager.instance;
this.notifications = [];
this.observers = [];
this.history = [];
NotificationManager.instance = this;
}
addNotification(notification) {
const id = 'notif_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
const newNotif = { ...notification, id, timestamp: Date.now() };
this.notifications.push(newNotif);
this._notify();
if (newNotif.duration > 0) {
setTimeout(() => this.dismissNotification(id), newNotif.duration);
}
return id;
}
dismissNotification(id) {
this.notifications = this.notifications.filter(n => n.id !== id);
this._notify();
}
getActiveNotifications() {
return [...this.notifications].sort((a, b) => b.priority - a.priority);
}
subscribe(fn) {
this.observers.push(fn);
return () => { this.observers = this.observers.filter(o => o !== fn); };
}
_notify() {
this.observers.forEach(fn => fn(this.getActiveNotifications()));
}
}
3. Armazenamento local e persistência
Para manter o histórico de notificações mesmo após recarregar a página, utilizamos localStorage com serialização JSON:
class PersistentNotificationManager extends NotificationManager {
constructor() {
super();
this._loadHistory();
}
_saveHistory() {
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
const recentHistory = this.history.filter(n => n.timestamp > thirtyDaysAgo);
localStorage.setItem('notif_history', JSON.stringify(recentHistory));
}
_loadHistory() {
try {
const saved = localStorage.getItem('notif_history');
if (saved) this.history = JSON.parse(saved);
} catch { this.history = []; }
}
dismissNotification(id) {
const notif = this.notifications.find(n => n.id === id);
if (notif) {
this.history.push({ ...notif, dismissedAt: Date.now() });
this._saveHistory();
}
super.dismissNotification(id);
}
}
A limpeza automática de notificações expiradas deve ser executada na inicialização e a cada 5 minutos:
setInterval(() => {
const mgr = NotificationManager.instance;
if (mgr) {
mgr.notifications = mgr.notifications.filter(n => {
if (n.duration === 0) return true;
return (Date.now() - n.timestamp) < n.duration;
});
mgr._notify();
}
}, 300000);
4. Componentização da interface de notificações
Crie um container posicionado no canto superior direito da tela:
function createNotificationContainer(position = 'top-right') {
const container = document.createElement('div');
container.id = 'notification-container';
container.style.cssText = `
position: fixed;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 8px;
max-height: 80vh;
overflow-y: auto;
padding: 16px;
`;
const positions = {
'top-right': { top: '0', right: '0' },
'top-left': { top: '0', left: '0' },
'bottom-right': { bottom: '0', right: '0' },
'bottom-left': { bottom: '0', left: '0' }
};
Object.assign(container.style, positions[position] || positions['top-right']);
document.body.appendChild(container);
return container;
}
Cada notificação é renderizada com template HTML puro e animações CSS:
function renderNotification(notif) {
const el = document.createElement('div');
el.className = `notification notification-${notif.type}`;
el.id = `notif-${notif.id}`;
el.setAttribute('role', 'alert');
el.setAttribute('aria-live', 'assertive');
el.innerHTML = `
<div class="notif-header">
<span class="notif-type-icon">${getIconForType(notif.type)}</span>
<strong class="notif-title">${notif.title}</strong>
<button class="notif-close" aria-label="Fechar notificação">×</button>
</div>
<p class="notif-message">${notif.message}</p>
${notif.action ? `<button class="notif-action">${notif.action.label}</button>` : ''}
`;
el.querySelector('.notif-close').addEventListener('click', () => {
dismissNotification(notif.id);
});
if (notif.action) {
el.querySelector('.notif-action').addEventListener('click', () => {
notif.action.callback();
dismissNotification(notif.id);
});
}
return el;
}
Animações CSS para entrada e saída:
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; max-height: 200px; }
to { opacity: 0; max-height: 0; padding: 0; margin: 0; }
}
.notification {
animation: slideIn 0.3s ease-out;
transition: all 0.3s ease;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 12px 16px;
min-width: 300px;
max-width: 400px;
}
.notification.dismissing {
animation: fadeOut 0.3s ease-in forwards;
}
5. Lógica de interação e ações
Sistema de fila para evitar sobreposição:
class QueueAwareNotificationManager extends PersistentNotificationManager {
constructor() {
super();
this.queue = [];
this.maxVisible = 5;
this.isProcessing = false;
}
addNotification(notification) {
if (this.getActiveNotifications().length >= this.maxVisible) {
this.queue.push(notification);
return null;
}
return super.addNotification(notification);
}
dismissNotification(id) {
super.dismissNotification(id);
this._processQueue();
}
_processQueue() {
if (this.queue.length > 0 && this.getActiveNotifications().length < this.maxVisible) {
const next = this.queue.shift();
super.addNotification(next);
}
}
}
Suporte a diferentes níveis de urgência com cores e ícones:
function getIconForType(type) {
const icons = {
info: 'ℹ️',
warning: '⚠️',
error: '❌',
success: '✅'
};
return icons[type] || 'ℹ️';
}
function getColorForType(type) {
const colors = {
info: '#2196F3',
warning: '#FF9800',
error: '#F44336',
success: '#4CAF50'
};
return colors[type] || '#2196F3';
}
6. Otimizações e boas práticas
Debounce para evitar excesso de notificações:
function createDebouncedNotifier(manager, delay = 500) {
let lastCall = 0;
return (notification) => {
const now = Date.now();
if (now - lastCall < delay) return;
lastCall = now;
return manager.addNotification(notification);
};
}
Controle de duração com timeout automático e acessibilidade:
function addNotificationWithTimeout(manager, notification) {
const id = manager.addNotification(notification);
if (notification.duration > 0) {
setTimeout(() => {
const el = document.getElementById(`notif-${id}`);
if (el) {
el.classList.add('dismissing');
setTimeout(() => manager.dismissNotification(id), 300);
}
}, notification.duration);
}
// Foco automático para leitores de tela
setTimeout(() => {
const el = document.getElementById(`notif-${id}`);
if (el) el.focus();
}, 100);
return id;
}
7. Integração com o ciclo de vida da aplicação
Notificações condicionais baseadas em estado online/offline:
window.addEventListener('online', () => {
const mgr = NotificationManager.instance;
if (mgr) {
mgr.addNotification({
type: 'success',
title: 'Conexão restaurada',
message: 'Você está online novamente.',
priority: 3,
duration: 5000
});
}
});
window.addEventListener('offline', () => {
const mgr = NotificationManager.instance;
if (mgr) {
mgr.addNotification({
type: 'warning',
title: 'Sem conexão',
message: 'Algumas funcionalidades podem estar indisponíveis.',
priority: 4,
duration: 0
});
}
});
Eventos customizados para comunicação entre módulos:
function emitNotificationEvent(eventName, detail) {
const event = new CustomEvent(eventName, { detail, bubbles: true });
document.dispatchEvent(event);
}
// Uso em outros módulos
document.addEventListener('notif:action-taken', (e) => {
console.log('Ação executada:', e.detail);
});
Testes unitários do gerenciador sem dependência de DOM:
// Exemplo de teste (executar em Node.js ou ambiente de teste)
function testNotificationManager() {
const mgr = new NotificationManager();
const id = mgr.addNotification({
type: 'info',
title: 'Teste',
message: 'Mensagem de teste',
priority: 1,
duration: 0
});
console.assert(mgr.getActiveNotifications().length === 1, 'Deveria ter 1 notificação');
mgr.dismissNotification(id);
console.assert(mgr.getActiveNotifications().length === 0, 'Deveria ter 0 notificações');
console.log('Todos os testes passaram!');
}
8. Exemplo prático completo
Abaixo, um sistema funcional mínimo em menos de 200 linhas:
// notification-system.js
class NotificationSystem {
constructor() {
this.container = this._createContainer();
this.manager = new QueueAwareNotificationManager();
this.manager.subscribe(notifications => this._render(notifications));
}
_createContainer() {
const container = document.createElement('div');
container.id = 'notif-container';
container.style.cssText = `
position: fixed; top: 16px; right: 16px; z-index: 9999;
display: flex; flex-direction: column; gap: 8px;
max-width: 400px;
`;
document.body.appendChild(container);
return container;
}
_render(notifications) {
this.container.innerHTML = '';
notifications.forEach(notif => {
const el = document.createElement('div');
el.className = `notif notif-${notif.type}`;
el.innerHTML = `
<span>${this._icon(notif.type)}</span>
<div><strong>${notif.title}</strong><p>${notif.message}</p></div>
<button onclick="notifSystem.dismiss('${notif.id}')">×</button>
`;
this.container.appendChild(el);
});
}
_icon(type) {
return { info: 'ℹ️', warning: '⚠️', error: '❌', success: '✅' }[type] || 'ℹ️';
}
notify(type, title, message, duration = 5000) {
this.manager.addNotification({ type, title, message, priority: 2, duration });
}
dismiss(id) {
this.manager.dismissNotification(id);
}
}
// Estilos inline (ou adicione via CSS)
const style = document.createElement('style');
style.textContent = `
.notif { display: flex; align-items: center; gap: 12px; padding: 12px 16px;
background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);
animation: slideIn 0.3s ease-out; }
.notif-info { border-left: 4px solid #2196F3; }
.notif-success { border-left: 4px solid #4CAF50; }
.notif-warning { border-left: 4px solid #FF9800; }
.notif-error { border-left: 4px solid #F44336; }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; } }
`;
document.head.appendChild(style);
// Uso
const notifSystem = new NotificationSystem();
notifSystem.notify('success', 'Operação concluída', 'Arquivo salvo com sucesso.');
notifSystem.notify('error', 'Erro crítico', 'Falha ao conectar ao servidor.', 0);
Para expansão futura, considere integrar com WebSocket para notificações em tempo real ou Service Workers para notificações push. O sistema atual já oferece uma base sólida e extensível.
Referências
- MDN Web Docs: CustomEvent — Documentação oficial sobre eventos customizados para comunicação entre módulos.
- MDN Web Docs: Window: localStorage — Guia completo sobre armazenamento local para persistência de dados.
- CSS-Tricks: Animations for Notification UI — Tutorial sobre animações CSS para notificações com exemplos práticos.
- W3C: WAI-ARIA Authoring Practices - Alerts — Diretrizes de acessibilidade para notificações com ARIA.
- JavaScript.info: Observer Pattern — Explicação detalhada do padrão Observer aplicado a sistemas de eventos em JavaScript.
- Node.js: Testing without DOM — Documentação sobre testes unitários em Node.js sem dependência de DOM.