Bỏ qua để đến nội dung

Các ràng buộc luồng một lượt cho HTML (ADR-001)

NextPDF kết xuất HyperText Markup Language (HTML) trong một lượt duy nhất theo chiều tiến và không giữ cây phần tử nào trong bộ nhớ. ADR-001 ghi lại lựa chọn đó cùng các ràng buộc mà nó đặt ra cho mọi tính năng Cascading Style Sheets (CSS).

Terminal window
composer require nextpdf/core:^3

Hệ thống con HTML kết xuất HTML và CSS thành Portable Document Format (PDF) trong một lượt luồng duy nhất. ADR-001 (“Stream-based Rendering Pipeline Retention”, được chấp thuận ngày 2026-04-06) là quyết định kiến trúc định nghĩa mô hình này. Trang này giải thích mô hình, các ranh giới của mô hình và những ràng buộc mà người đóng góp phải tuân thủ.

Trong mô hình này, bộ token hóa (HtmlTokenizer) đọc đầu vào một lần và tạo ra một danh sách token phẳng. HtmlParser::processTokens() duyệt danh sách đó từ trái sang phải và ghi các toán tử luồng nội dung PDF vào một bộ đệm chuỗi khi đi đến từng phần tử. Engine không xây dựng đồ thị phần tử bền vững nào giữa các lần gọi. Mọi trạng thái cần tồn tại qua một lần gọi trình xử lý đều được chuyển qua một value object dạng ảnh chụp (HtmlBlockCursor), chứ không qua các nút dùng chung. Việc kế thừa kiểu sử dụng một ngăn xếp push-and-pop gồm các thực thể HtmlStyleState phẳng, không phải một cây con trỏ cha.

Đây không phải là mô hình giữ tài liệu. Engine không giữ cây tài liệu, không bố cục lại nội dung đã ghi và không cho phép đầu vào thay đổi sau khi quá trình phân tích bắt đầu. Ranh giới rất rõ ràng: NextPDF truyền luồng từ đầu đến cuối. Một bộ kết xuất kiểu giữ tài liệu sẽ dựng toàn bộ tài liệu trong bộ nhớ trước; NextPDF thì không.

Hai thao tác cần một mức nhìn trước có giới hạn. Cả hai đều là ngoại lệ tường minh và bị giới hạn. Việc định kích thước cột của bảng quét qua mọi hàng trước khi đặt một ô. Nó đệm các hàng đó trong một bộ đệm bảng tạm thời bên trong TableParser, một ngoại lệ được ADR-001 nêu đích danh. Bộ chọn quan hệ :has() cùng các bộ chọn :last-child:last-of-type dùng một lượt quét trước có giới hạn trên danh sách token phẳng, chứ không duyệt cây. ADR-001 ghi lại cả hai ngoại lệ và các giới hạn của chúng.

Mô hình này an toàn với worker. HtmlParser được khởi tạo một lần cho mỗi yêu cầu, không bao giờ ở dạng singleton. HtmlParser::parse() đặt lại mọi trường ở đầu mỗi lần gọi. Không có trạng thái tĩnh có thể thay đổi nào trong đường kết xuất, vì vậy RoadRunner, Swoole và Laravel Octane có thể tái dùng tiến trình mà không làm rò rỉ trạng thái giữa các tài liệu.

Các ký hiệu dưới đây thực thi những ràng buộc này. Hãy kiểm chứng từng mục với src/Html/.

Ký hiệuVị tríVai trò
HtmlParser::parse(string $html): HtmlRenderResultsrc/Html/HtmlParser.phpĐiểm vào. Đặt lại toàn bộ trạng thái, rồi chạy lượt duy nhất.
HtmlParser::MAX_ELEMENT_COUNT (50_000)src/Html/HtmlParser.phpGiới hạn cứng về số phần tử được xử lý.
HtmlParser::MAX_NESTING_DEPTH (100)src/Html/HtmlParser.phpGiới hạn cứng về độ lồng nhau.
HtmlBlockCursorsrc/Html/HtmlBlockCursor.phpẢnh chụp con trỏ. Cơ chế trạng thái dùng chung duy nhất.
HtmlStyleStatesrc/Html/HtmlStyleState.phpKhung kiểu được đẩy vào ngăn xếp. Không có con trỏ cha.
TableParser::reset()src/Html/TableParser.phpBắt buộc đặt lại bộ đệm bảng tạm thời giữa các bảng.

Bạn không quản lý trực tiếp mô hình luồng. Một lần gọi sẽ kết xuất bất kỳ tài liệu nào được hỗ trợ.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->setTitle('Streaming render');
$doc->addPage();
$doc->writeHtml('<h1>One forward pass</h1><p>No retained tree.</p>');
$doc->save(__DIR__ . '/output/streaming.pdf');

Kết xuất một tài liệu lớn trong giới hạn bộ nhớ cố định. Giới hạn về số phần tử là ranh giới an toàn, vì vậy hãy ước lượng kích thước đầu vào trước khi gọi.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Exception\HtmlParsingException;
/**
* Render trusted HTML, surfacing the streaming-model limits as typed errors.
*
* @param non-empty-string $html
*/
function renderReport(string $html, string $out): void
{
$doc = Document::createStandalone();
$doc->addPage();
try {
$doc->writeHtml($html);
} catch (HtmlParsingException $e) {
// Thrown on the 10 MB input cap, the 50,000-element cap,
// or the 100-level nesting cap. These are model boundaries,
// not transient faults — do not retry.
throw $e;
}
$doc->save($out);
}
  • Giới hạn số phần tử là điểm dừng cứng. Engine ném ra HtmlParsingException tại MAX_ELEMENT_COUNT = 50_000. Hãy chia các báo cáo rất lớn thành nhiều lần gọi writeHtml() hoặc nhiều tài liệu.
  • Giới hạn độ lồng nhau là điểm dừng cứng. Độ sâu vượt quá MAX_NESTING_DEPTH = 100 sẽ ném ngoại lệ. Các lớp bao bọc lồng quá sâu thường là nguyên nhân gây ra điều này.
  • Giới hạn kích thước đầu vào. HtmlParser::parse() từ chối đầu vào lớn hơn 10 MB trước khi token hóa.
  • :has() bị kiểm soát. Lượt quét trước của :has() chỉ chạy khi tính năng thử nghiệm css.has đang được bật. Nếu không có nó, các bộ chọn :has() sẽ không khớp.
  • Việc đệm bảng là cây tạm thời duy nhất. Một bảng đơn lẻ rất rộng hoặc rất cao sẽ giữ các hàng của nó trong bộ nhớ cho đến khi render(). TableParser giới hạn bộ đệm này theo từng bảng và đặt lại giữa các bảng; đây không phải là một cây trên toàn tài liệu.
  • Không bố cục lại. Nội dung đã được ghi sẽ không bao giờ bị di chuyển. Một kiểu xuất hiện muộn không thể thay đổi ngược phần đầu ra trước đó.

Mô hình luồng giữ tối đa một HtmlStyleState cho mỗi mức lồng nhau, bị giới hạn bởi MAX_NESTING_DEPTH = 100, cộng với các trường con trỏ đang hoạt động. Bộ nhớ dành cho trạng thái kiểu và con trỏ là O(depth), không phải O(element count). ADR-001 ghi lại ý đồ thiết kế rằng mức này luôn thấp hơn hẳn so với một đồ thị đối tượng được giữ lại cho cùng một đầu vào. Benchmark đỉnh resident set size (RSS) được kiểm soát với 50,000 phần tử là mục tiêu kiểm định thực nghiệm được nêu trong ADR-001. Benchmark hiệu năng của đường kết xuất HTML theo dõi chỉ số này bằng một cổng kiểm soát hồi quy 5% (công việc đã hợp nhất, PR #564). Hãy xem performance_budget theo từng trang (wall_ms: 1500, peak_mb: 64) là trần vận hành.

Các giới hạn trên trang này cũng đóng vai trò là biện pháp kiểm soát từ chối dịch vụ. DefaultHtmlSecurityPolicy thực thi trần đầu vào 10 MB và trần lồng nhau 100 mức độc lập với bộ phân tích, nên một tài liệu độc hại không thể làm cạn bộ nhớ thông qua độ sâu hoặc kích thước. Bản thân mô hình luồng giới hạn bộ nhớ ngay từ thiết kế: không có đồ thị phần tử nào để kẻ tấn công làm phình to. Hãy xem mô hình bảo mật của mô-đun HTMLcác hợp đồng lớp để biết toàn bộ bề mặt chính sách.

Trang này không trích dẫn tiêu chuẩn bên ngoài nào. Các ràng buộc này đến từ ADR-001 và từ các ký hiệu mã nguồn thực thi được liệt kê trong mục Bề mặt API. Các ánh xạ hành vi theo đặc tả CSS được ghi trong tài liệu tại css-resolver, không ghi ở đây.

Khả năng cho doanh nghiệp. Kiến trúc luồng giống hệt nhau giữa Core và Premium. Premium mở rộng phạm vi hỗ trợ CSS; nó không thay đổi mô hình một lượt và cũng không nới lỏng các giới hạn này. Hãy xem ma trận hỗ trợ CSS.