حولت JavaScript المستندات الساكنة إلى برمجيات. تبدو تطبيقات الصفحة الواحدة (Single-page apps) فورية؛ فلا إعادة تحميل كاملة للصفحة، ولا وميض للشاشات البيضاء. لكن هذه السرعة تأتي بتكلفة يتجاهلها الكثير من الفرق: الآلية الأساسية للويب تبدأ في التآكل. تصبح التنقلات هشة، وتواجه محركات البحث صعوبة في تتبع المسارات، وتضيع قارئات الشاشة، ويجد المستخدمون أنفسهم محاصرين في واجهات تبدو كمواقع إلكترونية ولكنها تتصرف كتطبيقات سطح مكتب معطلة.
المسبب عادة هو عنصر div مع معالج onClick.
توقف عن استخدام الـ Divs كروابط
لا يحمل الـ div أي معنى دلالي (semantic meaning)؛ فهو مجرد صندوق. عندما تضع عليه معالج نقر وتستخدمه لتوجيه المستخدمين إلى عرض جديد، فأنت تطلب من المتصفح أن يعامل صندوقاً من الكرتون وكأنه باب. المتصفح يرفض ذلك، وكذلك كل أداة بُنيت حول المتصفح.
لا تعلن قارئات الشاشة عن الـ div كرابط أو زر، بل تتخطاه، أو تقرأه كنص عادي. لا يستطيع المستخدم الذي يتنقل عبر الصوت استهدافه. أما زاحف محرك البحث (search engine crawler)، الذي يمسح صفحتك بحثاً عن روابط URL قابلة للاكتشاف، فلا يرى شيئاً ليتبعه، وكأن مسارك غير موجود أصلاً.
والأسوأ من ذلك، أنك تفقد السلوكيات التي يعرفها المستخدمون بالفعل. فالرابط الحقيقي يسمح لأي شخص بالنقر بزر الماوس الأيمن لفتحه في علامة تبويب جديدة، أو حفظ الوجهة في الإشارات المرجعية، أو نسخ العنوان لمشاركته. ويتوقع مستخدمو لوحة المفاتيح الضغط على Tab للوصول إليه و Enter لفتحه. الـ div لا يوفر أي شيء من هذا. وحتى لو أضفت tabIndex و role="link" ومستمعي لوحة المفاتيح، فأنت تعيد بناء ما يمنحه لك المتصفح مجاناً، ولكن بشكل سيئ، وسوف تنسى حالة استثنائية (edge case). أنت تفعل ذلك دائماً.
استخدم الـ Anchors للوجهات، والـ Buttons للإجراءات
لقد حل HTML هذه المشكلة بالفعل. يبدأ الارتباك لأن كلا العنصرين يبدوان قابلين للنقر، لذا يعاملهما المطورون كبدائل لبعضهما البعض، لكنهما ليسا كذلك.
استخدم وسم <a عندما تريد نقل المستخدم إلى رابط URL جديد. ليس تبديلاً وهمياً للعرض، ولا تغييراً في الحالة (state change)، بل موقعاً فعلياً. يجب أن تحتوي سمة href على عنوان حقيقي:
<a href="/docs">Documentation</a>
هذا كل شيء. إذا كان المستخدم ذاهباً إلى مكان ما، فاستخدم رابطاً.
استخدم <button> عندما يحدث شيء ما في الصفحة الحالية. الأزرار مخصصة للإجراءات مثل:
- فتح نافذة منبثقة (modal)
- إرسال نموذج (form)
- حفظ الإعدادات
- تبديل قائمة (menu)
الروابط للوجهات، والأزرار للإجراءات. الخلط بينهما يربك واجهتك ويكسر توقعات المستخدم.
اترك المتصفح يقوم بعمله
المتصفحات الحديثة هي نتيجة عقود من التطور والتقنين. فهي تتعامل مع الأمان، والسجل (history)، والتحميل المسبق (prefetching)، وإمكانية الوصول (accessibility) بشكل أفضل مما ستفعله برمجيات JavaScript التي تكتبها يدوياً.
وسم الـ anchor الحقيقي يغذي تلقائياً سجل المتصفح (history stack). ويعمل مع قائمة السياق (context menu) الأصلية. ويشارك في خوارزميات التحميل المسبق المدمجة في المتصفح عندما يحوم المستخدم فوقه أو يركز عليه، مما يجعل تطبيقك يبدو أسرع دون أن تكتب سطراً واحداً من الكود. كما يحترم تفضيلات المستخدم لفتح الروابط، ويتعاون مع مديري كلمات المرور، وأدوات الترجمة، وأنماط القراءة.
عندما تستبدل ذلك بدالة تنقل JavaScript، فإنك تتخلى عن كل ذلك. أنت لا تفقد الميزات فحسب؛ بل تجبر المستخدمين على التخلي عن عادات بنوها عبر كل موقع آخر على الإنترنت. هذا ليس قراراً تقنياً، بل هو تجربة مستخدم عدائية.
تحقق مما يقوم إطار العمل الخاص بك بإنتاجه فعلياً
React Router، و Vue Router، ومكونات Link في Next.js، و SvelteKit. تجعل هذه الأدوات التنقل من جانب العميل (client-side routing) يبدو سهلاً للغاية، لكن التجريد (abstraction) يولد الأخطاء.
افحص الـ DOM الخاص بك. افتح أدوات المطور في المتصفح وانظر إلى العناصر التي ينتجها إطار العمل الخاص بك. يجب أن يتم عرض مكون <Link> كـ وسم <a حقيقي مع سمة href صالحة في ملف HTML النهائي. إذا تم عرضه كـ span أو div أو أي شيء آخر بدون href مناسب، فقد خذلك التجريد. قم بإصلاح المكون، أو قم بتجاوز الإعداد الافتراضي، أو استخدم خاصية passHref أو ما يعادلها في إطار العمل. لا تثق في أن إطار العمل سيقوم بالأمر بشكل صحيح دون تحقق.
هذا الأمر مهم أيضاً لمشاكل عدم تطابق الـ hydration. إذا قام الخادم بإنتاج رابط وقام العميل بتحويله (hydrate) إلى عنصر غير رابط، فستنشئ أخطاء في إمكانية الوصول يصعب تتبعها لأن الـ HTML يبدو صحيحاً في الكود المصدري ولكن خاطئاً في الـ DOM الفعلي.
امنع الوجهات الوهمية
هناك نمط يرفض الموت: href="javascript:void(0)". يستخدمه المطورون عندما يريدون شيئاً يبدو كرابط ولكنه يعمل كزر، عادةً لأنهم لا يريدون تنسيق زر أو لأن قاعدة كود قديمة تتطلب ذلك.
توقف. هذا ليس رابطاً (URL). فهو لا يمنح المتصفح أي وجهة. إنه يلوث سجل التاريخ (history stack) بحالات غير قابلة للاستخدام. كما أنه يفسد سجل المتصفح وإمكانية الوصول. إنه فخ. إذا كنت بحاجة إلى سلوك النقر دون انتقال، فأنت بحاجة إلى <button>. قم بتنسيقه بالشكل الذي تريده. لا تهتم لغة CSS بما إذا كان العنصر زرًا أم رابطًا، لكن مستخدميك يهتمون.
اكتب نصاً يوضح الوجهة التي سينتقل إليها المستخدم
الكلمات الموجودة داخل الرابط مهمة. فغالباً ما يستعرض مستخدمو قارئات الشاشة قائمة بكل الروابط الموجودة في الصفحة للمسح السريع. إذا كانت جميع روابطك تقول "اقرأ المزيد" أو "انقر هنا"، فستتحول تلك القائمة إلى ضجيج عديم الفائدة.
كن محدداً. قارن بين هذين المثالين:
- سيئ:
<a href="/security/api-guide">اقرأ المزيد</a> - جيد:
<a href="/security/api-guide">اقرأ دليل أمن واجهة برمجة التطبيقات (API)</a>
المثال الثاني يخبر المستخدم بالضبط بما سيجده. كما أنه يمنح محركات البحث سياقاً حول صفحة الوجهة، ويجعل قائمة الروابط الخاصة بك قابلة للتنقل. يُعد نص الرابط الوصفي أحد أسهل وأرخص المكاسب التي يمكنك تحقيقها في مجال إمكانية الوصول.
اختبر بجدية
الهندسة البرمجية لا تعني شيئاً إذا لم تقم بالتحقق منها.
أولاً، اختبر تدفق التنقل عبر لوحة المفاتيح. افصل الماوس الخاص بك. استخدم مفتاح Tab للتنقل عبر كل عنصر تفاعلي في موقعك. يجب أن يظهر كل رابط حقيقي إطار تركيز (focus outline) مرئي؛ ليس مجرد توهج خافت يختفي مع الخلفية، بل حلقة واضحة يمكن للعين المتعبة تمييزها. اضغط Enter، ويجب أن يقوم بتفعيل الرابط. إذا تجاوز مفتاح Tab عنصراً ما، أو إذا لم يفعل مفتاح Enter شيئاً، فهذا يعني وجود خطأ (bug).
ثانياً، اختبر المسارات (routes) على مستوى الخادم. التوجيه من جهة العميل (Client-side routing) ليس سوى طبقة سطحية. إذا قام مستخدم بحفظ /dashboard/reports في العلامات المرجعية وعاد في اليوم التالي، أو قام بتحديث الصفحة، فيجب أن يعرف خادمك كيفية تقديم تلك الصفحة. قم بتكوين الوكيل العكسي (reverse proxy) أو إطار عمل الخادم الخاص بك للرجوع إلى هيكل التطبيق (application shell) في حالة المسارات غير المعروفة، أو لتقديم ملف HTML الصحيح مباشرة. ظهور خطأ 404 عند تحديث الصفحة ليس مجرد خطأ بسيط، بل هو وعد لم يتم الوفاء به.
JavaScript هي طبقة قوية
