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

توحيد التوجيه من جهة العميل من خلال واجهة برمجة تطبيقات جديدة تمامًا تُجري تغييرات جذرية على إنشاء تطبيقات الصفحة الواحدة

دعم المتصفح

  • Chrome: 102
  • Edge: 102.
  • Firefox: غير متوافق
  • Safari: غير متاح.

المصدر

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

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

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

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

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

يتمّ تمرير 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));

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

بالإضافة إلى ذلك، لا تعالج الخطوات أعلاه عملية التنقّل للخلف أو للأمام. فِيهْ حَدَثْ تَانِي لِلْخِدْمَة دَهْ، "popstate".

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

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

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

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

canIntercept
إذا كان هذا الحقل غير صحيح، لا يمكنك اعتراض عملية التنقّل. ولا يمكن اعتراض عمليات الانتقال من نطاقات أخرى وعمليّات اجتياز جميع المستندات.
destination.url
ربما تكون هذه هي المعلومات الأكثر أهمية التي يجب أخذها في الاعتبار عند التعامل مع التنقّل.
hashChange
قيمة صحيحة إذا كان التنقّل في المستند نفسه، وكانت التجزئة هي الجزء الوحيد من عنوان URL الذي يختلف عن عنوان URL الحالي. في SPA الحديث، يجب أن تكون التجزئة للربط بأجزاء مختلفة من المستند الحالي. وبالتالي، إذا كان 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);
      },
    });
  }
});

وهكذا، تقدّم واجهة برمجة التطبيقات هذه مفهومًا دلاليًا يفهمه المتصفّح: يحدث حاليًا انتقال إلى تطبيق متعدّد الصفحات (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 مفيدًا لأنّ 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() وfriends، بالإضافة إلى طريقتَي pushState() وreplaceState() في History API.

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

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

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

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

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

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

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

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

تجربة Navigation API

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

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

المراجع

الشكر والتقدير

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