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

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

دعم المتصفح

  • 102
  • 102
  • x
  • x

المصدر

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

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

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

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

لاستخدام واجهة برمجة تطبيقات التنقل، ابدأ بإضافة أداة استماع "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. وهذا اقتراح صعب بشأن استخدام واجهات برمجة التطبيقات القديمة. إذا سبق لك كتابة التوجيه الخاص بـ 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. في 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 وعدًا (والذي يحدث تلقائيًا مع 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 سابق وحالة إلى عنوان جديد. ويشتمل ذلك على عدد من الفوائد المحتملة، بما في ذلك إمكانية الوصول: حيث يمكن أن تعرض المتصفحات بداية عملية التنقل أو نهايتها أو الإخفاق المحتمل لأي عملية تنقُّل. على سبيل المثال، ينشط Chrome مؤشر التحميل الأصلي ويسمح للمستخدم بالتفاعل مع زر الإيقاف. (لا يحدث ذلك حاليًا عندما ينتقل المستخدم من خلال زرَّي الرجوع/الأمام، ولكن سيتم إصلاح ذلك قريبًا).

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

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

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;

الحالة

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

المراجع

شكر وتقدير

نتوجّه بالشكر إلى توماس شتاينر ودومينيك دينيكولا و"نيت شابين" على مراجعة هذه المشاركة. صورة رئيسية من Unقلاش، بقلم Jeremy Zero