Sắp xếp
Thuộc phần REST API trong hành trình Node.js của Coddy. Bài học 16 trên 25.
Client muốn xem các nhiệm vụ mới nhất trước tiên, hoặc danh sách theo thứ tự từ A đến Z. Một quy ước phổ biến là ?sort=field, thêm - phía trước để đảo ngược thứ tự:
// GET /api/books?sort=-year
const field = sort.startsWith('-') ? sort.slice(1) : sort; // 'year'
const direction = sort.startsWith('-') ? -1 : 1;
list = list.toSorted((a, b) => (a[field] - b[field]) * direction);Trước tiên, hãy kiểm tra field có nằm trong các trường bạn cho phép không, và trả về 400 nếu không, vì trường không có trong sách sẽ không giúp sắp xếp hữu ích. toSorted trả về một bản sao đã sắp xếp. sort sắp xếp lại chính mảng đó. Nếu không có bộ lọc nào tạo ra bản sao, mảng đó chính là kho dữ liệu: mọi yêu cầu sau đều thấy thứ tự mới. Ví dụ này dùng phép trừ, chỉ áp dụng được cho số. Để so sánh hai chuỗi, hãy dùng a.localeCompare(b): giống như phép trừ, cách này trả về một số âm, 0 hoặc một số dương. Ngày ISO là các chuỗi được sắp xếp theo thứ tự thời gian.
Thử thách
DễThay thế cách sắp xếp chỉ theo tiêu đề mà bạn đã viết trong phần Sắp xếp văn bản bằng cách xử lý trường và thứ tự bên dưới.
Ứng dụng khách muốn xem các tác vụ mới nhất trước, hoặc danh sách theo thứ tự từ A đến Z. Để danh sách tự sắp xếp bằng ?sort=.
- Trong
routes/tasks.js, ở tuyến danh sách, đọcsorttừreq.query. Nếu không có, danh sách giữ nguyên thứ tự đã lưu. ?sort=titlesắp xếp theo tiêu đề từ A đến Z.?sort=createdAtsắp xếp theo thời gian tạo, cũ nhất trước. Thêm-ở phía trước sẽ đảo ngược thứ tự:?sort=-titlelà từ Z đến A, còn?sort=-createdAtlà mới nhất trước.- Cả hai trường đều là văn bản.
createdAtlà ngày tháng theo định dạng ISO như"2026-03-03T10:30:00.000Z", nên có thể sắp xếp chính xác dưới dạng văn bản. Vì vậy, hãy so sánh bằnglocaleCompare, không dùng phép trừ. - Trường bất kỳ khác, chẳng hạn
?sort=color, sẽ trả về mã trạng thái 400:{ "error": { "code": "invalid_query", "message": "sort must be title or createdAt, with - in front to reverse" } } - Sắp xếp sau khi lọc, để
?done=false&sort=-titletrả về các tác vụ đang mở theo thứ tự từ Z đến A. - Sắp xếp một bản sao bằng
toSorted.sortsắp xếp lại chính mảng đã lưu, khiến mọi yêu cầu tiếp theo đều thấy thứ tự mới.
Thử nhé: GET /api/tasks?sort=title trả về các tác vụ 2, 3, 1 ("Book...", "Send...", "Write..."). GET /api/tasks?sort=-createdAt trả về 3, 2, 1. Sau đó, GET /api/tasks thông thường trả về 1, 2, 3 như cũ. GET /api/tasks?sort=color trả về mã 400 với mã lỗi invalid_query.
Tự làm thử
import { Router } from 'express';
import { z } from 'zod';
import { sendError, sendInvalid } from '../errors.js';
import { nextId, tasks } from '../store.js';
const router = Router();
// Một tác vụ đầy đủ như PUT gửi đến. .strict() từ chối các trường không có trong danh sách.
const Task = z
.object({
title: z.string().trim().min(1).max(100),
done: z.boolean(),
})
.strict();
const NewTask = Task.partial({ done: true });
const TaskChanges = Task.partial();
// True nếu một tác vụ khác đã có tiêu đề này, không phân biệt chữ hoa chữ thường.
const titleTaken = (title, exceptId) =>
tasks.some((t) => t.id !== exceptId && t.title.toLowerCase() === title.toLowerCase());
router.get('/', (req, res) => {
const { done, q } = req.query;
let list = tasks;
if (done !== undefined) {
if (done !== 'true' && done !== 'false') {
return sendError(res, 400, 'invalid_query', 'done must be true or false');
}
list = list.filter((t) => t.done === (done === 'true'));
}
if (q !== undefined) {
const words = String(q).toLowerCase();
list = list.filter((t) => t.title.toLowerCase().includes(words));
}
if (req.query.sort === 'title') {
list = list.toSorted((a, b) => a.title.localeCompare(b.title));
}
res.json(list);
});
router.get('/:id', (req, res) => {
const task = tasks.find((t) => t.id === Number(req.params.id));
if (!task) {
return sendError(res, 404, 'not_found', 'Task not found');
}
res.json(task);
});
router.post('/', (req, res) => {
const result = NewTask.safeParse(req.body);
if (!result.success) {
return sendInvalid(res, result.error);
}
const { title, done = false } = result.data;
if (titleTaken(title)) {
return sendError(res, 409, 'conflict', 'A task with this title already exists');
}
const task = { id: nextId(), title, done, createdAt: new Date().toISOString() };
tasks.push(task);
res.status(201).location(`/api/tasks/${task.id}`).json(task);
});
router.patch('/:id', (req, res) => {
const task = tasks.find((t) => t.id === Number(req.params.id));
if (!task) {
return sendError(res, 404, 'not_found', 'Task not found');
}
const result = TaskChanges.safeParse(req.body);
if (!result.success) {
return sendInvalid(res, result.error);
}
if (result.data.title !== undefined && titleTaken(result.data.title, task.id)) {
return sendError(res, 409, 'conflict', 'A task with this title already exists');
}
Object.assign(task, result.data);
res.json(task);
});
router.put('/:id', (req, res) => {
const index = tasks.findIndex((t) => t.id === Number(req.params.id));
if (index === -1) {
return sendError(res, 404, 'not_found', 'Task not found');
}
const result = Task.safeParse(req.body);
if (!result.success) {
return sendInvalid(res, result.error);
}
const { id, createdAt } = tasks[index];
if (titleTaken(result.data.title, id)) {
return sendError(res, 409, 'conflict', 'A task with this title already exists');
}
tasks[index] = { id, ...result.data, createdAt };
res.json(tasks[index]);
});
router.delete('/:id', (req, res) => {
const index = tasks.findIndex((t) => t.id === Number(req.params.id));
if (index === -1) {
return sendError(res, 404, 'not_found', 'Task not found');
}
tasks.splice(index, 1);
res.status(204).end();
});
export default router;
Tất cả bài học trong REST API
1Tài nguyên và động từ
Tài nguyên và URLTạo tài nguyên bằng POST4Trình duyệt, phiên bản và cấu hình
Origin là gìCho phép một origin khácCORSTrang gọi APITự luyện tập: Playground Express