Nền tảng của Thiết kế Tách biệt Qua Sự kiện
Khi xây dựng ứng dụng tạo tài liệu PDF động, các module như tải dữ liệu, hiển thị giao diện và chuyển đổi định dạng thường dính chặt vào nhau. Sự thay đổi ở một vị trí có thể làm hỏng toàn bộ luồng xử lý. Để giải quyết vấn đề này, jsPDF tích hợp sẵn hệ thống Pub/Sub (Publish-Subscribe) nhẹ, cho phép tách biệt hoàn toàn các quy trình nghiệp vụ.
Cơ chế này mang lại ba lợi ích kỹ thuật chính:
- Giảm phụ thuộc (Low Coupling): Các module không cần import hoặc gọi trực tiếp lẫn nhau, chỉ trao đổi qua chủ đề sự kiện.
- Dễ mở rộng (Extensibility): Tính năng mới có thể được thêm vào bằng cách đăng ký lắng nghe sự kiện mà không sửa đổi lõi thư viện.
- Khó gỡ lỗi hơn (Debuggability): Dòng chảy sự kiện minh bạch, giúp theo dõi trạng thái từng bước của quá trình sinh file.
Trong môi trường tạo PDF, điều này đặc biệt hữu ích khi phối hợp giữa việc nạp font, chèn ảnh đa nguồn và vẽ nội dung trên nhiều trang.
Sơ lược về Cơ chế Nội tại
Hệ thống này được đóng gói gọn nhẹ bên trong đối tượng `internal`. Dữ liệu được lưu trữ dưới dạng cấu trúc Map, nơi mỗi Key là tên chủ đề (topic) và Value là danh sách các Callback đã đăng ký.
// Cấu trúc dữ liệu ẩn giấu trong jsPDF core
var eventRegistry = {
"user-action": [
[callbackFunction, false], // [handler, isOnce]
[anotherHandler, true]
]
};
Hàm `publish` sẽ duyệt qua mảng này và thực thi từng hàm handler. Nếu cờ `isOnce` được đặt là `true`, hệ thống sẽ tự động dọn dẹp sau lần kích hoạt đầu tiên.
Ghi đè ví dụ mã nguồn
Dưới đây là các trường hợp sử dụng thực tế với cấu trúc biến và logic khác biệt so với tài liệu gốc.
Quản lý vòng đời trang
Thay vì gọi phương thức vẽ ngay lập tức, chúng ta phát hành tín hiệu bắt đầu trang mới để các component con phản hồi độc lập.
const pdfDocument = new jsPDF();
const manager = pdfDocument.internal.EventHub;
// Đăng ký xử lý footer cho mọi trang
manager.on('start-page', (info) => {
const pageNumber = info.count;
pdfDocument.setFontSize(8);
pdfDocument.text(`Trang ${pageNumber}`, 105, 290);
});
// Kích hoạt chuỗi sự kiện
manager.emit('start-page', { count: 1 });
// Hủy bỏ bộ lắng nghe khi không dùng đến
manager.off('start-page');
Xử lý bất đồng bộ tài nguyên
Khi nhúng nhiều file hình ảnh, không nên chờ đợi tuần tự. Thay vào đó, dùng counter để theo dõi số lượng hoàn tất.
let pendingResources = ['src/img/icon.png', 'src/img/banner.jpg'];
let finishedLoad = 0;
const targetFile = 'output-report.pdf';
// Khi hệ thống báo có tài nguyên mới
manager.on('resource-ready', (item) => {
const { data } = item;
const x = (Math.random() * 100);
const y = (Math.random() * 100);
pdfDocument.addImage(data, 'PNG', x, y, 80, 60);
finishedLoad++;
if (finishedLoad === pendingResources.length) {
pdfDocument.save(targetFile);
}
});
// Mảng giả lập việc load từ URL
pendingResources.forEach(url => {
fetch(url).then(res => res.blob()).then(blob => {
manager.emit('resource-ready', { url, data: blob });
});
});
Mở rộng hệ thống Sự kiện tùy chỉnh
Để tránh xung đột tên sự kiện giữa các plugin, bạn có thể thiết lập phân cấp namespace thủ công.
class CustomEmitter extends manager {
register(namespace, eventName, listener) {
const fullTopic = `${namespace}.${eventName}`;
return this.once(fullTopic, (payload) => {
if (!payload.namespace) payload.namespace = namespace;
listener(payload);
});
}
}
const ext = new CustomEmitter(pdfDocument.internal);
const token = ext.register('report-module', 'render-changed', (ctx) => {
console.log('Đã cập nhật biểu đồ');
});
Nguyên tắc Tối ưu Hóa
Việc sử dụng đúng cách quyết định hiệu năng của ứng dụng cuối cùng.
Quy ước đặt tên
- Văn phòng nhỏ (lowercase), ngăn cách bằng dấu gạch ngang (kebab-case).
- Định dạng `verb-state` (ví dụ: `document-generated`) để thể hiện sự kiện đã hoàn thành.
- Sử dụng prefix module (`core-`, `pdf-render`) để tránh name collision.
Quản lý Bộ nhớ
Các sự kiện `on-demand` hoặc một lần chạy (`once=true`) phải được gỡ bỏ ngay sau khi thực thi để tránh rò rỉ bộ nhớ, đặc biệt quan trọng trong Single Page Applications (SPA) khi component bị hủy.
Hiệu năng xử lý
Không lạm dụng Pub/Sub cho các tương tác đơn giản. Chỉ sử dụng khi thực sự cần luồng truyền tin ngược chiều hoặc giữa các module độc lập. Đối với các tác vụ lặp lại tần suất cao (như progress bar), hãy áp dụng Debounce hoặc Throttling trước khi gọi hàm publish.