Quay lại Blog
javascriptreactpdf-libpdfjsclient-sideweb development

Kỹ Thuật Xây Dựng Công Cụ Ghép PDF Client-Side Bằng JavaScript

Phân tích kỹ thuật chi tiết về cách xây dựng một công cụ gộp PDF bảo mật, nhanh chóng và hoàn toàn chạy trên trình duyệt (Client-Side) bằng React, pdf-lib và pdfjs-dist.

April 1, 2026Bởi Toolavin Engineering

Dùng thử ngay

Sử dụng Ghép PDF miễn phí để kiểm tra văn bản ngay lập tức — không cần đăng ký.

Mở Công cụ

Kỹ Thuật Xây Dựng Công Cụ Ghép PDF Bằng JavaScript

Khi xây dựng các công cụ xử lý tài liệu trên web, cách dễ nhất thường là đẩy phần việc nặng nhọc sang Backend (máy chủ). Bạn tải file PDF lên, máy chủ chạy một tập lệnh Python hoặc Node.js (dùng Ghostscript hoặc PDFtk), sau đó gửi trả lại tệp đã ghép.

Tuy nhiên, cách tiếp cận này đi kèm với những nhược điểm lớn: chi phí máy chủ cao, tốc độ tải lên/tải xuống chậm, và những lo ngại về quyền riêng tư.

Bằng cách đưa khối lượng công việc sang xử lý hoàn toàn ở phía Client (trình duyệt web của người dùng) bằng JavaScript, bạn tạo ra một công cụ thao tác siêu tốc, cực kỳ bảo mật và dễ dàng bảo trì. Trong bài viết này, chúng tôi sẽ giải thích kiến trúc kỹ thuật đằng sau công cụ Ghép PDF của mình.

Bộ Công Cụ Công Nghệ (Tech Stack)

Để xây dựng một trình quản lý PDF mạnh mẽ trên trình duyệt, bạn cần hai thứ: một thư viện chỉnh sửa cấu trúc PDF và một thư viện để kết xuất (render) hình ảnh các trang để hiển thị lên UI.

  • UI Framework: React (Next.js)
  • Thao tác PDF: pdf-lib (dùng để đọc, hợp nhất và ghi dữ liệu PDF mới)
  • Render hình ảnh PDF: pdfjs-dist (thư viện PDF.js của Mozilla, dùng để vẽ ảnh bìa Thumbnail cho các tệp PDF)
Sơ đồ kiến trúc hiển thị sự phân chia nhiệm vụ giữa pdfjs-dist (dùng cho render ảnh) và pdf-lib (dùng cho ghép dữ liệu).
Sơ đồ kiến trúc hiển thị sự phân chia nhiệm vụ giữa pdfjs-dist (dùng cho render ảnh) và pdf-lib (dùng cho ghép dữ liệu).

1. Kết xuất (Render) Thumbnail với PDF.js

Để có trải nghiệm người dùng tốt, họ cần nhìn thấy ảnh thu nhỏ (thumbnail) của các file PDF tải lên. pdfjs-dist rất hoàn hảo cho việc này, tuy nhiên việc render PDF tốn rất nhiều CPU. Để tránh làm đơ giao diện trình duyệt, bắt buộc phải sử dụng Web Worker.

Dưới đây là cách chúng tôi render trang đầu tiên của file PDF sang một chuỗi ảnh JPEG (Data URL):

import * as pdfjsLib from "pdfjs-dist";

// Cấu hình URL của Web Worker
pdfjsLib.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`;

async function renderPdfThumbnail(file: File): Promise<string | null> {
    const buf = await file.arrayBuffer();
    const pdf = await pdfjsLib.getDocument({ data: buf }).promise;
    const page = await pdf.getPage(1);

    // Render ở tỷ lệ thu nhỏ để tiết kiệm bộ nhớ (ví dụ: 60%)
    const viewport = page.getViewport({ scale: 0.6 });
    const canvas = document.createElement("canvas");
    canvas.width = viewport.width;
    canvas.height = viewport.height;

    const ctx = canvas.getContext("2d");
    if (!ctx) return null;
    
    await page.render({ canvasContext: ctx, viewport }).promise;
    
    // Chuyển đổi thành ảnh JPEG tốc độ cao
    const dataUrl = canvas.toDataURL("image/jpeg", 0.75);
    pdf.destroy();
    
    return dataUrl;
}

2. Ghép Các Tệp PDF bằng pdf-lib

Việc hợp nhất các file diễn ra khi người dùng nhấn nút "Ghép PDF". Chúng tôi chọn pdf-lib vì nó xử lý rất mượt việc sao chép (copy) các trang giữa các Instance PDFDocument khác nhau mà không làm hỏng siêu dữ liệu (metadata) hay tài nguyên font chữ.

Quy trình hoạt động:

  1. Tạo một PDFDocument mới, trống.
  2. Lặp qua danh sách các file người dùng đã sắp xếp.
  3. Phân tích từng file dưới dạng PDFDocument.
  4. Sao chép các trang của nó và thêm vào tài liệu mới tạo ở bước 1.
  5. Lưu tài liệu mới thành Uint8Array (Blob) và kích hoạt hành động Download.
import { PDFDocument, degrees } from "pdf-lib";

async function mergePdfs(files: { file: File, rotation: number }[]) {
    // 1. Tạo một Document mới
    const mergedDoc = await PDFDocument.create();

    for (const pdfFile of files) {
        // 2. Tải Document nguồn
        const buf = await pdfFile.file.arrayBuffer();
        const srcDoc = await PDFDocument.load(buf, { ignoreEncryption: true });

        // 3. Xử lý các thao tác Xoay (Rotate) của người dùng
        if (pdfFile.rotation !== 0) {
            srcDoc.getPages().forEach((p) => {
                const currentAngle = p.getRotation().angle;
                p.setRotation(degrees((currentAngle + pdfFile.rotation) % 360));
            });
        }

        // 4. Sao chép tất cả các trang từ file gốc sang file ghép
        const copiedPages = await mergedDoc.copyPages(srcDoc, srcDoc.getPageIndices());
        copiedPages.forEach((p) => mergedDoc.addPage(p));
    }

    // 5. Lưu kết quả và tạo link tải xuống
    const mergedBytes = await mergedDoc.save();
    const blob = new Blob([mergedBytes], { type: "application/pdf" });
    const url = URL.createObjectURL(blob);
    
    // Logic kích hoạt tải xuống...
}

3. Xử Lý Kéo Thả và Thay Đổi Thứ Tự

Để tạo khả năng kéo thả (drag and drop) mượt mà trong React, chúng tôi tự triển khai các trình xử lý sự kiện mặc định là onDragStart, onDragEnteronDragEnd của HTML5.

Khi một phần tử được kéo đè lên một phần tử khác, React State sẽ lập tức cập nhật vị trí của chúng. Điều này tạo ra hiệu ứng phản hồi chuyển động tức thì mà không cần cài cắm thêm các thư viện kéo thả (như dnd-kit) làm nặng mã nguồn.

Kết Luận

Bằng cách đẩy hoàn toàn mọi quy trình xử lý tài liệu sang trình duyệt, các ứng dụng web ngày nay có sức mạnh ngang ngửa với phần mềm trên máy tính Desktop trong khi vượt xa về độ tiện lợi, sự miễn phí và tính riêng tư. Kết hợp pdf-lib cho cấu trúc dữ liệu và pdfjs-dist cho hiển thị hình ảnh là công thức tối thượng để thiết lập nên một hệ sinh thái chỉnh sửa PDF mạnh mẽ.

Sẵn sàng dùng thử?

Sử dụng Ghép PDF miễn phí — không cần tài khoản.

Mở Ghép PDF