SvelteKit là framework ứng dụng full-stack sắp tới cho Svelte, framework UI xây dựng ứng dụng tại thời điểm compile để tạo JavaScript nhỏ hơn, nhanh hơn. Mặc dù SvelteKit cho phép viết logic phía server dùng endpoints, việc bạn muốn lưu trữ dữ liệu ứng dụng như thế nào tùy thuộc vào bạn.
Trong bài viết này, tôi sẽ chỉ cách lưu trữ dữ liệu dùng Redis trong ứng dụng SvelteKit. Chúng ta sẽ dùng Redis để cache phản hồi API phim và hiển thị một phim ngẫu nhiên, dùng dữ liệu từ API The Movie Database (TMDB).
Bạn sẽ cần chuỗi kết nối Redis hoặc chạy Redis cục bộ để làm theo demo. Nếu chưa có instance Redis, tôi khuyến nghị Upstash. Giống như SvelteKit, nó được tối ưu cho ứng dụng serverless và miễn phí nếu bạn không làm nhiều yêu cầu (giới hạn 10k/ngày tại thời điểm viết). Dù bạn lấy instance Redis ở đâu, hãy chắc chắn nó đặt gần nơi ứng dụng được triển khai để giảm độ trễ.
Điều kiện tiên quyết
- Quen thuộc cơ bản với SvelteKit (ví dụ: pages vs endpoints, loading, truy xuất query và params cho trang cho trước)
- Khóa API TMDB và instance Redis (ví dụ: trên Upstash) nếu muốn chạy hoặc triển khai demo
Tổng quan Starter
Clone starter repo. Nhánh main có kết quả cuối, nên checkout nhánh initial để làm theo bài viết. Nếu muốn push thay đổi, fork repo trước.
git clone https://github.com/geoffrich/movie-search-redis.git
cd movie-search-redis
git checkout initial
Đây là ứng dụng SvelteKit nhỏ cho phép tìm kiếm phim và xem chi tiết dùng API TMDB. Nó dùng TypeScript để tương tác với phản hồi API dễ dàng hơn, nhưng không bắt buộc để dùng Redis hoặc SvelteKit. Các route sau đã tồn tại (tương ứng với file trong src/routes):
/render trang chủ/searchrender danh sách kết quả tìm kiếm. Nhậnqueryvàpagelàm query params, ví dụ?query=star wars&page=3hiển thị trang 3 kết quả cho "Star Wars"/search.jsonlà server endpoint truy vấn API TMDB và trả về danh sách kết quả. Nhận cùng query parameters như/search/movie/[id]render chi tiết phim với ID cho trước, ví dụ/movie/11/movie/[id].jsonlà server endpoint trả về chi tiết phim từ API TMDB. Vì phản hồi TMDB trả về nhiều dữ liệu hơn chúng ta dùng trên trang, chúng ta điều chỉnh phản hồi để trả về tập hợp con dữ liệu.
Bạn có thể xem demo chạy trên Netlify.
Lưu ý các cuộc gọi API TMDB chỉ xảy ra ở server endpoints. Điều này để khóa API không bị lộ ra client.
Để chạy demo cục bộ, tạo file .env ở thư mục gốc dự án và thêm khóa API TMDB và chuỗi kết nối Redis (mẫu dưới). Sau đó chạy npm install cài dependency và npm run dev chạy app.
TMDB_API_KEY=KEY_GOES_HERE
REDIS_CONNECTION=CONNECTION_GOES_HERE
Tại runtime, truy cập các giá trị này dùng process.env['TMDB_API_KEY'] hoặc process.env['REDIS_CONNECTION']. Giá trị đọc từ file .env dùng dotenv trong hooks.ts.
Cache Phản hồi API trong Redis
Một cải tiến cho dự án hiện tại là cache phản hồi API cho một phim riêng lẻ trong Redis. Hiện tại, app gọi API TMDB mỗi khi trang phim tải. Khi đầu tiên yêu cầu, lưu phản hồi API trong Redis để lần sau lấy bản cache thay vì gọi API.
Mặc dù API TMDB nhanh và không nhất thiết phải cache, kỹ thuật này hữu ích cho API phản hồi chậm hoặc có giới hạn yêu cầu. Nó cũng cung cấp mức độ phục hồi (resiliency) nếu API bị sập.
Chúng ta đã thêm ioredis làm dependency. Đây là Redis client cho Node giúp tương tác với cơ sở dữ liệu Redis.
Tạo file src/lib/redis.ts. File này khởi tạo Redis client và export để các hàm khác dùng. Nó cũng chứa hàm helper lấy key. Thêm mã dưới vào file:
import Redis from "ioredis";
const connectionString = process.env["REDIS_CONNECTION"];
export const MOVIE_IDS_KEY = "movie_ids";
/** Return the key used to store movie details for a given ID in Redis */
export function getMovieKey(id): string {
return `movie:${id}`;
}
export default connectionString ? new Redis(connectionString) : new Redis();
Lưu ý triển khai này tạo một Redis client đơn cho mỗi instance serverless. Tùy chọn khác là tạo và đóng kết nối Redis mỗi request. Upstash cũng có REST API không yêu cầu khởi tạo kết nối Redis. Tùy chọn tốt nhất phụ thuộc vào yêu cầu độ trễ và bộ nhớ của app.
Đi đến file /src/routes/movie/[id].json.ts. Import Redis client và hàm getMovieKey từ redis.ts.
import redis, { getMovieKey } from "$lib/redis";
Xem hàm getMovieDetailsFromApi. Nó gọi API TMDB lấy chi tiết phim và credits rồi trả về. Trước khi trả dữ liệu, muốn lưu vào cache Redis để lần sau lấy bản cache thay vì gọi API. Thêm hàm cacheMovieResponse mới vào file để cache dữ liệu trong Redis.
async function cacheMovieResponse(id: number, movie, credits) {
try {
const cache: MovieDetails = {
movie,
credits,
};
// store movie response for 24 hours
await redis.set(getMovieKey(id), JSON.stringify(cache), "EX", 24 * 60 * 60);
} catch (e) {
console.log("Unable to cache", id, e);
}
}
Mỗi ID phim sẽ lưu dưới khóa khác nhau. Ví dụ, nếu lấy thông tin phim ID 11, khóa sẽ là movie:11. Hai đối số cuối của redis.set nói rằng chỉ cache dữ liệu 24 giờ (86400 giây). Chúng ta không nên cache mãi mãi - vi phạm điều khoản sử dụng, và dữ liệu cuối cùng sẽ lỗi thời.
Lưu ý cần stringify đối tượng JS trước khi lưu vào cache. Chúng ta cũng bắt mọi exception ném ra trong thao tác này để endpoint có khả năng chịu lỗi hơn. Nếu không cache được dữ liệu, vẫn nên trả dữ liệu từ API thay vì ném exception không xử lý.
Bây giờ dùng hàm cacheMovieResponse mới trong getMovieDetailsFromApi để lưu phản hồi API vào cache.
async function getMovieDetailsFromApi(id: number) {
const [movieResponse, creditsResponse] = await Promise.all([
getMovieDetails(id),
getCredits(id),
]);
if (movieResponse.ok) {
const movie = await movieResponse.json();
const credits = await creditsResponse.json();
// add this line
await cacheMovieResponse(id, movie, credits);
return {
movie,
credits,
};
}
return {
status: movieResponse.status,
};
}
Bây giờ đã lưu dữ liệu trong cache, nhưng vẫn cần lấy dữ liệu đã cache. Hãy thêm hàm đọc chi tiết phim từ cache.
async function getMovieDetailsFromCache(
id: number
): Promise> {
try {
const cached: string = await redis.get(getMovieKey(id));
if (cached) {
const parsed: MovieDetails = JSON.parse(cached);
console.log(`Found ${id} in cache`);
return parsed;
}
} catch (e) {
console.log("Unable to retrieve from cache", id, e);
}
return {};
}
Vì dữ liệu lưu dưới dạng chuỗi, cần parse thành đối tượng để dùng được. Tương tự hàm trước, log exception nhưng không cho chúng thoát khỏi hàm cache. Chúng ta luôn có thể fallback lấy dữ liệu từ API.
Cuối cùng, gọi hàm cache trong handler request chính. Nếu tìm thấy dữ liệu trong cache, trả về ngay; ngược lại, đọc từ API như trước.
export const get: RequestHandler = async function ({ params }) {
const { id: rawId } = params;
// validate and sanitize the input
const id = parseInt(rawId);
if (isNaN(id)) {
return {
status: 400,
};
}
// add these lines
const { movie, credits } = await getMovieDetailsFromCache(id);
if (movie && credits) {
return {
body: adaptResponse(movie, credits),
};
}
// fallback to the API
const result = await getMovieDetailsFromApi(id);
Bạn có thể xem mã cuối cùng cho endpoint này trong demo repo.
Với những thay đổi này, hãy thử điều hướng đến trang phim. Khi refresh trang, bạn sẽ thấy "Found id in cache" trong console log, cho biết đã lưu và lấy thành công phim đó từ cache.
Lấy Phim Ngẫu nhiên
Redis làm được nhiều hơn chỉ cache phản hồi API. Hãy xem cách xây dựng route chuyển hướng người dùng đến phim ngẫu nhiên.
Điều này không đơn giản như chọn số ngẫu nhiên giữa 1 và 300000 làm movie ID. Không phải mọi số trong phạm vi đó tương ứng với phim - ví dụ, không có phim ID 1 hay 1000. Việc theo dõi ID tối đa có thể cũng phức tạp, vì luôn thay đổi khi phim mới được thêm. Thay vào đó, chọn phim ngẫu nhiên bằng quy trình 2 bước:
- Khi thực hiện truy vấn tìm kiếm, đặt tất cả ID trả về vào Redis Set.
- Khi route
/movie/randomđược yêu cầu, lấy một thành viên ngẫu nhiên của set đó và chuyển hướng đến trang chi tiết phim tương ứng.
Các phim ngẫu nhiên có thể trả về sẽ bắt đầu nhỏ, nhưng tăng dần khi nhiều tìm kiếm hơn được thực hiện.
Để điền random set, cập nhật /src/routes/search.json.ts như sau:
import type { RequestHandler } from "@sveltejs/kit";
import type { SearchResponse } from "$lib/types/tmdb";
import redis, { MOVIE_IDS_KEY } from "$lib/redis";
const VOTE_THRESHOLD = 20;
export const get: RequestHandler = async function ({ query }) {
const searchQuery = query.get("query");
const page = query.get("page") ?? 1;
const response = await fetch(
`https://api.themoviedb.org/3/search/movie?api_key=${process.env["TMDB_API_KEY"]}&page=${page}&include_adult=false&query=${searchQuery}`
);
const parsed: SearchResponse = await response.json();
// add these lines
const filteredMovies = parsed.results.filter(
(movie) => movie.vote_count >= VOTE_THRESHOLD
);
if (filteredMovies.length > 0) {
try {
await redis.sadd(MOVIE_IDS_KEY, ...filteredMovies.map((r) => r.id));
} catch (e) {
console.log(e);
}
}
return {
body: parsed,
};
};
Lưu ý chúng ta không thêm mọi phim trả về vào set. Tôi chọn lọc bỏ phim không có nhiều vote, vì những phim đó ít được người dùng kiểm duyệt. Bạn có thể điều chỉnh VOTE_THRESHOLD theo ý thích.
Với thay đổi này, tìm kiếm phim sẽ bắt đầu điền set ID phim. Thực hiện một vài tìm kiếm để thêm ID vào set. Ví dụ:
- Star Wars
- Lion King
- Spider Man
Sau vài lần tìm kiếm, bạn sẽ có set ID ngẫu nhiên lưu trong Redis. Hãy tạo route cho endpoint và trang /movie/random.
src/routes/movie/random.json.ts
import type { RequestHandler } from "@sveltejs/kit";
import redis, { MOVIE_IDS_KEY } from "$lib/redis";
export const get: RequestHandler = async function () {
const randomId = await redis.srandmember(MOVIE_IDS_KEY);
return {
body: randomId,
};
};
Server endpoint này dùng SRANDMEMBER chọn ID ngẫu nhiên từ set ID phim.
src/routes/movie/random.svelte
Trang Svelte tương ứng chỉ cần hàm load vì không render UI. Nó gọi server endpoint vừa thêm và chuyển hướng đến trang phim tương ứng.
Đó là tất cả! Bây giờ, điều hướng đến https://localhost:3000/movie/random. Bạn sẽ tự động chuyển hướng đến phim ngẫu nhiên từ các tìm kiếm trước. Để truy cập route dễ dàng hơn, thêm vào navigation trong /src/routes/__layout.svelte:
Bạn có thể thấy hoạt động trong live demo.
Tổng kết
Còn nhiều cách khác dùng Redis, nhưng hy vọng bài viết này cho bạn hiểu tốt cơ bản về tích hợp Redis trong app SvelteKit. Bạn có thể xem mã cuối cùng trên GitHub và demo trực tiếp trên Netlify.
Có câu hỏi? Liên hệ trên Twitter. Bạn cũng có thể tìm các bài viết khác về Svelte trên blog của tôi.