التوجيه الحديث من جهة العميل: واجهة برمجة تطبيقات التنقل

تتضمّن هذه الميزة توحيد التوجيه من جهة العميل من خلال واجهة برمجة تطبيقات جديدة تمامًا تعمل على تحسين عملية إنشاء تطبيقات الصفحة الواحدة.

Browser Support

  • Chrome: 102.
  • Edge: 102.
  • Firefox: 147.
  • Safari: 26.2.

Source

تتميز التطبيقات من صفحة واحدة، أو SPAs، بميزة أساسية هي إعادة كتابة محتواها بشكل ديناميكي أثناء تفاعل المستخدم مع الموقع الإلكتروني، بدلاً من الطريقة التلقائية لتحميل صفحات جديدة بالكامل من الخادم.

في حين أنّ تطبيقات الصفحة الواحدة كانت قادرة على توفير هذه الميزة من خلال History API (أو في حالات محدودة، من خلال تعديل جزء #hash في الموقع الإلكتروني)، إلا أنّها واجهة برمجة تطبيقات غير ملائمة تم تطويرها قبل وقت طويل من أن تصبح تطبيقات الصفحة الواحدة هي القاعدة، والويب يحتاج إلى نهج جديد تمامًا. ‫Navigation API هي واجهة برمجة تطبيقات مقترَحة تعمل على إصلاح هذه المشاكل بشكل كامل، بدلاً من محاولة إصلاح المشاكل البسيطة في History API. (على سبيل المثال، عملت ميزة استعادة موضع التمرير على تصحيح History API بدلاً من محاولة إعادة اختراعه).

توضّح هذه المشاركة Navigation API بشكل عام. لقراءة الاقتراح الفني، يُرجى الاطّلاع على مسودة التقرير في مستودع WICG.

مثال على الاستخدام

لاستخدام Navigation API، ابدأ بإضافة أداة معالجة "navigate" إلى العنصر العام navigation. هذا الحدث مركزي بشكل أساسي: سيتم تنشيطه لجميع أنواع عمليات التنقّل، سواء اتّخذ المستخدِم إجراءً (مثل النقر على رابط أو إرسال نموذج أو الرجوع إلى صفحة سابقة أو الانتقال إلى صفحة لاحقة) أو عندما يتم تنشيط التنقّل آليًا (أي من خلال رمز موقعك الإلكتروني). في معظم الحالات، يتيح لك ذلك الرمز إلغاء السلوك التلقائي للمتصفّح لهذا الإجراء. بالنسبة إلى التطبيقات ذات الصفحة الواحدة، من المحتمل أن يعني ذلك إبقاء المستخدم على الصفحة نفسها وتحميل محتوى الموقع الإلكتروني أو تغييره.

يتم تمرير NavigateEvent إلى أداة معالجة "navigate" التي تحتوي على معلومات حول عملية التنقّل، مثل عنوان URL للوجهة، وتتيح لك الردّ على عملية التنقّل في مكان مركزي واحد. قد يبدو مستمع "navigate" الأساسي على النحو التالي:

navigation.addEventListener('navigate', navigateEvent => {
  // Exit early if this navigation shouldn't be intercepted.
  // The properties to look at are discussed later in the article.
  if (shouldNotIntercept(navigateEvent)) return;

  const url = new URL(navigateEvent.destination.url);

  if (url.pathname === '/') {
    navigateEvent.intercept({handler: loadIndexPage});
  } else if (url.pathname === '/cats/') {
    navigateEvent.intercept({handler: loadCatsPage});
  }
});

يمكنك التعامل مع التنقّل بإحدى الطريقتَين التاليتَين:

  • الاتصال بـ intercept({ handler }) (كما هو موضّح أعلاه) للتعامل مع التنقّل
  • الاتصال بـ preventDefault()، ما يؤدي إلى إلغاء التنقّل بالكامل

يستدعي هذا المثال intercept() في الحدث. يستدعي المتصفّح دالة handler، التي يجب أن تضبط الحالة التالية لموقعك الإلكتروني. سيؤدي ذلك إلى إنشاء عنصر انتقال، navigation.transition، يمكن أن يستخدمه رمز آخر لتتبُّع تقدّم عملية التنقّل.

يُسمح عادةً باستخدام كل من intercept() وpreventDefault()، ولكن هناك حالات لا يمكن فيها إجراء المكالمات. لا يمكنك التعامل مع عمليات التنقّل من خلال intercept() إذا كانت عملية التنقّل من مصادر متعددة. ولا يمكنك إلغاء عملية تنقّل من خلال preventDefault() إذا كان المستخدم يضغط على زر الرجوع أو زر التقدم في المتصفح، إذ لا يجب أن تتمكّن من حصر المستخدمين في موقعك الإلكتروني. (تتم مناقشة هذا الموضوع على GitHub).

حتى إذا لم تتمكّن من إيقاف التنقّل أو اعتراضه، سيظلّ الحدث "navigate" يتم تنشيطه. وهي إعلامية، لذا يمكن أن يسجّل الرمز، على سبيل المثال، حدثًا في "إحصاءات Google" للإشارة إلى أنّ أحد المستخدِمين سيغادر موقعك الإلكتروني.

لماذا يجب إضافة حدث آخر إلى المنصة؟

تعمل أداة معالجة الأحداث "navigate" على مركزية معالجة تغييرات عناوين URL داخل صفحة SPA. ويصعب تحقيق ذلك باستخدام واجهات برمجة التطبيقات القديمة. إذا سبق لك كتابة التوجيه لتطبيقك الخاص ذي الصفحة الواحدة باستخدام History API، ربما أضفت رمزًا برمجيًا مشابهًا لما يلي:

function updatePage(event) {
  event.preventDefault(); // we're handling this link
  window.history.pushState(null, '', event.target.href);
  // TODO: set up page based on new URL
}
const links = [...document.querySelectorAll('a[href]')];
links.forEach(link => link.addEventListener('click', updatePage));

هذا جيد، ولكنّه ليس شاملاً. قد تظهر الروابط وتختفي على صفحتك، وهي ليست الطريقة الوحيدة التي يمكن للمستخدمين من خلالها التنقّل بين الصفحات. على سبيل المثال، قد يرسلون نموذجًا أو حتى يستخدمون خريطة صور. قد تتعامل صفحتك مع هذه الحالات، ولكن هناك مجموعة كبيرة من الاحتمالات التي يمكن تبسيطها، وهو ما تحقّقه واجهة برمجة التطبيقات الجديدة للتنقّل.

بالإضافة إلى ذلك، لا يتعامل ما سبق مع التنقّل للخلف أو للأمام. هناك حدث آخر لذلك، "popstate".

أرى أنّ واجهة برمجة التطبيقات History API يمكن أن تساعد إلى حدّ ما في تحقيق هذه الإمكانات. ومع ذلك، لا يتضمّن سوى مساحتَي عرض: الاستجابة إذا ضغط المستخدم على "رجوع" أو "تقديم" في المتصفّح، بالإضافة إلى إرسال عناوين URL واستبدالها. ولا يتضمّن وظيفة مشابهة للوظيفة "navigate"، إلا إذا أعددت يدويًا أدوات معالجة أحداث النقر مثلاً، كما هو موضّح أعلاه.

تحديد كيفية التعامل مع عملية التنقّل

تحتوي navigateEvent على الكثير من المعلومات حول التنقّل التي يمكنك استخدامها لتحديد كيفية التعامل مع عملية تنقّل معيّنة.

في ما يلي السمات الرئيسية:

canIntercept
إذا كانت القيمة خطأ، لن تتمكّن من اعتراض عملية التنقّل. لا يمكن اعتراض عمليات التنقّل بين المصادر المختلفة وعمليات الانتقال بين المستندات المختلفة.
destination.url
ربما تكون هذه هي أهم معلومة يجب مراعاتها عند التعامل مع التنقّل.
hashChange
تكون القيمة "صحيح" إذا كانت عملية التنقّل تتم في المستند نفسه، وكان الجزء المختلف في عنوان URL هو التجزئة فقط. في تطبيقات الصفحة الواحدة الحديثة، يجب أن تكون قيمة التجزئة مخصّصة للربط بأجزاء مختلفة من المستند الحالي. لذلك، إذا كانت قيمة hashChange صحيحة، من المحتمل أنّك لن تحتاج إلى اعتراض عملية التنقّل هذه.
downloadRequest
إذا كانت القيمة صحيحة، يعني ذلك أنّ عملية التنقّل بدأت من خلال رابط يتضمّن السمة download. في معظم الحالات، ليس عليك اعتراض هذا الإجراء.
formData
إذا لم تكن هذه القيمة فارغة، يعني ذلك أنّ عملية التنقّل هذه هي جزء من إرسال نموذج POST. يجب مراعاة ذلك عند التعامل مع التنقّل. إذا كنت تريد التعامل مع عمليات التنقّل باستخدام GET فقط، تجنَّب اعتراض عمليات التنقّل التي لا تكون فيها قيمة formData فارغة. اطّلِع على مثال حول كيفية التعامل مع عمليات إرسال النماذج لاحقًا في المقالة.
navigationType
هذا هو أحد الخيارات التالية: "reload" أو "push" أو "replace" أو "traverse". إذا كانت القيمة "traverse"، لا يمكن إلغاء عملية التنقّل هذه من خلال preventDefault().

على سبيل المثال، يمكن أن تكون الدالة shouldNotIntercept المستخدَمة في المثال الأول على النحو التالي:

function shouldNotIntercept(navigationEvent) {
  return (
    !navigationEvent.canIntercept ||
    // If this is just a hashChange,
    // just let the browser handle scrolling to the content.
    navigationEvent.hashChange ||
    // If this is a download,
    // let the browser perform the download.
    navigationEvent.downloadRequest ||
    // If this is a form submission,
    // let that go to the server.
    navigationEvent.formData
  );
}

اعتراض الكرة

عندما يستدعي الرمز intercept({ handler }) من داخل أداة معالجة "navigate"، يتم إعلام المتصفّح بأنّه بصدد إعداد الصفحة للحالة الجديدة المعدَّلة، وأنّ عملية التنقّل قد تستغرق بعض الوقت.

يبدأ المتصفّح بتسجيل موضع التمرير للحالة الحالية، حتى يمكن استعادته لاحقًا بشكل اختياري، ثم يستدعي دالة handler. إذا عرضت الدالة handler وعدًا (يحدث ذلك تلقائيًا مع الدوال غير المتزامنة)، يخبر هذا الوعد المتصفّح بالمدة التي يستغرقها التنقّل وما إذا كان ناجحًا.

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        const articleContent = await getArticleContent(url.pathname);
        renderArticlePage(articleContent);
      },
    });
  }
});

وبالتالي، تقدّم واجهة برمجة التطبيقات هذه مفهومًا دلاليًا يفهمه المتصفّح: يحدث حاليًا تنقّل في صفحة واحدة، ويتم بمرور الوقت تغيير المستند من عنوان URL وحالة سابقَين إلى عنوان URL وحالة جديدَين. ويوفّر ذلك عددًا من المزايا المحتملة، بما في ذلك تسهيل الاستخدام، إذ يمكن للمتصفحات عرض بداية عملية التنقّل أو نهايتها أو احتمال تعذّرها. على سبيل المثال، يفعّل Chrome مؤشر التحميل الأصلي، ويسمح للمستخدم بالتفاعل مع زر الإيقاف. (لا يحدث ذلك حاليًا عندما يتنقّل المستخدم من خلال زرَّي الرجوع/التقديم، ولكن سيتم إصلاح ذلك قريبًا).

عند اعتراض عمليات التنقّل، سيتم تطبيق عنوان URL الجديد قبل استدعاء دالة رد الاتصال handler مباشرةً. إذا لم تعدّل DOM على الفور، سيؤدي ذلك إلى عرض المحتوى القديم مع عنوان URL الجديد لفترة من الوقت. ويؤثر ذلك في أمور مثل تحديد عناوين URL النسبية عند جلب البيانات أو تحميل موارد فرعية جديدة.

تتم مناقشة طريقة لتأخير تغيير عنوان URL على GitHub، ولكن يُنصح بشكل عام بتعديل الصفحة على الفور باستخدام نوع من العناصر النائبة للمحتوى الوارد:

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        // The URL has already changed, so quickly show a placeholder.
        renderArticlePagePlaceholder();
        // Then fetch the real data.
        const articleContent = await getArticleContent(url.pathname);
        renderArticlePage(articleContent);
      },
    });
  }
});

لا يؤدي ذلك إلى تجنُّب مشاكل تحليل عناوين URL فحسب، بل يمنح المستخدم أيضًا شعورًا بالسرعة لأنّك تردّ عليه على الفور.

إشارات الإلغاء

بما أنّه يمكنك تنفيذ عمل غير متزامن في معالج intercept()، من المحتمل أن يصبح التنقّل غير ضروري. يحدث ذلك في الحالات التالية:

  • ينقر المستخدم على رابط آخر، أو ينفّذ بعض الرموز عملية تنقّل أخرى. في هذه الحالة، يتم إيقاف التنقّل القديم واستخدام التنقّل الجديد.
  • ينقر المستخدم على زر "إيقاف" في المتصفّح.

للتعامل مع أيّ من هذه الاحتمالات، يحتوي الحدث الذي تمّ تمريره إلى أداة معالجة "navigate" على السمة signal، وهي AbortSignal. لمزيد من المعلومات، يُرجى الاطّلاع على عمليات الجلب القابلة للإلغاء.

باختصار، يوفّر هذا الإجراء عنصرًا ينشئ حدثًا عندما يحين وقت التوقف عن العمل. والجدير بالذكر أنّه يمكنك تمرير AbortSignal إلى أي مكالمات تجريها إلى fetch()، ما سيؤدي إلى إلغاء طلبات الشبكة أثناء التنفيذ في حال تم إيقاف التنقّل. سيؤدي ذلك إلى حفظ نطاق ترددي للمستخدم، ورفض Promise الذي تم إرجاعه بواسطة fetch()، ما يمنع أي رمز برمجي لاحق من تنفيذ إجراءات مثل تعديل نموذج المستند (DOM) لعرض عملية تنقّل غير صالحة في الصفحة.

في ما يلي المثال السابق، ولكن مع تضمين getArticleContent، ما يوضّح كيفية استخدام AbortSignal مع fetch():

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        // The URL has already changed, so quickly show a placeholder.
        renderArticlePagePlaceholder();
        // Then fetch the real data.
        const articleContentURL = new URL(
          '/get-article-content',
          location.href
        );
        articleContentURL.searchParams.set('path', url.pathname);
        const response = await fetch(articleContentURL, {
          signal: navigateEvent.signal,
        });
        const articleContent = await response.json();
        renderArticlePage(articleContent);
      },
    });
  }
});

معالجة التمرير

عند intercept() عملية تنقّل، سيحاول المتصفّح التعامل مع التمرير تلقائيًا.

بالنسبة إلى عمليات الانتقال إلى سجلّ جديد (عندما تكون قيمة navigationEvent.navigationType هي "push" أو "replace")، يعني ذلك محاولة الانتقال إلى الجزء المشار إليه في جزء عنوان URL (الجزء الذي يلي #)، أو إعادة ضبط موضع التمرير إلى أعلى الصفحة.

بالنسبة إلى عمليات إعادة التحميل والتنقّل، يعني ذلك استعادة موضع التمرير إلى الموضع الذي كان عليه في آخر مرة تم فيها عرض سجلّ التصفّح هذا.

يحدث ذلك تلقائيًا بعد أن يتم تنفيذ الوعد الذي تعرضه الدالة handler، ولكن إذا كان من المنطقي التمرير للأعلى في وقت سابق، يمكنك استدعاء الدالة navigateEvent.scroll():

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        const articleContent = await getArticleContent(url.pathname);
        renderArticlePage(articleContent);
        navigateEvent.scroll();

        const secondaryContent = await getSecondaryContent(url.pathname);
        addSecondaryContent(secondaryContent);
      },
    });
  }
});

بدلاً من ذلك، يمكنك إيقاف معالجة التمرير التلقائي تمامًا من خلال ضبط الخيار scroll الخاص بـ intercept() على "manual":

navigateEvent.intercept({
  scroll: 'manual',
  async handler() {
    // …
  },
});

التعامل مع التركيز

بعد أن يتم حلّ الوعد الذي تعرضه handler، سيركّز المتصفّح على العنصر الأول الذي تم ضبط السمة autofocus فيه، أو على العنصر <body> إذا لم يتضمّن أي عنصر هذه السمة.

يمكنك إيقاف هذا السلوك من خلال ضبط الخيار focusReset الخاص بـ intercept() على "manual":

navigateEvent.intercept({
  focusReset: 'manual',
  async handler() {
    // …
  },
});

أحداث النجاح والفشل

عند استدعاء معالج intercept()، سيحدث أحد الأمرين التاليين:

  • إذا تم استيفاء Promise الذي تم عرضه (أو إذا لم تستدعِ intercept())، ستفعّل Navigation API الحدث "navigatesuccess" مع Event.
  • إذا تم رفض Promise المعروض، ستطلق واجهة برمجة التطبيقات "navigateerror" مع ErrorEvent.

تسمح هذه الأحداث للرمز البرمجي بالتعامل مع النجاح أو الفشل بطريقة مركزية. على سبيل المثال، يمكنك التعامل مع النجاح من خلال إخفاء مؤشر مستوى التقدّم الذي تم عرضه سابقًا، كما يلي:

navigation.addEventListener('navigatesuccess', event => {
  loadingIndicator.hidden = true;
});

أو قد تعرض رسالة خطأ في حال تعذُّر ذلك:

navigation.addEventListener('navigateerror', event => {
  loadingIndicator.hidden = true; // also hide indicator
  showMessage(`Failed to load page: ${event.message}`);
});

يُعدّ مستمع حدث "navigateerror"، الذي يتلقّى ErrorEvent، مفيدًا بشكل خاص لأنّه يضمن تلقّي أي أخطاء من الرمز البرمجي الذي يضبط صفحة جديدة. يمكنك ببساطة await fetch() مع العلم أنّه في حال عدم توفّر الشبكة، سيتم توجيه الخطأ في النهاية إلى "navigateerror".

توفّر navigation.currentEntry إمكانية الوصول إلى الإدخال الحالي. هذا عنصر يصف الموقع الجغرافي الحالي للمستخدم. يتضمّن هذا الإدخال عنوان URL الحالي والبيانات الوصفية التي يمكن استخدامها لتحديد هذا الإدخال بمرور الوقت والحالة التي يقدّمها المطوّر.

تتضمّن البيانات الوصفية key، وهي سمة سلسلة فريدة لكل إدخال تمثّل الإدخال الحالي والفتحة الخاصة به. يظل هذا المفتاح كما هو حتى إذا تغيّر عنوان URL أو حالة الإدخال الحالي. سيظلّ في الموضع نفسه. في المقابل، إذا ضغط المستخدم على "رجوع" ثم أعاد فتح الصفحة نفسها، سيتغيّر key لأنّ هذا الإدخال الجديد ينشئ خانة جديدة.

بالنسبة إلى المطوّر، يكون key مفيدًا لأنّ Navigation API تتيح لك توجيه المستخدم مباشرةً إلى إدخال يتضمّن مفتاحًا مطابقًا. يمكنك الاحتفاظ بها، حتى في حالات الإدخالات الأخرى، من أجل التنقّل بسهولة بين الصفحات.

// On JS startup, get the key of the first loaded page
// so the user can always go back there.
const {key} = navigation.currentEntry;
backToHomeButton.onclick = () => navigation.traverseTo(key);

// Navigate away, but the button will always work.
await navigation.navigate('/another_url').finished;

ولاية

تعرض Navigation API مفهوم "الحالة"، وهو عبارة عن معلومات يقدّمها المطوّر ويتم تخزينها بشكل دائم في سجلّ التصفّح الحالي، ولكنّها غير مرئية للمستخدم مباشرةً. هذا مشابه تمامًا للرمز history.state في History API، ولكنّه محسّن.

في Navigation API، يمكنك استدعاء طريقة .getState() للإدخال الحالي (أو أي إدخال) لعرض نسخة من حالته:

console.log(navigation.currentEntry.getState());

ستكون القيمة التلقائية هي undefined.

حالة الإعداد

على الرغم من إمكانية تغيير عناصر الحالة، لا يتم حفظ هذه التغييرات مع إدخال السجلّ، لذا:

const state = navigation.currentEntry.getState();
console.log(state.count); // 1
state.count++;
console.log(state.count); // 2
// But:
console.info(navigation.currentEntry.getState().count); // will still be 1

الطريقة الصحيحة لضبط الحالة هي أثناء التنقّل بين النصوص البرمجية:

navigation.navigate(url, {state: newState});
// Or:
navigation.reload({state: newState});

حيث يمكن أن يكون newState أي كائن قابل للاستنساخ.

إذا كنت تريد تعديل حالة الإدخال الحالي، من الأفضل إجراء عملية تنقّل تحلّ محل الإدخال الحالي:

navigation.navigate(location.href, {state: newState, history: 'replace'});

بعد ذلك، يمكن لبرنامج معالجة الأحداث "navigate" رصد هذا التغيير من خلال navigateEvent.destination:

navigation.addEventListener('navigate', navigateEvent => {
  console.log(navigateEvent.destination.getState());
});

تعديل الحالة بشكل متزامن

بشكل عام، من الأفضل تعديل الحالة بشكل غير متزامن من خلال navigation.reload({state: newState})، ثم يمكن لمستمع "navigate" تطبيق هذه الحالة. ومع ذلك، في بعض الأحيان، يكون تغيير الحالة قد تم تطبيقه بالكامل عندما يصلك إشعار به، مثلاً عندما ينقر المستخدم على عنصر <details> أو يغيّر حالة إدخال نموذج. في هذه الحالات، قد تحتاج إلى تعديل الحالة حتى يتم الاحتفاظ بهذه التغييرات عند إعادة التحميل والتنقّل. يمكن إجراء ذلك باستخدام updateCurrentEntry():

navigation.updateCurrentEntry({state: newState});

يمكنك أيضًا المشاركة في فعالية للتعرّف على هذا التغيير:

navigation.addEventListener('currententrychange', () => {
  console.log(navigation.currentEntry.getState());
});

ولكن، إذا وجدت نفسك تتفاعل مع تغييرات الحالة في "currententrychange"، قد تحتاج إلى تقسيم رمز معالجة الحالة أو حتى تكراره بين حدث "navigate" وحدث "currententrychange"، بينما يتيح لك navigation.reload({state: newState}) معالجته في مكان واحد.

الحالة مقابل مَعلمات عنوان URL

بما أنّ الحالة يمكن أن تكون عنصرًا منظَّمًا، من المغري استخدامها لجميع حالات تطبيقك. ومع ذلك، من الأفضل في كثير من الحالات تخزين هذه الحالة في عنوان URL.

إذا كنت تتوقّع الاحتفاظ بالحالة عندما يشارك المستخدم عنوان URL مع مستخدم آخر، خزِّنها في عنوان URL. في ما عدا ذلك، يكون عنصر الحالة هو الخيار الأفضل.

الوصول إلى جميع الإدخالات

مع ذلك، لا يقتصر الأمر على "الإدخال الحالي". توفّر واجهة برمجة التطبيقات أيضًا طريقة للوصول إلى القائمة الكاملة بالإدخالات التي تنقّل المستخدم بينها أثناء استخدام موقعك الإلكتروني من خلال طلب navigation.entries()، والذي يعرض مصفوفة لقطة للإدخالات. يمكن استخدام ذلك، على سبيل المثال، لعرض واجهة مستخدم مختلفة استنادًا إلى كيفية انتقال المستخدم إلى صفحة معيّنة، أو لمراجعة عناوين URL السابقة أو حالاتها. لا يمكن إجراء ذلك باستخدام History API الحالية.

يمكنك أيضًا الاستماع إلى حدث "dispose" على NavigationHistoryEntry فردي، ويتم تشغيله عندما لا يكون الإدخال جزءًا من سجلّ المتصفّح. يمكن أن يحدث ذلك كجزء من عملية تنظيف عامة، ولكن يمكن أن يحدث أيضًا عند التنقّل. على سبيل المثال، إذا رجعت إلى 10 أماكن سابقة، ثم انتقلت إلى الأمام، سيتم التخلص من إدخالات السجلّ العشرة هذه.

أمثلة

يتم تنشيط الحدث "navigate" لجميع أنواع التنقّل، كما هو موضّح أعلاه. (يتضمّن الملحق الطويل في المواصفات جميع الأنواع الممكنة).

مع أنّ الحالة الأكثر شيوعًا في العديد من المواقع الإلكترونية هي عندما ينقر المستخدم على <a href="...">، هناك نوعان ملحوظان وأكثر تعقيدًا من أنواع التنقّل يجدر تناولهما.

التنقّل الآلي

أولاً، التنقّل الآلي، حيث يحدث التنقّل بسبب طلب طريقة داخل الرمز البرمجي من جهة العميل.

يمكنك طلب navigation.navigate('/another_page') من أي مكان في الرمز البرمجي لتنفيذ عملية تنقّل. ستتم معالجة ذلك من خلال أداة معالجة الأحداث المركزية المسجّلة في أداة المعالجة "navigate"، وسيتم استدعاء أداة المعالجة المركزية بشكل متزامن.

يهدف ذلك إلى تحسين تجميع الطرق القديمة، مثل location.assign() وغيرها، بالإضافة إلى طريقتَي History API، وهما pushState() وreplaceState().

تعرض الطريقة navigation.navigate() عنصرًا يحتوي على مثيلَين من Promise في { committed, finished }. يتيح ذلك للمستدعي الانتظار إلى أن يتم "تنفيذ" الانتقال (تغيّر عنوان URL المرئي وأصبح NavigationHistoryEntry جديدًا متاحًا) أو "اكتماله" (اكتملت جميع الوعود التي تم إرجاعها بواسطة intercept({ handler }) أو تم رفضها بسبب حدوث خطأ أو بسبب إيقافها بشكل استباقي من خلال عملية تنقّل أخرى).

تحتوي الطريقة navigate أيضًا على عنصر خيارات، حيث يمكنك ضبط ما يلي:

  • state: حالة إدخال السجلّ الجديد، كما هو متاح من خلال طريقة .getState() في NavigationHistoryEntry
  • history: يمكن ضبطها على "replace" لاستبدال إدخال السجلّ الحالي.
  • info: عنصر يتم تمريره إلى حدث التنقّل من خلال navigateEvent.info

على وجه الخصوص، يمكن أن يكون info مفيدًا، على سبيل المثال، للإشارة إلى رسم متحرك معيّن يؤدي إلى ظهور الصفحة التالية. (قد يكون الحل البديل هو ضبط متغير عام أو تضمينه كجزء من #hash. كلا الخيارَين غير مناسبَين بعض الشيء.) يُرجى العِلم أنّه info لن يتم إعادة تشغيل هذا الحدث إذا تسبّب المستخدم لاحقًا في التنقّل، مثلاً من خلال زرَّي "رجوع" و"تقديم". في الواقع، سيكون undefined دائمًا في هذه الحالات.

عرض توضيحي لفتح التطبيق من اليسار أو اليمين

تتضمّن navigation أيضًا عددًا من طرق التنقّل الأخرى، وكلّها تعرض عنصرًا يحتوي على { committed, finished }. سبَق أن ذكرتُ traverseTo() (الذي يقبل key يشير إلى إدخال معيّن في سجلّ المستخدم) وnavigate(). وتشمل أيضًا back() وforward() وreload(). يتم التعامل مع كل هذه الطرق، تمامًا مثل navigate()، من خلال أداة معالجة الأحداث المركزية "navigate".

عمليات إرسال النماذج

ثانيًا، يُعدّ إرسال HTML <form> عبر POST نوعًا خاصًا من التنقّل، ويمكن لواجهة برمجة التطبيقات Navigation API اعتراض هذا النوع. على الرغم من أنّها تتضمّن حمولة إضافية، لا يزال يتم التعامل مع التنقّل مركزيًا من خلال أداة معالجة الأحداث "navigate".

يمكن رصد عملية إرسال النموذج من خلال البحث عن السمة formData في NavigateEvent. في ما يلي مثال يحوّل أي عملية إرسال نموذج إلى عملية تبقى على الصفحة الحالية من خلال fetch():

navigation.addEventListener('navigate', navigateEvent => {
  if (navigateEvent.formData && navigateEvent.canIntercept) {
    // User submitted a POST form to a same-domain URL
    // (If canIntercept is false, the event is just informative:
    // you can't intercept this request, although you could
    // likely still call .preventDefault() to stop it completely).

    navigateEvent.intercept({
      // Since we don't update the DOM in this navigation,
      // don't allow focus or scrolling to reset:
      focusReset: 'manual',
      scroll: 'manual',
      handler() {
        await fetch(navigateEvent.destination.url, {
          method: 'POST',
          body: navigateEvent.formData,
        });
        // You could navigate again with {history: 'replace'} to change the URL here,
        // which might indicate "done"
      },
    });
  }
});

ما المفقود؟

على الرغم من الطبيعة المركزية لأداة معالجة الأحداث "navigate"، لا يؤدي مواصفات Navigation API الحالية إلى تنشيط "navigate" عند تحميل الصفحة لأول مرة. بالنسبة إلى المواقع الإلكترونية التي تستخدم العرض من جهة الخادم (SSR) لجميع الحالات، قد يكون هذا الإجراء مناسبًا، إذ يمكن أن يعرض الخادم الحالة الأولية الصحيحة، وهي أسرع طريقة لعرض المحتوى للمستخدمين. لكن المواقع الإلكترونية التي تستخدم رموزًا برمجية من جهة العميل لإنشاء صفحاتها قد تحتاج إلى إنشاء وظيفة إضافية لتهيئة الصفحة.

من الخيارات الأخرى التي تم اتّخاذها عن قصد في تصميم Navigation API هو أنّها تعمل فقط ضمن إطار واحد، أي الصفحة ذات المستوى الأعلى أو <iframe> معيّن واحد. يترتّب على ذلك عدد من الآثار المثيرة للاهتمام التي تم توثيقها بشكل أكبر في المواصفات، ولكن في الواقع، سيؤدي ذلك إلى تقليل الالتباس لدى المطوّرين. تتضمّن واجهة برمجة التطبيقات السابقة History API عددًا من حالات الاستخدام غير الواضحة، مثل إتاحة استخدام الإطارات، وتتعامل واجهة برمجة التطبيقات Navigation API المعاد تصميمها مع حالات الاستخدام هذه منذ البداية.

أخيرًا، لم يتم التوصّل بعد إلى توافق في الآراء بشأن تعديل قائمة الإدخالات التي تنقّل المستخدم بينها أو إعادة ترتيبها آليًا. هذا الموضوع قيد المناقشة حاليًا، ولكن أحد الخيارات المتاحة هو السماح بعمليات الحذف فقط، إما الإدخالات السابقة أو "جميع الإدخالات المستقبلية". سيسمح الأخير بالحالة المؤقتة. على سبيل المثال، بصفتي مطوّرًا، يمكنني إجراء ما يلي:

  • طرح سؤال على المستخدم من خلال الانتقال إلى عنوان URL أو حالة جديدة
  • السماح للمستخدم بإكمال عمله (أو الرجوع إلى الخلف)
  • إزالة إدخال في السجلّ عند إكمال مهمة

قد يكون ذلك مثاليًا للنوافذ المنبثقة المؤقتة أو الإعلانات البينية: عنوان URL الجديد هو شيء يمكن للمستخدم استخدام إيماءة "الرجوع" للخروج منه، ولكن لا يمكنه بعد ذلك الانتقال إلى "التالي" عن طريق الخطأ لفتحه مرة أخرى (لأنّ الإدخال تمت إزالته). لا يمكن إجراء ذلك باستخدام History API الحالية.

تجربة Navigation API

تتوفّر Navigation API في الإصدار 102 من Chrome بدون ميزات تجريبية. يمكنك أيضًا تجربة عرض توضيحي من إعداد دومينيك دينيكولا.

على الرغم من أنّ واجهة برمجة التطبيقات History API الكلاسيكية تبدو مباشرة، إلا أنّها غير محدّدة بشكل جيد وتتضمّن عددًا كبيرًا من المشاكل المتعلقة بالحالات الحدّية وطريقة تنفيذها بشكل مختلف على جميع المتصفحات. نأمل أن تقدّم ملاحظاتك حول واجهة برمجة التطبيقات الجديدة للتنقّل.

المراجع

الإقرارات

نشكر توماس شتاينر ودومينيك دينيكولا و"نيت تشابين" على مراجعة هذه المشاركة.