توحيد التوجيه من جهة العميل من خلال واجهة برمجة تطبيقات جديدة كليًا تعمل على إجراء إصلاحات كاملة لإنشاء تطبيقات من صفحة واحدة.
يتم تعريف تطبيقات الصفحة الواحدة، أو 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
وعدًا (والذي يحدث تلقائيًا مع الوظائف غير المتزامنة)، يحدد هذا الوعد المتصفح المدة التي يستغرقها التنقل وما إذا كان ناجحًا أم لا.
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 السابقة على عدد من الحالات الحدّية المربكة، مثل دعم الإطارات، كما تتعامل واجهة واجهة برمجة تطبيقات التنقّل المُعاد تصميمها مع الحالات الحدّية هذه من البداية.
أخيرًا، لم يحدث توافق حتى الآن حول التعديل أو إعادة ترتيب قائمة الإدخالات التي انتقل المستخدم من خلالها آليًا. هذا القسم قيد المناقشة حاليًا، ولكن قد يكون أحد الخيارات السماح بعمليات الحذف فقط: إما الإدخالات السابقة أو "جميع الإدخالات المستقبلية". يسمح الخيار الأخير بالحالة المؤقتة. على سبيل المثال، بصفتي مطوِّرًا، يمكنني:
- يمكنك طرح سؤال على المستخدم من خلال الانتقال إلى حالة أو عنوان URL جديد.
- السماح للمستخدم بإكمال عمله (أو الرجوع)
- إزالة إدخال سجلّ عند اكتمال مهمة
وقد يكون هذا مثاليًا للنماذج أو الصفحات البينية المؤقتة: فعنوان URL الجديد هو عنوان يمكن للمستخدم استخدام إيماءة "الرجوع" للخروج منه، ولكن بعد ذلك لا يمكنه الانتقال بدون قصد لفتحه مرة أخرى (لأنه تمت إزالة الإدخال). لا يمكن ذلك باستخدام History API الحالي.
تجربة واجهة برمجة تطبيقات التنقّل
تتوفر واجهة برمجة تطبيقات التنقل في Chrome 102 بدون علامات. يمكنك أيضًا تجربة عرض توضيحي من تأليف دومينيك دينيولا.
على الرغم من أنّ واجهة History API الكلاسيكية تبدو واضحة، فهي غير مُعرَّفة جيدًا وتضم عددًا كبيرًا من المشاكل بالقرب من بعض الحالات وكيفية تنفيذها بشكل مختلف على مختلف المتصفّحات. نأمل أن تفكر في تقديم تعليقات حول واجهة برمجة تطبيقات التنقل الجديدة.
المراجع
شكر وتقدير
نتوجّه بالشكر إلى توماس شتاينر ودومينيك دينيكولا و"نيت شابين" على مراجعة هذه المشاركة. صورة رئيسية من Unقلاش، بقلم Jeremy Zero