Web Push không chỉ dành cho ứng dụng di động
Đa số developer mặc định rằng muốn gửi thông báo (push notification) lên trình duyệt thì phải dùng Firebase Cloud Messaging (FCM). Thực tế, chúng ta có một giải pháp “thuần khiết” hơn, giúp bạn làm chủ hoàn toàn hạ tầng mà không cần phụ thuộc bên thứ ba. Đó chính là giao thức Web Push chuẩn của W3C.
Mình từng xử lý một dashboard quản lý cho đơn vị logistics với lưu lượng khoảng 1.000 đơn hàng mỗi ngày. Lúc đó, việc tích hợp Firebase SDK khiến bundle size của frontend tăng thêm gần 200KB chỉ để phục vụ một tính năng nhận tin nhắn. Sau khi chuyển sang dùng thư viện web-push trên Node.js, team mình đã loại bỏ hoàn toàn được gánh nặng này. Tốc độ load trang tăng đáng kể, đồng thời quy trình triển khai cũng gọn nhẹ hơn vì không phải setup project phức tạp trên Google Console.
So sánh các phương pháp triển khai
Khi tích hợp thông báo đẩy, bạn thường cân nhắc giữa hai hướng đi chính:
- Firebase Cloud Messaging (FCM): Google đóng vai trò trung gian. Họ lo phần hạ tầng nhưng đổi lại bạn phải nhúng SDK nặng nề và tuân thủ các chính sách ngặt nghèo của họ.
- Giao thức Web Push (VAPID): Đây là cách tiếp cận trực tiếp. Bạn sử dụng cặp khóa VAPID (Voluntary Application Server Identification) để định danh server của mình với các Push Service tích hợp sẵn trong trình duyệt như Chrome hay Firefox.
Tại sao web-push là lựa chọn tối ưu cho Backend Node.js?
Mỗi giải pháp đều có ưu thế riêng. Tuy nhiên, nếu bạn ưu tiên sự tinh gọn và quyền kiểm soát, web-push mang lại những lợi ích rất thực tế:
Ưu điểm nổi bật:
- Bundle size cực nhẹ: Bạn không cần nhúng bộ SDK đồ sộ. Frontend chỉ cần vài dòng Service Worker thuần túy để lắng nghe sự kiện.
- Bảo mật dữ liệu: Thông tin đăng ký (subscription) của người dùng nằm gọn trong database của bạn. Không ai ngoài bạn và trình duyệt biết về chúng.
- Tiết kiệm chi phí: Hoàn toàn miễn phí. Bạn sẽ không bao giờ phải lo lắng về việc vượt hạn mức (quota) hay thay đổi chính sách giá đột ngột từ nhà cung cấp Cloud.
- Tự do tùy biến: Bạn toàn quyền quyết định cách mã hóa payload và thời gian tồn tại (TTL) của thông báo.
Vài điểm cần lưu ý:
- Bạn cần tự thiết kế bảng trong database để lưu trữ
endpointvàkeyscủa user. - Cần viết thêm logic để xử lý cơ chế retry (thử lại) khi gửi tin nhắn thất bại.
Cơ chế hoạt động của Web Push
Để triển khai mượt mà, bạn cần hiểu luồng đi của dữ liệu. Nó không truyền trực tiếp từ Server đến Browser mà đi qua một trạm trung chuyển:
- Subscription: Trình duyệt xin quyền người dùng. Nếu được cho phép, nó trả về một đối tượng
subscriptionchứa URL của Push Service (do Google hoặc Mozilla quản lý). - Lưu trữ: Frontend gửi đối tượng này về Server Node.js để lưu vào database (như MongoDB hoặc PostgreSQL).
- Push: Khi có sự kiện mới, Server dùng thư viện
web-pushký tên vào gói tin bằng khóa VAPID rồi gửi tới Push Service. Push Service sẽ đẩy thông báo xuống máy người dùng, kể cả khi họ đã tắt website.
Hướng dẫn triển khai chi tiết
Hãy bắt tay vào code. Bạn chỉ cần máy tính đã cài sẵn Node.js.
Bước 1: Khởi tạo Project và tạo khóa VAPID
Đầu tiên, hãy tạo thư mục và cài đặt các package cần thiết:
mkdir web-push-tutorial
cd web-push-tutorial
npm init -y
npm install web-push express body-parser cors
Cặp khóa VAPID đóng vai trò như chữ ký số của server. Bạn tạo nhanh bằng lệnh:
./node_modules/.bin/web-push generate-vapid-keys
Hãy copy lại Public Key và Private Key. Chúng ta sẽ dùng chúng ở các bước tiếp theo.
Bước 2: Xây dựng Server Node.js
Tạo file server.js. Đây là bộ não điều khiển việc gửi thông báo.
const webpush = require('web-push');
const express = require('express');
const app = express();
app.use(require('cors')());
app.use(require('body-parser').json());
const publicVapidKey = 'YOUR_PUBLIC_KEY';
const privateVapidKey = 'YOUR_PRIVATE_KEY';
webpush.setVapidDetails(
'mailto:[email protected]',
publicVapidKey,
privateVapidKey
);
app.post('/subscribe', (req, res) => {
const subscription = req.body;
res.status(201).json({});
const payload = JSON.stringify({
title: 'Thông báo hệ thống',
body: 'Đơn hàng #1234 của bạn đã được xác nhận!',
});
webpush.sendNotification(subscription, payload)
.catch(err => console.error('Gửi push thất bại:', err));
});
app.listen(5000, () => console.log('Server sẵn sàng tại port 5000'));
Bước 3: Thiết lập Client và Service Worker
Tại frontend, file client.js sẽ đảm nhận việc đăng ký với trình duyệt.
const publicVapidKey = 'YOUR_PUBLIC_KEY';
async function subscribeUser() {
// Đăng ký Service Worker
const register = await navigator.serviceWorker.register('/sw.js', { scope: '/' });
// Tạo subscription
const subscription = await register.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
});
// Gửi thông tin về server
await fetch('http://localhost:5000/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: { 'content-type': 'application/json' }
});
}
Cuối cùng là sw.js. File này chạy ngầm để hiển thị popup thông báo:
self.addEventListener('push', e => {
const data = e.data.json();
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icon.png'
});
});
Kinh nghiệm thực tế để tránh lỗi
Khi đưa hệ thống này lên môi trường production, bạn cần lưu ý 3 điểm quan trọng:
- Bắt buộc dùng HTTPS: Trình duyệt sẽ từ chối Service Worker nếu trang web của bạn chạy trên HTTP (ngoại trừ localhost).
- Dọn dẹp Database: Khi người dùng chặn thông báo, Push Service sẽ trả về lỗi 404 hoặc 410. Bạn nên xóa ngay subscription đó trong database để tránh lãng phí tài nguyên.
- Giới hạn Payload: Kích thước gói tin tối đa là 4KB. Đừng cố gửi quá nhiều dữ liệu hoặc ảnh base64 vào đây.
Tự xây dựng hệ thống Web Push không hề khó như chúng ta tưởng. Nó mang lại sự linh hoạt tuyệt đối và giúp ứng dụng của bạn chuyên nghiệp hơn mà không cần phụ thuộc vào hệ sinh thái của Google. Chúc các bạn áp dụng thành công!

