StreamPDFStreamPDF
Quay lại blog

PDF.js Khám phá sâu từ Demo 2: Toàn cảnh phương thức getDocument

27 tháng 10, 2026 · StreamPDF Team

PDF.js Khám phá sâu từ Demo 2: Toàn cảnh phương thức getDocument

Lời nói đầu

Demo trong bài viết trước gọi getDocument với chỉ { url: '...' }. Bài viết này theo lời gọi đó đi qua toàn bộ chuỗi thực thi của getDocument.

Lời gọi getDocument

Tham số hình thức trong mã nguồn tên là src. Demo thực tế chỉ truyền một khóa:

Khóa trong srcGiá trị
url'https://assets-blog.streampdf.com/PDF32000_2008.pdf'

Dựa trên định nghĩa của getDocument trong display/api.js, luồng thực thi mã như sau

Dòng 235-236: Khởi tạo tác vụ loading

Khởi tạo lớp PDFDocumentLoadingTask: nhấp để xem lời gọi, và gán giá trị trả về cho biến task

Lấy biến docId từ task, tại đây docId là d1

Dòng 238-335: Kiểm tra & xử lý tham số

Đoạn mã này bung đối tượng src do người dùng truyền vào thành 35+ biến cục bộ: giá trị truyền vào sẽ được kiểm tra và chuyển đổi kiểu, giá trị không truyền vào sẽ lấy mặc định. Demo chỉ truyền url, nên phần lớn tham số đi theo nhánh mặc định.

Tham số url duy nhất được truyền vào, qua getUrlProp chuyển thành đối tượng URL:

const url = URL.parse('https://assets-blog.streampdf.com/PDF32000_2008.pdf', window.location);
// const url = src.url ? getUrlProp(src.url) : null;

Các tham số còn lại đều không truyền vào. Theo thứ tự mã nguồn, kết quả tính toán như sau:

Biến cục bộKết quảLogic lấy giá trị
datanullNếu truyền vào thì qua getDataProp chuyển đồng bộ thành Uint8Array
httpHeadersnullTruyền nguyên, không truyền thì null
withCredentialsfalseChỉ chấp nhận === true
passwordnull?? null
rangeTransportnullChỉ chấp nhận thực thể PDFDataRangeTransport
rangeChunkSize2 ** 16 (64KB)Chỉ chấp nhận giá trị truyền vào nếu là số nguyên dương
workernullChỉ chấp nhận thực thể PDFWorker
verbosityundefinedLấy nguyên từ src.verbosity
docBaseUrlnullChỉ chấp nhận chuỗi không phải giao thức data:
cMapUrlnullgetFactoryUrlProp: không phải chuỗi thì null, và phải kết thúc bằng /
cMapPackedtrue!== false
iccUrlnullGiống cMapUrl
standardFontDataUrlnullGiống cMapUrl
wasmUrlnullGiống cMapUrl
ignoreErrorstruesrc.stopAtErrors !== true, xem bên dưới
maxImageSize-1 (không giới hạn)Chỉ chấp nhận là số nguyên và > -1
isOffscreenCanvasSupportedtrueKhông truyền thì lấy !isNodeJS
isImageDecoderSupportedtrueKhông truyền thì lấy !isNodeJS
canvasMaxAreaInBytes-1 (không giới hạn)Chỉ chấp nhận là số nguyên
disableFontFacefalseKhông truyền thì lấy isNodeJS
fontExtraPropertiesfalseChỉ chấp nhận === true
enableXfafalseChỉ chấp nhận === true
ownerDocumentglobalThis.documentKhông truyền thì lấy document toàn cục
disableRangefalseChỉ chấp nhận === true
disableStreamfalseChỉ chấp nhận === true
disableAutoFetchfalseChỉ chấp nhận === true
pdfBugfalseChỉ chấp nhận === true
CanvasFactoryDOMCanvasFactoryNhánh môi trường, xem bên dưới
FilterFactoryDOMFilterFactoryGiống trên
BinaryDataFactoryDOMBinaryDataFactoryGiống trên
enableHWAfalseChỉ chấp nhận === true
enableWebGPUfalseChỉ chấp nhận === true
gpuPromisePromise.resolve(false)WebGPU tắt, không khởi tạo GPU
useWasmtrue!== false
pagesMappernew PagesMapper()Không truyền thì tạo mới
useSystemFontstrueMặc định phụ thuộc tham số khác, xem bên dưới
useWorkerFetchfalseKiểm tra tổ hợp, xem Hỏi & Đáp bên dưới
styleElementnullChỉ giữ giá trị truyền vào ở bản dựng TESTING, trong生产 luôn là null

Trên đây là tất cả biến cục bộ mà đoạn mã này tạo ra. Hầu hết các kiểm tra đều là trường hợp「không truyền thì lấy mặc định」thông thường, nhưng có vài thứ đáng để xem xét kỹ hơn.

ignoreErrors: biến nội bộ có nghĩa nghịch đảo

Tham số hướng đến người dùng tên là stopAtErrors, nhưng biến nội bộ lại tên là ignoreErrors, với logic取值 là src.stopAtErrors !== true. Nghĩa là hành vi mặc định là bỏ qua lỗi: khi parse lỗi, cố gắng khôi phục trạng thái thay vì reject các promise như getOperatorList / getTextContent / RenderTask. Khi đọc mã nguồn, đây là chỗ rất dễ nhầm lẫn giữa tên và giá trị.

rangeChunkSize: chuẩn bị 64KB cho phần sau

rangeChunkSize mặc định là 64KB. Do disableRange = false, khi tạo transport ở phần sau, sẽ phát các yêu cầu range đối với PDF, và mỗi chunk được lấy sẽ có đúng kích thước này. Hãy nhớ nó nhé.

docBaseUrl: tại sao loại trừ data: URL

Bình luận trong mã nguồn giải thích lý do: URL data: không thể dùng để khôi phục đường dẫn tuyệt đối, và nó chứa toàn bộ tài liệu PDF với độ dài không giới hạn, nên không gửi nó sang luồng worker.

gpuPromise: thiết kế chạy song song với khởi động worker

const gpuPromise = Promise.resolve(false); // enableWebGPU ? initGPU() : Promise.resolve(false);

Ngay cả khi WebGPU không được bật, tại đây vẫn trả về promise đã resolve ngay lập tức, thay vì trì hoãn kiểm tra cho đến khi sử dụng. Bình luận giải thích ý định: cho phép khởi tạo GPU chạy song song với khởi động worker, giá trị boolean sau khi resolve sẽ được chuyển tiếp cho worker.

Ba Factory: nhánh môi trường

const CanvasFactory = DOMCanvasFactory;
// src.CanvasFactory || (GENERIC build && isNodeJS ? NodeCanvasFactory : DOMCanvasFactory);

FilterFactory và BinaryDataFactory cũng tương tự. Kiểm tra có ba cấp: tùy biến của người dùng > triển khai môi trường Node > triển khai DOM. Demo chạy trên trình duyệt mà không có tùy biến, nên cả ba factory đều là phiên bản DOM. Nhiệm vụ của từng cái:

  • CanvasFactory: tạo canvas để render
  • FilterFactory: tạo bộ lọc hình ảnh (như bộ lọc SVG)
  • BinaryDataFactory: tải tài nguyên tĩnh tích hợp, phối hợp với useWorkerFetch ở bên dưới

useSystemFonts: giá trị mặc định phụ thuộc tham số khác

const useSystemFonts = true;
// typeof src.useSystemFonts === "boolean" ? src.useSystemFonts : !isNodeJS && !disableFontFace;

Giá trị mặc định của nó phụ thuộc vào disableFontFace, vừa được parse ở trên. Tại sao? Phông chữ không được nhúng trong PDF sẽ được chuyển thành FontFace để tải; nếu disableFontFace là true, đường dẫn tải này bị tắt, và lúc đó dựa vào phông chữ hệ thống sẽ dẫn đến glyph hoàn toàn lộn xộn. Vì vậy trong trường hợp này giá trị mặc định cũng là false, để tránh render ra phông chữ bị hỏng hoàn toàn.

useWorkerFetch: kiểm tra tổ hợp

const useWorkerFetch = false;
// typeof src.useWorkerFetch === "boolean"
//   ? src.useWorkerFetch
//   : MOZCENTRAL ||
//     !!(
//       BinaryDataFactory === DOMBinaryDataFactory &&
//       cMapUrl && cMapPacked && standardFontDataUrl && wasmUrl &&
//       isValidFetchUrl(cMapUrl, document.baseURI) &&
//       isValidFetchUrl(standardFontDataUrl, document.baseURI) &&
//       isValidFetchUrl(wasmUrl, document.baseURI)
//     );

H: Kiểm tra này đang làm gì? Đ: Quyết định ai sẽ tải các「tài nguyên tĩnh tích hợp」(tệp CMap, tệp phông chữ chuẩn, tệp wasm): luồng worker tự tải bằng Fetch API (true), hay luồng chính tải rồi chuyển tiếp cho worker (false).

Nếu người dùng truyền boolean rõ ràng, dùng giá trị của người dùng trực tiếp. Còn không thì tính giá trị mặc định: MOZCENTRAL (viewer tích hợp Firefox) luôn là true; bản dựng thông thường phải đồng thời thỏa mãn ba điều kiện sau mới là true:

  1. Không có BinaryDataFactory tùy biến —— vì khi useWorkerFetch là true, tham số này sẽ bị bỏ qua trực tiếp (bên trong transportFactory, binaryDataFactory được đặt thành null). Factory tùy biến của người dùng phải được tôn trọng, nên mặc định không thể là true;
  2. cMapUrl / standardFontDataUrl / wasmUrl đều được cung cấp, và cMapPacked là true —— không có URL thì không có gì để fetch;
  3. Ba URL này sau khi resolve tương đối với document.baseURI là http/https —— Fetch API chỉ hỗ trợ http/https, không hỗ trợ file/ftp.

Demo của bài này chỉ truyền url, cMapUrl là null, điều kiện 2 không thỏa mãn, nên useWorkerFetch = false: đi theo đường dẫn luồng chính, DOMBinaryDataFactory tải trên luồng chính, worker khi cần sẽ yêu cầu từ luồng chính qua tin nhắn FetchBinaryData.

Dòng 338: Đặt mức độ nhật ký luồng chính

setVerbosityLevel(verbosity); // verbosity là undefined

verbosity là undefined, nên mức độ nhật ký luồng chính giữ mặc định. Lưu ý điều này chỉ ảnh hưởng đến luồng chính; mức độ nhật ký của luồng worker sẽ được truyền riêng khi tạo worker.

Dòng 342-350: Khởi tạo ba factory

const transportFactory = {
  canvasFactory: new DOMCanvasFactory({ ownerDocument: document, enableHWA: false }),
  filterFactory: new DOMFilterFactory({ docId: 'd1', ownerDocument: document }),
  binaryDataFactory: new DOMBinaryDataFactory({
    cMapUrl: null,
    standardFontDataUrl: null,
    wasmUrl: null,
  }),
};
// binaryDataFactory: null khi MOZCENTRAL hoặc useWorkerFetch là true

Khởi tạo ba lớp Factory đã chọn trước đó, gói thành transportFactory, sau đó giao cho transport sử dụng. Do useWorkerFetch = false, binaryDataFactory không phải null —— 呼应 với Hỏi & Đáp ở phần trước: tài nguyên tĩnh tích hợp được tải trên luồng chính.

Khả năng bên trong của ba thực thể factory sẽ được trình bày ở bài viết riêng.

Dòng 352-360: Tạo Worker

worker = PDFWorker.create({ verbosity: undefined, port: undefined });
// port đến từ GlobalWorkerOptions.workerPort, demo không đặt
task._worker = worker;

Demo không truyền worker, nên PDF.js tự tạo và sở hữu một. PDFWorker.create trả về thực thể PDFWorker một cách đồng bộ, hàm khởi tạo của nó sẽ đọc GlobalWorkerOptions.workerSrc (tức là địa chỉ tệp worker cấu hình ở bài thứ nhất), tạo Worker và hoàn tất bắt tay khởi động qua tin nhắn ready / test —— đây là điểm trọng tâm, đã được trình bày chi tiết ở bài 8. Ở đây chỉ cần nhớ: đối tượng worker trả về có một promise, sẽ resolve khi worker sẵn sàng.

Dòng 362-402: Lắp hai gói tham số

Gói đầu tiên docParams, sau đó sẽ được gửi xuyên luồng sang worker:

const docParams = {
  docId: 'd1',
  apiVersion: '6.3.289', // số phiên bản bản dựng
  data: null,
  password: null,
  disableAutoFetch: false,
  rangeChunkSize: 65536,
  docBaseUrl: null,
  enableXfa: false,
  evaluatorOptions: {
    maxImageSize: -1,
    disableFontFace: false,
    ignoreErrors: true,
    isOffscreenCanvasSupported: true,
    isImageDecoderSupported: true,
    canvasMaxAreaInBytes: -1,
    fontExtraProperties: false,
    useSystemFonts: true,
    useWasm: true,
    useWorkerFetch: false,
    cMapUrl: null,
    cMapPacked: true,
    iccUrl: null,
    standardFontDataUrl: null,
    wasmUrl: null,
    hasGPU: false, // placeholder, sẽ điền lại sau
  },
};

Gói thứ hai transportParams, chỉ留在 luồng chính:

const transportParams = {
  ownerDocument: document,
  pdfBug: false,
  styleElement: null,
  enableHWA: false,
  loadingParams: { disableAutoFetch: false, enableXfa: false },
};

Một câu để phân biệt: docParams là「lệnh làm việc」gửi cho worker, còn transportParams là「cấu hình cục bộ」luồng chính tự giữ.

Dòng 404-473: Bắt tay sau khi worker sẵn sàng

Promise.all([worker.promise, gpuPromise]).then(...)

Sau khi cả worker sẵn sàng và phát hiện GPU (resolve là false) đều hoàn tất, tuần tự xảy ra:

  1. Điền lại hasGPU = false vào evaluatorOptions;
  2. Gửi tin nhắn GetDocRequest cho worker, payload là docParams. data là null, nên không có buffer nào cần chuyển quyền sở hữu;
  3. Chọn luồng mạng: data là null và rangeTransport là null, đi theo nhánh url, khởi tạo luồng mạng (ưu tiên triển khai fetch, không hỗ trợ thì fallback XHR; cách phát yêu cầu range bên trong, xem bài 9):
networkStream = new NetworkStream({
  url,
  httpHeaders: null,
  withCredentials: false,
  rangeChunkSize: 65536,
  disableRange: false,
  disableStream: false,
});
  1. Sau khi nhận được回执 của worker đối với GetDocRequest (workerId), tạo MessageHandler và WorkerTransport, và gắn vào task._transport —— hai lớp cốt lõi này lần lượt được trình bày ở bài 6 và 7;
  2. Gửi tin nhắn Ready cho worker, phía luồng chính tuyên bố sẵn sàng; hàm xử lý Ready của worker sau đó gọi setupDoc(docParams).

Dòng 474-475: Giá trị trả về

Toàn bộ chuỗi .then trên tiến hành bất đồng bộ ở hậu trường, còn getDocument bản thân trả về đồng bộ task. Bước nào trong chuỗi gặp lỗi sẽ reject capability bên trong task, tức là promise mà demo đang đợi với await loadingTask.promise.

Đến đây getDocument đã thực thi xong.

Tổng kết

Trong phần này, chúng ta theo dõi demo đi qua toàn bộ lời gọi getDocument:

  1. Tạo PDFDocumentLoadingTask, lấy docId = 'd1';
  2. Bung src thành 35+ biến cục bộ, demo chỉ truyền url, còn lại đều rơi vào giá trị mặc định;
  3. Khởi tạo ba factory DOM, gói thành transportFactory;
  4. PDFWorker.create tạo worker và khởi động bắt tay;
  5. Lắp docParams (gửi cho worker) và transportParams (留在 luồng chính);
  6. Sau khi worker sẵn sàng, gửi GetDocRequest, thiết lập luồng mạng, tạo MessageHandler và WorkerTransport, cuối cùng gửi Ready;
  7. Trả về task đồng bộ, demo đợi tài liệu sẵn sàng qua await loadingTask.promise.

Trong quá trình, chúng ta cố ý bỏ qua bốn「nhân vật quan trọng」: việc tạo và bắt tay của PDFWorker, cơ chế tin nhắn của MessageHandler, khởi tạo của WorkerTransport, và chiến lược yêu cầu range của luồng mạng. Mỗi cái sẽ có bài viết riêng, bắt đầu từ bài tiếp theo sẽ lần lượt đi sâu vào.

Tiếp tục PDF.js Khám phá sâu từ Demo