تقسيم الصفحات
جزء من قسم واجهات REST API في رحلة Node.js على Coddy. الدرس 12 من 18.
قائمة تضم عشرة آلاف مهمة طويلة جدًا بحيث لا يمكن إرسالها في إجابة واحدة. أرسلها على صفحات: يطلب العميل ?page=2&limit=20 ويحصل على هذا الجزء، بالإضافة إلى ما يحتاج إليه لعرض «الصفحة 2 من 5»:
{ "items": [ ...books 21 to 40... ], "page": 2, "total": 95, "pages": 5 }const start = (page - 1) * limit; // تبدأ الصفحة 1 عند 0
const items = list.slice(start, start + limit);
const pages = Math.ceil(list.length / limit);احسب total بعد تطبيق عوامل التصفية، لأنه يصف القائمة التي طلبها العميل. اقرأ page وlimit باستخدام Number وارفض أي قيمة ليست عددًا صحيحًا ضمن النطاق، مع إرجاع 400. يحدّ وضع قيمة قصوى لـ limit من قدرة العميل على طلب كل شيء دفعة واحدة.
التحدي
سهل- يعيد
GET /api/tasksالاستجابة{ items, page, total, pages }. - يجب أن تكون قيمة
?page=عددًا صحيحًا يبدأ من 1؛ وقيمة?limit=من 1 إلى 50، والقيمة الافتراضية 10. - أي قيمة أخرى تؤدي إلى استجابة 400 مع الرمز
invalid_query.
أرسل GET /api/tasks?limit=2، ثم GET /api/tasks?page=2&limit=2، ثم GET /api/tasks?page=0.
جرّب بنفسك
import { Router } from 'express';
import { z } from 'zod';
import { sendError, sendInvalid } from '../errors.js';
import { nextId, tasks } from '../store.js';
const router = Router();
// مهمة كاملة كما يرسلها PUT. ترفض .strict() الحقول التي لا تُدرجها.
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();
const SORT_FIELDS = ['title', 'createdAt'];
// تكون القيمة true عندما تكون لمهمة أخرى هذه التسمية بالفعل، بغض النظر عن حالة الأحرف.
const titleTaken = (title, exceptId) =>
tasks.some((t) => t.id !== exceptId && t.title.toLowerCase() === title.toLowerCase());
router.get('/', (req, res) => {
const { done, q, sort } = 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 (sort !== undefined) {
const field = String(sort).replace(/^-/, '');
if (!SORT_FIELDS.includes(field)) {
return sendError(res, 400, 'invalid_query', 'sort must be title or createdAt, with - in front to reverse');
}
const direction = String(sort).startsWith('-') ? -1 : 1;
list = list.toSorted((a, b) => a[field].localeCompare(b[field]) * direction);
}
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;
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.