Server-Side Rendering: Khi định nghĩa này thực sự chỉ là việc render từ máy chủ
Bài viết này nhìn lại sự thay đổi của khái niệm "render phía máy chủ" (SSR) kể từ khi các framework JavaScript lên ngôi và giới thiệu giải pháp của Waaseyaa. Đây là một gói SSR sử dụng PHP và Twig để tạo HTML thuần túy, loại bỏ các bước phức tạp như hydration hay virtual DOM, mang lại trải nghiệm phát triển đơn giản và hiệu quả.

Chào các bạn!
Khoảng năm 2016, khái niệm "render phía máy chủ" (server-side rendering) không còn mang nghĩa là "máy chủ tạo ra HTML" nữa. Nó bắt đầu chuyển sang ý nghĩa "chạy framework JavaScript của bạn trên máy chủ để nó tạo ra HTML mà trình duyệt sẽ sau đó vứt bỏ và xây dựng lại". Ngành công nghiệp dường như đã quên mất cách gọi tên của nó sau khi React xuất hiện.
Gói SSR của Waaseyaa thực hiện việc theo cách gốc thủy: Một yêu cầu (request) được gửi đến, PHP xử lý template, Twig render HTML và máy chủ gửi trả về. Không có bước hydration (cấp nước), không có so sánh virtual DOM (virtual DOM diffing), và cũng chẳng cần thư mục node_modules dung lượng 200MB chỉ để sinh ra một thẻ <div>.
Bài viết này sẽ đi sâu vào cách hoạt động của pipeline render: từ yêu cầu ban đầu đến HTML cuối cùng, với các thành phần như entity renderer, field formatters và theme chain loader giúp nó trở nên thú vị hơn những câu lệnh echo đơn thuần trong tệp .php.
Cơ chế thực sự của pipeline render
Điểm nhập vào là SsrPageHandler::handleRenderPage(). Phương thức này nhận một đường dẫn (path), một tài khoản (account) và một yêu cầu HTTP. Nó trả về một mảng bao gồm HTML đã được render, mã trạng thái (status code) và các header. Chỉ có vậy.
public function handleRenderPage(
string $path,
AccountInterface $account,
HttpRequest $httpRequest,
string $requestedViewMode = 'full',
): array {
Chữ ký phương thức cho bạn biết những gì quan trọng: đường dẫn cần render, ai đang yêu cầu và chế độ hiển thị (view mode) họ muốn. Kiểu trả về là một mảng cấu trúc, không phải đối tượng phản hồi đặc thù của framework nào cả. Kernel sẽ quyết định cách gửi nó đi.
Giữa việc nhận đường dẫn và trả về HTML, năm thứ sẽ diễn ra theo trình tự:
- Thương lượng ngôn ngữ (Language negotiation): Xác định ngôn ngữ nội dung từ tiền tố URL và header
Accept-Language. - Phân giải đường dẫn (Path alias resolution): Ánh xạ các URL thân thiện với người dùng tới các tham chiếu entity.
- Kiểm soát hiển thị biên tập (Editorial visibility): Kiểm tra xem tài khoản hiện tại có được phép xem nội dung đó không.
- Render Entity: Chuyển đổi entity thành một túi biến (variable bag) của Twig với các trường đã được định dạng.
- Phân giải template (Template resolution): Tìm kiếm template Twig cụ thể nhất để render.
Nếu đường dẫn không giải quyết được thành một entity, RenderController sẽ thử tìm một template dựa trên đường dẫn thay thế. Ví dụ, truy cập /about nó sẽ tìm about.html.twig. Truy cập / nó sẽ tìm home.html.twig. Không cần tệp định tuyến (route file) nào cả.
Bước 1 đến 3 thu hẹp những gì cần render. Bước 4 mới là nơi thực sự thú vị.
Cách entity trở thành biến template
EntityRenderer là nơi công việc thực sự diễn ra. Nó nhận một entity và một chế độ hiển thị (view mode), sau đó trả về một mảng phẳng mà Twig có thể tiêu thụ trực tiếp:
public function render(EntityInterface $entity, ViewMode|string $viewMode = 'full'): array
{
$mode = $viewMode instanceof ViewMode ? $viewMode->name : (string) $viewMode;
$entityTypeId = $entity->getEntityTypeId();
$definition = $this->entityTypeManager->getDefinition($entityTypeId);
$fieldDefinitions = $definition->getFieldDefinitions();
$display = $this->viewModeConfig->getDisplay($entityTypeId, $mode);
// ... định dạng trường diễn ra ở đây ...
return [
'entity' => $entity,
'entity_type' => $entityTypeId,
'bundle' => $entity->bundle(),
'view_mode' => $mode,
'template_suggestions' => $this->buildTemplateSuggestions($entityTypeId, (string) $entity->bundle(), $mode),
'fields' => $fields,
];
}
Giá trị trả về là một mảng kết hợp (associative array) đơn giản. Mỗi trường dữ liệu nhận được ba thứ: giá trị gốc (raw value), chuỗi đã định dạng sẵn để xuất ra và kiểu dữ liệu của trường đó. Template Twig của bạn có thể dùng {{ fields.body.formatted }} để lấy HTML đã xử lý hoặc {{ fields.body.raw }} khi bạn cần dữ liệu ban đầu.
Cấu hình chế độ hiển thị (view mode) kiểm soát các trường xuất hiện và thứ tự của chúng. Chế độ teaser có thể chỉ hiển thị tiêu đề và tóm tắt. Chế độ full hiển thị mọi thứ. Nếu không có cấu hình hiển thị nào tồn tại cho một view mode, renderer sẽ xây dựng một mặc định hợp lý từ các định nghĩa trường của entity.
Field formatters: Output an toàn kiểu dữ liệu mà không rườm rà
Mỗi kiểu trường dữ liệu có một formatter biết cách chuyển đổi giá trị thô thành HTML an toàn. Gói này đi kèm với các formatter cho các trường hợp phổ biến:
PlainTextFormattercho chuỗi (với việc thoát ký tự - escaping phù hợp).HtmlFormattercho văn bản định dạng.DateFormattercho dấu thời gian (timestamps).ImageFormattercho các trường ảnh.BooleanFormattercho các cờ (flags).EntityReferenceFormattercho các mối quan hệ giữa các entities.
FieldFormatterRegistry ánh xạ các kiểu trường đến các formatter. Khi entity renderer xử lý một trường, nó hỏi registry về formatter phù hợp và gọi nó:
$fields[$fieldName] = [
'raw' => $raw,
'formatted' => $this->formatterRegistry->format($formatterType, $raw, $settings),
'type' => $fieldType,
];
Một dòng code xử lý việc phân phát. Formatter sẽ thực hiện việc thoát ký tự, định dạng ngày tháng hoặc giải quyết tham chiếu. Template của bạn không bao giờ phải lo lắng xem một giá trị có an toàn để xuất ra hay không.
Bạn có thể đăng ký các formatter tùy chỉnh cho các kiểu dữ liệu đặc thù. Thuộc tính #[AsFormatter] đánh dấu một lớp là một formatter, và registry sẽ tự động nhận diện nó.
Phân giải template: Chain loader
Waaseyaa sử dụng ChainLoader của Twig để tìm kiếm template theo thứ tự ưu tiên. ThemeServiceProvider xây dựng chuỗi này khi khởi động (boot):
public static function createTemplateChainLoader(
string $projectRoot,
string $activeTheme = '',
): ChainLoader {
$chain = new ChainLoader();
// 1) App templates (ưu tiên cao nhất)
self::addPathLoaderIfExists($chain, $root . '/templates');
// 2) Active theme templates
// ... được phát hiện từ metadata của composer ...
// 3) Package templates
// ... từ packages/*/templates ...
// 4) Base SSR templates (ưu tiên thấp nhất)
self::addPathLoaderIfExists($chain, $root . '/packages/ssr/templates');
return $chain;
}
Thư mục templates/ của ứng dụng của bạn được ưu tiên hơn mọi thứ. Theme đang hoạt động xếp bên dưới nó. Templates của gói (package) đến tiếp theo. Gói SSR cơ bản cung cấp phương án dự phòng (fallback).
Điều này có nghĩa là bạn có thể ghi đè bất kỳ template nào ở bất kỳ cấp độ nào. Muốn trang lỗi 404 tùy chỉnh? Hãy thả 404.html.twig vào thư mục templates/ của ứng dụng. Muốn một theme cung cấp bố cục mặc định mà các ứng dụng riêng lẻ có thể ghi đè? Điều đó cũng hoàn toàn khả thi.
Khám phá theme (theme discovery) đọc metadata từ composer.json. Bất kỳ gói nào có khóa waaseyaa.theme trong khối extra của nó đều là ứng viên theme:
{
"extra": {
"waaseyaa": {
"theme": {
"id": "my-theme",
"templates": "templates"
}
}
}
}
Không có registry theme, không có tệp cấu hình, không có bảng quản trị (admin panel). Composer đã biết những gì được cài đặt. Gói SSR chỉ cần đọc thông tin đó.
Gợi ý template: Tính cụ thể mà không phức tạp
Khi entity renderer xây dựng một túi biến, nó cũng tạo ra các gợi ý template (template suggestions), một danh sách có thứ tự của các tên tệp template từ cụ thể nhất đến chung nhất:
private function buildTemplateSuggestions(
string $entityTypeId,
string $bundle,
string $mode,
): array {
return [
"{$entityTypeId}.{$bundle}.{$mode}.html.twig", // node.article.teaser.html.twig
"{$entityTypeId}.{$bundle}.full.html.twig", // node.article.full.html.twig
"{$entityTypeId}.{$mode}.html.twig", // node.teaser.html.twig
"{$entityTypeId}.full.html.twig", // node.full.html.twig
"entity.html.twig", // catch-all
];
}
RenderController sẽ duyệt qua danh sách này và sử dụng template đầu tiên tồn tại. Tạo node.article.teaser.html.twig và nó sẽ render các đoạn giới thiệu (teasers) bài viết. Xóa nó đi và renderer sẽ rớt xuống kết quả khớp tiếp theo. Bạn chỉ tạo những template bạn cần.
Điều này không phải là gì
Đây không phải là PHP 4. Không có <?php echo $row['title'] ?> nằm trong một tệp đang chạy các truy vấn SQL. Lớp render tách biệt khỏi quyền truy cập dữ liệu, có cơ chế thoát ký tự phù hợp qua auto-escape của Twig, hỗ trợ quốc tế hóa (i18n) và xử lý bộ nhớ đệm (caching) với các khóa đại diện (surrogate keys) để vô hiệu hóa CDN.
Nhưng mô hình cơ bản vẫn giống như mô hình mà PHP đã sử dụng từ ngày đầu tiên: máy chủ nhận yêu cầu, tìm template phù hợp, điền dữ liệu vào và gửi HTML đến trình duyệt. Trình duyệt nhận một trang đã được render hoàn chỉnh và hiển thị nó. Không có gì để hydrate. Không có gì để xây dựng lại.
Hệ sinh thái JavaScript đã mất một thập kỷ để phát minh lại mô hình này và đặt cho nó một cái tên mới. Waaseyaa chỉ đơn giản là tiếp tục làm điều đó.
Tạm biệt và hẹn gặp lại!
Bài viết liên quan

Phần mềm
Anthropic ra mắt Claude Opus 4.7: Nâng cấp mạnh mẽ cho lập trình nhưng vẫn thua Mythos Preview
16 tháng 4, 2026

Công nghệ
Qwen3.6-35B-A3B: Quyền năng Lập trình Agentic, Nay Đã Mở Cửa Cho Tất Cả
16 tháng 4, 2026

Công nghệ
Spotify thắng kiện 322 triệu USD từ nhóm pirate Anna's Archive nhưng đối mặt với bài toán thu hồi
16 tháng 4, 2026
