Bài 24.3: Data fetching trong SSR

Bài 24.3: Data fetching trong SSR
Chào mừng bạn quay trở lại với hành trình khám phá lập trình web! Trong các bài trước, chúng ta đã tìm hiểu về Server-Side Rendering (SSR) và những lợi ích tuyệt vời mà nó mang lại như cải thiện SEO và tăng tốc độ tải trang ban đầu. Tuy nhiên, một trang web thực tế hiếm khi chỉ hiển thị nội dung tĩnh. Hầu hết đều cần lấy dữ liệu động từ API hoặc database để hiển thị thông tin mới nhất.
Vậy, khi chúng ta thực hiện render trên server, quá trình lấy dữ liệu này diễn ra như thế nào? Đây chính là lúc "Data fetching trong SSR" trở thành một chủ đề cực kỳ quan trọng mà mọi lập trình viên muốn làm chủ SSR cần phải nắm vững.
Hãy tưởng tượng bạn đang xây dựng một trang web thương mại điện tử. Trang chi tiết sản phẩm cần hiển thị tên, mô tả, giá, và đánh giá của sản phẩm đó. Nếu bạn chỉ render khung sườn HTML trên server và chờ JavaScript ở client chạy rồi mới đi fetch dữ liệu sản phẩm, điều gì sẽ xảy ra?
- Người dùng nhận được HTML ban đầu không có dữ liệu sản phẩm.
- Trình duyệt tải JavaScript.
- JavaScript chạy, xác định cần lấy dữ liệu sản phẩm X.
- JavaScript gửi yêu cầu fetch dữ liệu đến API.
- API trả về dữ liệu.
- JavaScript nhận dữ liệu và cập nhật DOM để hiển thị thông tin sản phẩm.
Quá trình này tốn thời gian và trong khoảng thời gian từ bước 1 đến bước 6, người dùng chỉ thấy một trang trống hoặc một trạng thái loading. Quan trọng hơn, các search engine crawler khi truy cập trang có thể chỉ thấy HTML ban đầu không có dữ liệu, gây ảnh hưởng nghiêm trọng đến SEO.
Đây chính là lý do tại sao chúng ta cần fetch dữ liệu ngay trên server trong quá trình SSR.
Tại Sao Phải Fetch Data Trên Server?
Fetch dữ liệu trên server mang lại những ưu điểm cốt lõi:
- SEO Mạnh Mẽ Hơn: Khi server render trang, nó đã fetch dữ liệu và nhúng nó vào HTML cuối cùng. Search engine crawler nhận được toàn bộ nội dung trang web ngay lập tức, bao gồm cả dữ liệu động. Điều này giúp index chính xác và đầy đủ nội dung của bạn.
- Hiệu Suất Tải Trang Ban Đầu Tốt Hơn: Người dùng nhận được HTML đã có sẵn dữ liệu. Trang web hiển thị nội dung gần như ngay lập tức (hoặc nhanh hơn đáng kể) so với việc phải chờ client fetch data. Trải nghiệm người dùng (UX) được cải thiện rõ rệt.
- Giảm Phụ Thuộc Vào JavaScript Phía Client Cho Nội Dung Ban Đầu: Nội dung chính đã có sẵn trong HTML. JavaScript ở client chỉ cần "hydrate" (kích hoạt các sự kiện và tương tác) lên cấu trúc HTML đã có.
Data Fetching Trong Các Framework SSR Hiện Đại
Các framework hỗ trợ SSR như Next.js cung cấp các cơ chế đặc biệt để bạn có thể định nghĩa các hàm chạy trên server để fetch dữ liệu trước khi trang được render thành HTML. Đây là "bí quyết" đằng sau việc có dữ liệu sẵn sàng khi render.
Chúng ta sẽ lấy Next.js làm ví dụ điển hình, vì nó là framework React phổ biến cho SSR và nằm trong chuỗi bài học của chúng ta. Next.js cung cấp một vài cách để fetch dữ liệu, nhưng trong ngữ cảnh SSR (render trên mỗi request), hàm được sử dụng phổ biến nhất là getServerSideProps
.
getServerSideProps
Đây là hàm "ngôi sao" cho việc fetch dữ liệu trên server cho các trang có nội dung thay đổi thường xuyên hoặc phụ thuộc vào request cụ thể (ví dụ: dữ liệu người dùng đăng nhập, kết quả tìm kiếm theo query param).
- Nó là một hàm
async
được export từ một file trong thư mụcpages
. - Hàm này chạy trên server trên mỗi request đến trang đó.
- Dữ liệu fetch được trả về thông qua thuộc tính
props
và sẽ được truyền làm props cho component page tương ứng.
Hãy xem một ví dụ đơn giản: Fetch danh sách bài viết từ một API.
// pages/posts.js
import React from 'react';
function Posts({ posts }) {
// Component này nhận 'posts' làm props từ getServerSideProps
return (
<div>
<h1>Danh sách Bài viết</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
// Hàm này chạy trên server trên mỗi request đến trang /posts
export async function getServerSideProps() {
// Thực hiện fetch dữ liệu từ API
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=10');
const posts = await res.json(); // Chuyển đổi response sang JSON
// Trả về object chứa props sẽ truyền cho component Posts
return {
props: {
posts, // dữ liệu posts sẽ có sẵn trong component Posts
},
};
}
export default Posts;
Giải thích Code:
function Posts({ posts })
: Đây là React component cho trang/posts
. Nó mong đợi nhận một prop tên làposts
.export async function getServerSideProps()
: Đây là phần "phép thuật" của SSR data fetching trong Next.js.export
: Bắt buộc phải export hàm này để Next.js nhận diện và chạy nó.async
: Việc fetch dữ liệu là bất đồng bộ, nên hàm này cần làasync
.getServerSideProps()
: Tên hàm cố định. Next.js sẽ tìm và chạy hàm này trên server.
const res = await fetch(...)
: Chúng ta sử dụngfetch
(hoạt động trên Node.js ở phía server) để gọi đến một API bên ngoài. Từ khóaawait
là cần thiết vìfetch
trả về một Promise.const posts = await res.json()
: Đọc response từ API và chuyển đổi nó thành đối tượng JavaScript (ở đây là một mảng các bài viết). Lại cầnawait
vì.json()
cũng trả về Promise.return { props: { posts } }
: Đây là cấu trúc trả về bắt buộc. Object phải có keyprops
, và giá trị của nó là một object chứa dữ liệu bạn muốn truyền cho componentPosts
.- Bây giờ, trong component
Posts
, biếnposts
đã chứa dữ liệu được fetch trên server và sẵn sàng để render.
Khi người dùng yêu cầu trang /posts
, Next.js server sẽ chạy getServerSideProps
trước. Sau khi fetch dữ liệu xong, nó sẽ render component Posts
với dữ liệu đó thành HTML và gửi HTML đầy đủ về trình duyệt. Người dùng sẽ thấy danh sách bài viết ngay lập tức.
Sử dụng Context trong getServerSideProps
getServerSideProps
cũng nhận một tham số gọi là context
. Object context
này chứa thông tin về request đang được xử lý, rất hữu ích cho việc fetch dữ liệu phụ thuộc vào request, như query parameters, cookies, headers, v.v.
// pages/products/[id].js (Ví dụ trang chi tiết sản phẩm)
import React from 'react';
function ProductDetail({ product }) {
if (!product) {
return <p>Sản phẩm không tìm thấy.</p>;
}
return (
<div>
<h1>{product.name}</h1>
<p>Giá: ${product.price}</p>
<p>{product.description}</p>
</div>
);
}
// Hàm này chạy trên server trên mỗi request đến trang chi tiết sản phẩm
export async function getServerSideProps(context) {
// Lấy ID sản phẩm từ URL (ví dụ: /products/123 -> id = '123')
const { id } = context.params; // 'params' chứa các tham số động trong URL
try {
// Fetch dữ liệu sản phẩm dựa trên ID
const res = await fetch(`https://api.example.com/products/${id}`);
if (!res.ok) { // Kiểm tra mã trạng thái HTTP (ví dụ: 404 Not Found)
return { notFound: true }; // Trả về trang 404 tích hợp của Next.js
}
const product = await res.json();
// Trả về props cho component ProductDetail
return {
props: {
product,
},
};
} catch (error) {
console.error("Lỗi khi fetch sản phẩm:", error);
// Xử lý lỗi: có thể redirect đến trang lỗi hoặc trả về props rỗng
return {
props: {
product: null, // Truyền null để component hiển thị trạng thái không tìm thấy
}
};
}
}
export default ProductDetail;
Giải thích Code:
pages/products/[id].js
: Đây là một trang dynamic route trong Next.js.[id]
là tham số sẽ thay đổi dựa trên URL.getServerSideProps(context)
: Hàm nhận thêm đối sốcontext
.const { id } = context.params;
: Objectcontext
có thuộc tínhparams
chứa các giá trị từ dynamic route segments. Ở đây, chúng ta lấy giá trị của[id]
.await fetch(.../${id})
: Sử dụng ID lấy được để tạo URL API cụ thể cho sản phẩm đó.if (!res.ok) { return { notFound: true }; }
: Một cách xử lý khi API trả về lỗi (ví dụ: sản phẩm không tồn tại).notFound: true
là một cách Next.js xử lý trang 404.try...catch
: Bao quanh logic fetch để bắt các lỗi mạng hoặc lỗi parse JSON. Trong trường hợp lỗi, chúng ta có thể trả vềproduct: null
để component xử lý hiển thị thông báo hoặc trang lỗi.- Component
ProductDetail
nhậnproduct
làm props và kiểm tra xem nó có tồn tại không trước khi hiển thị chi tiết.
Khi Nào Nên Dùng getServerSideProps
?
Dùng getServerSideProps
khi:
- Trang cần hiển thị dữ liệu luôn được cập nhật trên mỗi request (ví dụ: giá cổ phiếu theo thời gian thực, giỏ hàng của người dùng).
- Nội dung trang phụ thuộc vào thông tin của request (ví dụ: cookies, headers, query parameters từ client).
- Bạn cần bảo mật dữ liệu nhạy cảm bằng cách fetch nó ở server mà không bao gồm khóa API hoặc logic phức tạp ở client.
Lưu ý quan trọng: getServerSideProps
chỉ chạy ở server. Code viết trong hàm này sẽ không bao giờ chạy ở trình duyệt. Điều này có nghĩa là bạn có thể viết code Node.js ở đây, truy cập hệ thống file, hoặc sử dụng các biến môi trường nhạy cảm mà không lo chúng bị lộ ra client.
So Sánh Ngắn: SSR Fetching vs. Client-Side Fetching
SSR Fetching (
getServerSideProps
):- Khi chạy: Trên server, trước khi trang được render.
- Kết quả: HTML được trả về trình duyệt đã có dữ liệu.
- Ưu điểm: Tốt cho SEO, hiệu suất tải trang ban đầu cao, dữ liệu nhạy cảm được giữ kín.
- Nhược điểm: Tốc độ render phụ thuộc vào tốc độ fetch API; mỗi request đều phải chờ fetch.
Client-Side Fetching (useEffect, SWR, React Query):
- Khi chạy: Trên trình duyệt, sau khi component đã mount.
- Kết quả: HTML ban đầu không có dữ liệu; dữ liệu được điền vào sau khi fetch xong.
- Ưu điểm: Phù hợp cho nội dung không cần SEO, dữ liệu ít quan trọng hoặc hiển thị sau khi trang đã tương tác; trang ban đầu tải nhanh hơn (nhưng chưa có nội dung động).
- Nhược điểm: Kém cho SEO, tạo trạng thái loading ban đầu, có thể lộ thông tin API ở client.
Trong thực tế, một ứng dụng SSR hiện đại thường kết hợp cả hai phương pháp: dùng SSR fetching cho nội dung chính cần SEO và tốc độ tải ban đầu, và client-side fetching cho các phần động, tương tác hoặc dữ liệu cập nhật sau khi trang đã tải xong.
Những Lưu Ý Khi Fetch Data Trên Server
- Thời gian phản hồi (Latency): Thời gian fetch data trực tiếp ảnh hưởng đến thời gian phản hồi của server. API chậm sẽ làm trang web tải chậm. Cần tối ưu hóa API hoặc cân nhắc caching.
- Xử lý lỗi: Luôn luôn xử lý các trường hợp API bị lỗi hoặc trả về dữ liệu không mong muốn. Bạn cần có chiến lược hiển thị lỗi cho người dùng hoặc trả về trang 404/trang lỗi.
- Bảo mật: Dữ liệu nhạy cảm nên được fetch trên server. Tránh đưa các khóa API bí mật ra code client.
- Kích thước Props: Dữ liệu trả về từ
getServerSideProps
sẽ được serialize và gửi đến client như là một phần của HTML (hoặc thông qua state management). Dữ liệu quá lớn có thể làm tăng kích thước HTML và ảnh hưởng đến hiệu suất.
Data fetching trong SSR là một kỹ thuật mạnh mẽ giúp bạn xây dựng các ứng dụng web có hiệu suất cao và thân thiện với search engine. Bằng cách tận dụng các hàm như getServerSideProps
của Next.js, bạn có thể đảm bảo rằng nội dung động của mình sẵn sàng ngay từ lần render đầu tiên trên server, mang lại trải nghiệm tốt nhất cho cả người dùng và bot tìm kiếm.
Hãy luyện tập và thử nghiệm với các ví dụ này để nắm vững cách lấy dữ liệu hiệu quả trong môi trường SSR!
Comments