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

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

توافق المتصفّح

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

المصدر

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

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

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

ثانيًا، يُعدّ إرسال <form> HTML من خلال 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 الكلاسيكية تبدو بسيطة، إلا أنّها غير محدّدة جيدًا وتتضمن عددًا كبيرًا من المشاكل المتعلّقة بالحالات الشاذة وكيفية تنفيذها بشكل مختلف في جميع المتصفّحات. نرجو منك تقديم ملاحظاتك حول واجهة برمجة التطبيقات Navigation API الجديدة.

المراجع

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

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