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

يتم توحيد عملية التوجيه من جهة العميل من خلال واجهة برمجة تطبيقات جديدة تتضمّن تغييرًا كاملاً في تصميم تطبيقات الصفحة الواحدة.

دعم المتصفح

  • 102
  • 102
  • x
  • x

المصدر

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

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

تصف هذه المشاركة واجهة برمجة تطبيقات التنقل على مستوى عالٍ. إذا كنت ترغب في قراءة الاقتراح الفني، فراجع مسودة التقرير في مستودع WICG.

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

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

يتم تمرير 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 الذي يختلف عن عنوان 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 وعدًا (يحدث ذلك تلقائيًا مع async functions)، يوضِّح هذا الوعد للمتصفِّح بالمدة التي يستغرقها التنقّل وما إذا كان ناجحًا.

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);
      },
    });
  }
});

وبالتالي، تقدّم واجهة برمجة التطبيقات هذه مفهومًا دلالياً يمكن للمتصفّح فهمه: يحدث التنقّل في "SPA" حاليًا - بمرور الوقت - مع تغيير المستند من عنوان 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())، ستُنشِّط واجهة برمجة تطبيقات التنقّل "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 مفيدًا لأحد المطوّرين لأنّ واجهة برمجة تطبيقات التنقّل تسمح لك بانتقال المستخدم مباشرةً إلى إدخال باستخدام مفتاح مطابق. ويمكنك الاحتفاظ بها، حتى في حالات الإدخالات الأخرى، للتنقّل بسهولة بين الصفحات.

// 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;

الحالة

تعرِض واجهة برمجة تطبيقات التنقّل مفهوم "الحالة"، وهي معلومات يقدِّمها المطوِّر ويتم تخزينها بشكلٍ مستمر في إدخال السجلّ الحالي، ولكن لا يمكن للمستخدم رؤيتها مباشرةً. إنّ واجهة برمجة التطبيقات هذه مشابهة إلى حدّ كبير لـ "history.state" في واجهة برمجة التطبيقات History 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() والأصدقاء، بالإضافة إلى الطريقتين pushState() وreplaceState() في History API.

تعرض الطريقة 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 نوعًا خاصًا من التنقل، ويمكن لواجهة برمجة تطبيقات التنقل اعتراضه. على الرغم من أنّه يتضمّن حمولة إضافية، يتولّى مستمع "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"، لا تؤدي مواصفات واجهة برمجة تطبيقات التنقّل الحالية إلى تشغيل "navigate" عند التحميل الأول للصفحة. وبالنسبة إلى المواقع الإلكترونية التي تستخدم العرض من جهة الخادم (SSR) لجميع الحالات، قد يكون ذلك جيدًا، إذ قد يعرض الخادم الحالة الأولية الصحيحة، وهي أسرع طريقة لعرض المحتوى للمستخدمين. ولكن المواقع الإلكترونية التي تستفيد من الرمز البرمجي من جهة العميل لإنشاء صفحاتها قد تحتاج إلى إنشاء وظيفة إضافية لإعداد صفحاتها.

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

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

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

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

تجربة واجهة برمجة التطبيقات للتنقل

تتوفّر واجهة برمجة تطبيقات التنقّل في Chrome 102 بدون علامات. يمكنك أيضًا تجربة عرض توضيحي من إنشاء دومينيك دينيكولا.

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

المراجع

شكر وتقدير

شكرًا لكل من توماس شتاينر ودومينيك دينيكولا و"نيت شابين" على مراجعة هذه المشاركة. صورة رئيسية من قناة Unsplash من تصميم جيريمي زيرو