الوحدة 5 · كتابة برامج حقيقية
بناء Pocket
22 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
تجمع Pocket في تطبيق متصفّح مكتمل له كائن state واحد، ومنطق نقي، ودالة render واحدة، وأحداث مفوَّضة، وتصفية، وبيانات محفوظة، وتفحصه وفق تعريف واضح لـ “مكتمل”.
- 1الشكل: state واحد، وطريقة واحدة لتغييره
- 2دالة render تشتقّ كل شيء
- 3معالجات الأحداث تترجم الأحداث إلى تحديثات
- 4تعريف “مكتمل”
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
لديك كل القطع: دوال مساعدة للمال، والتحقّق من المدخلات، والعرض، والأحداث، والنموذج، والتخزين، والوحدات، والأصناف. يجمعها هذا الدرس في Pocket المكتمل، والأهم أنه يُريك الشكل الذي يُمسك تطبيقًا صغيرًا متماسكًا فيبقى سهل التغيير.
الشكل: state واحد، وطريقة واحدة لتغييره
لكل تطبيق تفاعلي بيانات تتغيّر. في Pocket هي قائمة المصاريف وفلتر الفئة الذي اختاره المستخدم. ضعها كلها في كائن واحد، هو الـ state (الحالة):
const state = {
expenses: loadExpenses(),
filter: "all",
};ثم اجعل دالة واحدة هي الطريقة الوحيدة لتغييره. إنها تطبّق التغيير، وتحفظ، وتعيد العرض:
function update(changes) {
Object.assign(state, changes);
saveExpenses(state.expenses);
render();
}Object.assign(state, changes) تنسخ خصائص changes إلى state، لذلك لا تغيّر update({ filter: "food" }) إلا الفلتر. وصار كل معالج حدث ينتهي بالطريقة نفسها: احسب القيم الجديدة، ثم استدعِ update. ولن تحتاج أبدًا إلى تذكّر الحفظ أو إعادة الرسم، لأن نسيانهما صار مستحيلًا.
دالة render تشتقّ كل شيء
تقرأ render الـ state وترسم الواجهة كلها. وكل ما يمكن حسابه من الـ state يُحسب هنا، لا يُخزَّن: القائمة المرئية تعتمد على الفلتر، والمجموع يعتمد على القائمة المرئية.
function render() {
const visible =
state.filter === "all"
? state.expenses
: state.expenses.filter((e) => e.category === state.filter);
list.replaceChildren();
if (visible.length === 0) {
const empty = document.createElement("li");
empty.className = "empty";
empty.textContent = "No expenses yet. Add your first one above.";
list.append(empty);
}
for (const expense of visible) {
list.append(renderItem(expense));
}
totalEl.textContent = `Total: ${formatMoney(totalCents(visible))}`;
}
function renderItem(expense) {
const li = document.createElement("li");
li.dataset.id = expense.id;
const text = document.createElement("span");
text.textContent = `${expense.label}: ${formatMoney(expense.amount)} (${expense.category})`;
const button = document.createElement("button");
button.type = "button";
button.className = "delete";
button.textContent = "Delete";
button.setAttribute("aria-label", `Delete ${expense.label}`);
li.append(text, button);
return li;
}لأن render تبدأ دائمًا من الـ state، لا يمكن للفلتر والقائمة والمجموع أن تتناقض أبدًا. وتبديل الفلتر إلى “food” لا يُخفي بعض عناصر <li> ويعيد حساب رقم؛ بل يرسم ببساطة صورة مختلفة للـ state نفسه.
تفصيلتان تستحقّان النسخ. الحالة الفارغة تحوّل القائمة الخالية إلى إرشاد. والـ aria-label على زر الحذف تخبر مستخدمي قارئات الشاشة أيّ مصروف يحذفه الزر، فعشرة أزرار كلها باسم “Delete” لا فائدة منها لمن لا يرى الصف.
معالجات الأحداث تترجم الأحداث إلى تحديثات
حين يكون الـ state وupdate وrender في مكانها، تصبح كل ميزة بضعة أسطر:
form.addEventListener("submit", (event) => {
event.preventDefault();
const result = readExpenseForm(form); // { values } or { errors }
if (result.errors) {
showErrors(form, result.errors);
return;
}
update({ expenses: addExpense(state.expenses, { id: nextId(), ...result.values }) });
form.reset();
form.elements.label.focus();
});
list.addEventListener("click", (event) => {
const button = event.target.closest(".delete");
if (!button) return;
const id = button.closest("li").dataset.id;
update({ expenses: state.expenses.filter((e) => e.id !== id) });
});
filterSelect.addEventListener("change", () => {
update({ filter: filterSelect.value });
});
render();readExpenseForm وshowErrors هما كود النموذج من القسم 4، وقد نُقل إلى دالتين صغيرتين لهما اسمان: الأولى تقرأ وتشذّب وتحوّل وتتحقّق، وتُرجع إمّا قيمًا نظيفة أو كائن أخطاء؛ والثانية تكتب الرسائل وتضبط aria-invalid. ومنح كل منهما اسمًا يحوّل معالج الإرسال إلى ملخّص تستطيع قراءته في خمس ثوانٍ.
لاحظ ما لا تحتويه معالجات الأحداث: لا بناء للـ DOM، ولا حفظ، ولا مجاميع. إنها تقرأ الحدث، وتستدعي دوال نقية مثل addExpense من القسم 3، وتسلّم النتيجة إلى update. القواعد تعيش في expenses.js، حيث تستطيع اختبارها دون متصفّح؛ أما معالجات الأحداث فهي الغراء الذي يربط الأجزاء.
تعريف “مكتمل”
“إضافة مصروف تعمل” هي نقطة بداية الاختبار، لا نهايته. قبل أن تقول إن Pocket اكتمل، مرّ على هذه القائمة يدويًا:
- الفراغ: دون أي بيانات، تشرح الصفحة ما يجب فعله.
- المدخلات السيئة: التسمية الفارغة، والمبلغ
abcأو0أو-5، كل منها يعرض رسالة واضحة ولا يضيف شيئًا. - إعادة التحميل: كل ما أضفته ما زال موجودًا، والفلتر يبدأ على “All”.
- لوحة المفاتيح وحدها: تستطيع الانتقال بـ Tab إلى كل حقل وزر، والإرسال بـ Enter، والحذف بـ Enter أو Space.
- البيانات الغريبة: تسمية مثل
<b>hi</b>تظهر نصًا، والتسمية الطويلة جدًا لا تكسر التخطيط. - الـ Console: لا أخطاء ولا تحذيرات أثناء قيامك بكل ما سبق.
كل بند هنا يأتي من درس في هذه الدورة، وكل منها يلتقط فئة حقيقية من الأخطاء كان المستخدمون سيكتشفونها نيابةً عنك.
التمرين أدناه يسلّمك Pocket شبه مكتمل، وما زال عليك ربط التصفية والحذف. وبعد ذلك، درس قصير أخير حول وجهتك التالية.
أهم النقاط
- احتفظ بكل البيانات المتغيّرة في مكان واحد، وغيّرها عبر دالة واحدة تحفظ وتعيد العرض، فلا تختلف الصفحة والبيانات والتخزين أبدًا.
- اشتقّ كل ما يمكنك اشتقاقه، مثل القائمة المرئية والمجاميع، من الـ state وقت العرض بدلًا من تخزينه منفصلًا.
- الدوال النقية تحمل القواعد؛ ومعالجات الأحداث تبقى قصيرة ولا تفعل إلا ترجمة الأحداث إلى تغييرات في الـ state.
- يكتمل التطبيق حين يتعامل مع الحالة الفارغة، والمدخلات السيئة، وإعادة التحميل، والاستخدام بلوحة المفاتيح وحدها، لا مع المسار السعيد فقط.