Bài 24.2: Setting up SSR với Express

Chào mừng các bạn quay trở lại với chuỗi bài viết về Lập trình Web Front-end!

Trong những bài trước, chúng ta đã cùng tìm hiểu về các nền tảng cơ bản như HTML, CSS, JavaScript, và cả những framework/thư viện mạnh mẽ như React hay Next.js. Next.js là một ví dụ điển hình của việc tích hợp SSR (Server-Side Rendering), một kỹ thuật cực kỳ quan trọng trong thế giới web hiện đại. Nhưng tại sao SSR lại quan trọng, và làm thế nào chúng ta có thể tự tay xây dựng một hệ thống SSR cơ bản mà không phụ thuộc hoàn toàn vào các framework opinionated?

Bài viết này sẽ đưa chúng ta đi sâu vào việc Setting up SSR với Express – một framework Node.js đơn giản, linh hoạt và cực kỳ phổ biến. Chúng ta sẽ hiểu cách thức SSR hoạt động ở mức cơ bản nhất và tự mình dựng lên một ứng dụng web có khả năng render HTML ngay trên server.

Tại sao lại là SSR?

Trong mô hình CSR (Client-Side Rendering) phổ biến với các SPA (Single Page Application) hiện đại, trình duyệt sẽ tải một file HTML "rỗng" hoặc rất ít nội dung, sau đó sử dụng JavaScript để lấy dữ liệu và "xây dựng" giao diện trang web ngay tại máy người dùng. Điều này mang lại trải nghiệm tương tác mượt mà sau lần tải đầu tiên, nhưng lại gặp phải hai vấn đề lớn:

  1. SEO (Search Engine Optimization): Các bot của Search Engine gặp khó khăn hoặc tốn nhiều thời gian hơn để crawl và index nội dung được tạo ra hoàn toàn bằng JavaScript trên client. Điều này ảnh hưởng tiêu cực đến thứ hạng tìm kiếm của bạn.
  2. Performance ban đầu: Người dùng phải chờ JavaScript được tải xuống, parse, và thực thi để thấy nội dung trang web. Với kết nối mạng chậm hoặc thiết bị yếu, điều này có thể gây ra trải nghiệm "trang trắng" khó chịu.

SSR giải quyết vấn đề này bằng cách "tiền xử lý" – tạo ra file HTML hoàn chỉnh ngay trên server dựa trên dữ liệu cần thiết, và gửi file HTML đó về cho trình duyệt.

  • Trình duyệt nhận được HTML đã có đầy đủ nội dung, hiển thị ngay lập tức (Fast First Contentful Paint).
  • Search Engine bot dễ dàng đọc hiểu và index nội dung trang web.

Sau khi HTML được hiển thị, JavaScript sẽ được tải xuống và "hydrate" (gắn kết các sự kiện, làm cho trang web có tính tương tác) lên cấu trúc HTML đã có.

Express, với tính linh hoạt của mình, là một nền tảng tuyệt vời để học cách triển khai SSR một cách thủ công, giúp chúng ta hiểu rõ hơn về quá trình này.

Chuẩn bị: Express và View Engine

Express tự nó không biết cách "render" file HTML từ các template. Nó chỉ đơn thuần xử lý request và gửi response. Để thực hiện SSR, chúng ta cần một "View Engine" (còn gọi là templating engine).

View Engine là một thư viện giúp Express xử lý các tệp mẫu (template files), thay thế các biến động hoặc logic điều khiển (như vòng lặp, câu điều kiện) bằng dữ liệu thực tế, và cuối cùng trả về một chuỗi HTML hoàn chỉnh.

Có rất nhiều View Engine phổ biến cho Node.js và Express như:

  • EJS (Embedded JavaScript): Đơn giản, cho phép nhúng mã JavaScript thuần vào template HTML. Rất dễ học.
  • Pug (trước đây là Jade): Sử dụng cú pháp dựa trên thụt lề, gọn gàng hơn HTML truyền thống nhưng cần làm quen.
  • Handlebars: Sử dụng cú pháp logic mustache {{...}}, tách biệt khá rõ ràng logic và template.

Trong bài viết này, chúng ta sẽ sử dụng EJS vì sự đơn giản và quen thuộc với cú pháp giống HTML.

Bắt tay vào Code: Thiết lập SSR với Express và EJS

Hãy cùng đi từng bước để xây dựng ứng dụng SSR đầu tiên của chúng ta.

Bước 1: Khởi tạo Project và Cài đặt Dependencies

Đầu tiên, tạo một thư mục cho project và mở terminal trong thư mục đó. Chạy lệnh npm init để tạo file package.json. Bạn có thể dùng cờ -y để bỏ qua các câu hỏi tương tác và sử dụng giá trị mặc định.

mkdir express-ssr-app
cd express-ssr-app
npm init -y

Tiếp theo, cài đặt Express và EJS:

npm install express ejs

Lệnh này sẽ thêm expressejs vào mục dependencies trong file package.json của bạn và tải chúng về thư mục node_modules.

Bước 2: Cấu hình Express Cơ bản

Tạo một file mới, ví dụ server.js, và viết code khởi tạo Express:

// server.js
const express = require('express');
const app = express();
const port = 3000;

// Các cấu hình khác sẽ thêm vào đây

// Khởi động server
app.listen(port, () => {
  console.log(`Server đang chạy tại http://localhost:${port}`);
});

Đoạn code này tạo ra một ứng dụng Express cơ bản và lắng nghe kết nối trên cổng 3000. Nếu chạy file này (node server.js), bạn sẽ thấy thông báo "Server đang chạy..." trong console, nhưng chưa có route nào được xử lý cả.

Bước 3: Thiết lập View Engine (EJS)

Để Express biết cách sử dụng EJS và tìm các file template ở đâu, chúng ta cần thêm hai dòng cấu hình vào file server.js:

// server.js
const express = require('express');
const app = express();
const port = 3000;

// --- Thêm đoạn này ---
app.set('view engine', 'ejs'); // Đặt EJS làm view engine mặc định
app.set('views', './views');     // Thư mục chứa các file .ejs template
// ---------------------

// Khởi động server
app.listen(port, () => {
  console.log(`Server đang chạy tại http://localhost:${port}`);
});
  • app.set('view engine', 'ejs');: Nói với Express rằng khi chúng ta muốn render một "view", hãy sử dụng engine có tên là ejs. Thư viện ejs mà chúng ta đã cài đặt sẽ xử lý việc này.
  • app.set('views', './views');: Chỉ định thư mục mà Express sẽ tìm kiếm các file template (.ejs). Theo quy ước, thư mục này thường được đặt tên là views và nằm ở cùng cấp với file server chính. Hãy tạo thư mục này ngay bây giờ: mkdir views.
Bước 4: Tạo View File Đầu tiên

Bây giờ, tạo một file template đơn giản trong thư mục views vừa tạo. Đặt tên là index.ejs:

<!-- views/index.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ứng dụng SSR Cơ bản</title>
    <style>
        body { font-family: sans-serif; margin: 20px; }
        h1 { color: navy; }
    </style>
</head>
<body>
    <h1>Chào mừng đến với SSR cùng Express!</h1>
    <p>Đây là nội dung tĩnh từ template EJS.</p>
</body>
</html>

Đây chỉ là một file HTML thông thường. Sức mạnh của EJS sẽ được thể hiện ở bước tiếp theo.

Bước 5: Xây dựng Route và Render View

Trở lại file server.js. Bây giờ chúng ta cần định nghĩa một route (ví dụ: trang chủ "/") để khi người dùng truy cập, server sẽ render file index.ejs và trả về HTML hoàn chỉnh.

// server.js
const express = require('express');
const app = express();
const port = 3000;

app.set('view engine', 'ejs');
app.set('views', './views');

// --- Thêm đoạn này ---
// Định nghĩa route cho trang chủ
app.get('/', (req, res) => {
  // Express sẽ tìm file index.ejs trong thư mục 'views'
  // và render nó.
  res.render('index');
});
// ---------------------

// Khởi động server
app.listen(port, () => {
  console.log(`Server đang chạy tại http://localhost:${port}`);
});
  • app.get('/', ...): Đây là cách định nghĩa một route xử lý yêu cầu GET đến đường dẫn gốc (/).
  • (req, res) => { ... }: Đây là hàm xử lý (handler) cho route đó, nhận đối tượng request (req) và response (res).
  • res.render('index');: Đây là điều kỳ diệu của SSR! Thay vì gửi một chuỗi text (res.send('...')) hay file tĩnh (res.sendFile('...')), chúng ta gọi res.render(). Express sẽ dùng View Engine (mà chúng ta đã set là EJS) để xử lý file index.ejs và gửi kết quả HTML cuối cùng về cho trình duyệt.

Bây giờ, chạy lại server: node server.js. Mở trình duyệt và truy cập http://localhost:3000. Bạn sẽ thấy nội dung của file index.ejs được hiển thị. Kiểm tra mã nguồn trang (View Page Source) trong trình duyệt, bạn sẽ thấy toàn bộ cấu trúc HTML đã có sẵn, không phải là file HTML "rỗng" như khi dùng CSR thuần túy.

Bước 6: Truyền Dữ liệu Động vào View

Ứng dụng web thực tế thường hiển thị dữ liệu động. Sức mạnh của View Engine là khả năng chèn dữ liệu từ server vào template.

Sửa route trong server.js:

// server.js (chỉ thay đổi route handler)
// ... (các phần khác giữ nguyên)

// Định nghĩa route cho trang chủ
app.get('/', (req, res) => {
  // Dữ liệu động mà bạn muốn truyền vào template
  const data = {
    pageTitle: 'Trang chủ SSR Động',
    greeting: 'Xin chào từ Server!',
    items: ['Item 1', 'Item 2', 'Item 3']
  };

  // Truyền dữ liệu vào hàm render dưới dạng một object
  res.render('index', data);
});

// ... (phần listen server)

Bây giờ, sửa file views/index.ejs để sử dụng dữ liệu này:

<!-- views/index.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%= pageTitle %></title> <!-- Sử dụng biến pageTitle -->
    <style>
        body { font-family: sans-serif; margin: 20px; }
        h1 { color: navy; }
        ul { list-style-type: disc; margin-left: 20px; }
    </style>
</head>
<body>
    <h1><%= greeting %></h1> <!-- Sử dụng biến greeting -->
    <p>Nội dung động:</p>
    <ul>
        <% items.forEach(function(item) { %> <!-- Vòng lặp qua mảng items -->
            <li><%= item %></li> <!-- Hiển thị từng item -->
        <% }); %>
    </ul>
</body>
</html>
  • Cú pháp <%= variable %> trong EJS được sử dụng để hiển thị giá trị của một biến ra HTML. EJS sẽ tự động escape các ký tự đặc biệt để ngăn chặn XSS.
  • Cú pháp <% code %> được sử dụng để thực thi mã JavaScript bên trong template, ví dụ như vòng lặp forEach. Lưu ý rằng code trong <% ... %> không hiển thị trực tiếp ra HTML mà chỉ thực thi logic.

Chạy lại server (node server.js) và refresh trang http://localhost:3000. Bạn sẽ thấy nội dung động từ object data đã được hiển thị trên trang.

Quan sát mã nguồn trang, bạn sẽ thấy toàn bộ HTML, bao gồm cả danh sách các items, đều đã được server render sẵn. Đây chính là Server-Side Rendering ở dạng cơ bản nhất!

Comments

There are no comments at the moment.