مسیریابی مدرن سمت مشتری: Navigation API

استانداردسازی مسیریابی سمت کلاینت از طریق یک API کاملاً جدید که ساخت برنامه‌های تک صفحه‌ای را کاملاً متحول می‌کند.

جیک آرچیبالد
Jake Archibald

Browser Support

  • کروم: ۱۰۲.
  • لبه: ۱۰۲.
  • فایرفاکس: ۱۴۷.
  • سافاری: ۲۶.۲.

Source

برنامه‌های تک صفحه‌ای یا SPAها با یک ویژگی اصلی تعریف می‌شوند: بازنویسی پویای محتوای آنها همزمان با تعامل کاربر با سایت، به جای روش پیش‌فرض بارگذاری صفحات کاملاً جدید از سرور.

اگرچه SPAها توانسته‌اند این ویژگی را از طریق History API (یا در موارد محدود، با تنظیم قسمت #hash سایت) برای شما فراهم کنند، اما این یک API قدیمی است که مدت‌ها قبل از اینکه SPAها رایج شوند، توسعه یافته است - و وب به شدت به دنبال یک رویکرد کاملاً جدید است. Navigation API یک API پیشنهادی است که این فضا را کاملاً متحول می‌کند، نه اینکه صرفاً سعی در اصلاح نواقص History API داشته باشد. (به عنوان مثال، Scroll Restoration به جای تلاش برای بازسازی History API، آن را اصلاح کرد.)

این پست، API ناوبری را در سطح بالایی شرح می‌دهد. برای مطالعه پیشنهاد فنی، به پیش‌نویس گزارش در مخزن WICG مراجعه کنید.

مثال استفاده

برای استفاده از Navigation API، با اضافه کردن یک شنونده "navigate" به شیء navigation سراسری شروع کنید. این رویداد اساساً متمرکز است: برای همه انواع ناوبری‌ها، چه کاربر عملی را انجام داده باشد (مانند کلیک روی یک لینک، ارسال فرم یا رفتن به عقب و جلو) و چه زمانی که ناوبری به صورت برنامه‌نویسی شده (یعنی از طریق کد سایت شما) فعال شود، فعال می‌شود. در بیشتر موارد، به کد شما اجازه می‌دهد تا رفتار پیش‌فرض مرورگر را برای آن عمل نادیده بگیرد. برای SPAها، این احتمالاً به معنای نگه داشتن کاربر در همان صفحه و بارگیری یا تغییر محتوای سایت است.

یک NavigateEvent به شنونده‌ی "navigate" ارسال می‌شود که حاوی اطلاعاتی در مورد ناوبری، مانند URL مقصد، است و به شما امکان می‌دهد تا به ناوبری در یک مکان متمرکز پاسخ دهید. یک شنونده‌ی "navigate" پایه می‌تواند به شکل زیر باشد:

navigation.addEventListener('navigate', navigateEvent => {
  // Exit early if this navigation shouldn't be intercepted.
  // The properties to look at are discussed later in the article.
  if (shouldNotIntercept(navigateEvent)) return;

  const url = new URL(navigateEvent.destination.url);

  if (url.pathname === '/') {
    navigateEvent.intercept({handler: loadIndexPage});
  } else if (url.pathname === '/cats/') {
    navigateEvent.intercept({handler: loadCatsPage});
  }
});

شما می‌توانید به یکی از دو روش زیر با ناوبری (navigation) برخورد کنید:

  • فراخوانی intercept({ handler }) (همانطور که در بالا توضیح داده شد) برای مدیریت ناوبری.
  • فراخوانی preventDefault() که می‌تواند ناوبری را به طور کامل لغو کند.

این مثال متد intercept() را در رویداد فراخوانی می‌کند. مرورگر تابع callback handler شما را فراخوانی می‌کند که باید وضعیت بعدی سایت شما را پیکربندی کند. این کار یک شیء transition به navigation.transition ایجاد می‌کند که سایر کدها می‌توانند از آن برای ردیابی پیشرفت navigation استفاده کنند.

هر دو intercept() و preventDefault() معمولاً مجاز هستند، اما مواردی وجود دارد که نمی‌توان آنها را فراخوانی کرد. اگر ناوبری از نوع ناوبری بین مبدا (cross-origin) باشد، نمی‌توانید ناوبری را از طریق intercept() مدیریت کنید. و اگر کاربر دکمه‌های Back یا Forward را در مرورگر خود فشار می‌دهد، نمی‌توانید ناوبری را از طریق preventDefault() لغو کنید. نباید بتوانید کاربران خود را در سایت خود به دام بیندازید. (این موضوع در GitHub در حال بحث است.)

حتی اگر نتوانید خودِ ناوبری را متوقف یا قطع کنید، رویداد "navigate" همچنان اجرا می‌شود. این رویداد آموزنده است، بنابراین کد شما می‌تواند، برای مثال، یک رویداد Analytics را ثبت کند تا نشان دهد که کاربر در حال خروج از سایت شماست.

چرا یک رویداد دیگر به پلتفرم اضافه کنیم؟

یک شنونده رویداد "navigate" مدیریت تغییرات URL را در داخل یک SPA متمرکز می‌کند. این کار با استفاده از APIهای قدیمی‌تر دشوار است. اگر تا به حال مسیریابی 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));

این خوب است، اما جامع نیست. لینک‌ها ممکن است در صفحه شما بیایند و بروند، و آنها تنها راهی نیستند که کاربران می‌توانند از طریق آنها در صفحات پیمایش کنند. به عنوان مثال، آنها ممکن است یک فرم ارسال کنند یا حتی از یک نقشه تصویری استفاده کنند. صفحه شما ممکن است با این موارد سر و کار داشته باشد، اما مجموعه‌ای طولانی از امکانات وجود دارد که می‌توان آنها را ساده کرد - چیزی که API ناوبری جدید به آن دست یافته است.

علاوه بر این، مورد بالا پیمایش به عقب/جلو را مدیریت نمی‌کند. رویداد دیگری برای آن وجود دارد، "popstate" .

شخصاً، اغلب احساس می‌کنم که History API می‌تواند به نوعی به این امکانات کمک کند. با این حال، در واقع فقط دو بخش سطحی دارد: پاسخ دادن در صورتی که کاربر در مرورگر خود دکمه‌های Back یا Forward را فشار دهد، به علاوه ارسال و جایگزینی URLها. این API هیچ شباهتی به "navigate" ندارد، مگر اینکه به صورت دستی، همانطور که در بالا نشان داده شد، برای رویدادهای کلیک، شنونده تنظیم کنید.

تصمیم‌گیری در مورد نحوه مدیریت ناوبری

navigateEvent شامل اطلاعات زیادی در مورد ناوبری است که می‌توانید از آنها برای تصمیم‌گیری در مورد نحوه برخورد با یک ناوبری خاص استفاده کنید.

خواص کلیدی عبارتند از:

canIntercept
اگر این مقدار نادرست باشد، نمی‌توانید ناوبری را رهگیری کنید. ناوبری‌های بین مبدا و پیمایش‌های بین اسناد را نمی‌توان رهگیری کرد.
destination.url
احتمالاً مهمترین اطلاعاتی که هنگام مدیریت ناوبری باید در نظر بگیرید.
hashChange
اگر ناوبری same-document باشد و هش تنها بخشی از URL باشد که با URL فعلی متفاوت است، مقدار صحیح (true) را برمی‌گرداند. در SPA های مدرن، هش باید برای لینک دادن به بخش‌های مختلف سند فعلی باشد. بنابراین، اگر hashChange درست باشد، احتمالاً نیازی به قطع کردن این ناوبری ندارید.
downloadRequest
اگر این درست باشد، ناوبری توسط لینکی با ویژگی download آغاز شده است. در بیشتر موارد، نیازی به قطع کردن این مورد نیست.
formData
اگر این مقدار null نباشد، پس این ناوبری بخشی از ارسال فرم POST است. هنگام مدیریت ناوبری، حتماً این نکته را در نظر بگیرید. اگر فقط می‌خواهید ناوبری‌های GET را مدیریت کنید، از رهگیری ناوبری‌هایی که formData در آن‌ها null نیست، خودداری کنید. مثال مربوط به مدیریت ارسال فرم را بعداً در مقاله مشاهده کنید.
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 شما یک promise را برگرداند (که به طور خودکار با توابع async اتفاق می‌افتد)، آن promise به مرورگر می‌گوید که پیمایش چقدر طول می‌کشد و آیا موفقیت‌آمیز است یا خیر.

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

به این ترتیب، این API یک مفهوم معنایی را معرفی می‌کند که مرورگر آن را درک می‌کند: یک پیمایش SPA در حال حاضر در حال وقوع است، به مرور زمان، سند را از یک URL و حالت قبلی به یک URL جدید تغییر می‌دهد. این امر مزایای بالقوه‌ای از جمله قابلیت دسترسی دارد: مرورگرها می‌توانند ابتدا، انتها یا شکست احتمالی یک پیمایش را نشان دهند. به عنوان مثال، کروم نشانگر بارگذاری بومی خود را فعال می‌کند و به کاربر اجازه می‌دهد تا با دکمه توقف تعامل داشته باشد. (این اتفاق در حال حاضر زمانی که کاربر از طریق دکمه‌های عقب/جلو پیمایش می‌کند، رخ نمی‌دهد، اما این مشکل به زودی برطرف خواهد شد .)

هنگام رهگیری ناوبری‌ها، 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 است. برای اطلاعات بیشتر به Abortable fetch مراجعه کنید.

خلاصه کلام این است که اساساً یک شیء فراهم می‌کند که وقتی باید کار خود را متوقف کنید، یک رویداد را فعال می‌کند. نکته قابل توجه این است که می‌توانید یک 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 نشان داده شده است (بیت بعد از # )، یا تنظیم مجدد پیمایش به بالای صفحه.

برای بارگذاری مجدد و پیمایش، این به معنای بازگرداندن موقعیت اسکرول به جایی است که آخرین بار این ورودی تاریخچه نمایش داده شده بود.

به طور پیش‌فرض، این اتفاق زمانی می‌افتد که promise برگردانده شده توسط handler شما resolve شود، اما اگر منطقی باشد که زودتر اسکرول کنید، می‌توانید 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() {
    // …
  },
});

مدیریت فوکوس

زمانی که promise برگردانده شده توسط handler شما اجرا شود، مرورگر روی اولین عنصری که ویژگی autofocus روی آن تنظیم شده است فوکوس می‌کند، یا اگر هیچ عنصری این ویژگی را نداشته باشد، روی عنصر <body> فوکوس می‌کند.

شما می‌توانید با تنظیم گزینه focusReset از intercept() روی "manual" از این رفتار جلوگیری کنید:

navigateEvent.intercept({
  focusReset: 'manual',
  async handler() {
    // …
  },
});

رویدادهای موفقیت و شکست

وقتی که تابع intercept() فراخوانی می‌شود، یکی از دو اتفاق زیر رخ می‌دهد:

  • اگر Promise برگردانده شده برآورده شود (یا شما intercept() را فراخوانی نکرده باشید)، API ناوبری، "navigatesuccess" را با یک Event اجرا می‌کند.
  • اگر Promise برگردانده شده رد شود، API با یک ErrorEvent "navigateerror" را اجرا می‌کند.

این رویدادها به کد شما اجازه می‌دهند تا موفقیت یا شکست را به صورت متمرکز مدیریت کند. برای مثال، می‌توانید با پنهان کردن یک نشانگر پیشرفت که قبلاً نمایش داده شده است، مانند این، با موفقیت برخورد کنید:

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 مفید است زیرا 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;

ایالت

API ناوبری مفهومی از "state" را ارائه می‌دهد که اطلاعات ارائه شده توسط توسعه‌دهنده است که به طور مداوم در ورودی تاریخچه فعلی ذخیره می‌شود، اما مستقیماً برای کاربر قابل مشاهده نیست. این بسیار شبیه به history.state در API تاریخچه است، اما نسبت به آن بهبود یافته است.

در 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

روش صحیح تنظیم وضعیت (state) در حین پیمایش اسکریپت است:

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

به‌روزرسانی وضعیت به صورت همزمان

به طور کلی، بهتر است state را به صورت غیرهمزمان از طریق navigation.reload({state: newState}) به‌روزرسانی کنید، سپس شنونده "navigate" شما می‌تواند آن state را اعمال کند. با این حال، گاهی اوقات تغییر state تا زمانی که کد شما از آن مطلع می‌شود، به طور کامل اعمال شده است، مانند زمانی که کاربر یک عنصر <details> را تغییر می‌دهد، یا کاربر وضعیت یک ورودی فرم را تغییر می‌دهد. در این موارد، ممکن است بخواهید state را به‌روزرسانی کنید تا این تغییرات از طریق بارگذاری مجدد و پیمایش حفظ شوند. این کار با استفاده از updateCurrentEntry() امکان‌پذیر است:

navigation.updateCurrentEntry({state: newState});

همچنین رویدادی برای شنیدن در مورد این تغییر وجود دارد:

navigation.addEventListener('currententrychange', () => {
  console.log(navigation.currentEntry.getState());
});

اما اگر متوجه شدید که در حال واکنش به تغییرات وضعیت در "currententrychange" هستید، ممکن است کد تغییر وضعیت خود را بین رویداد "navigate" و رویداد "currententrychange" تقسیم یا حتی کپی کنید، در حالی که navigation.reload({state: newState}) به شما امکان می‌دهد آن را در یک مکان مدیریت کنید.

پارامترهای وضعیت در مقابل پارامترهای URL

از آنجا که state می‌تواند یک شیء ساختاریافته باشد، استفاده از آن برای تمام stateهای برنامه وسوسه‌انگیز است. با این حال، در بسیاری از موارد بهتر است آن state را در URL ذخیره کنید.

اگر انتظار دارید که وضعیت (state) هنگام اشتراک‌گذاری URL توسط کاربر با کاربر دیگر حفظ شود، آن را در URL ذخیره کنید. در غیر این صورت، شیء وضعیت (state object) گزینه بهتری است.

دسترسی به همه ورودی‌ها

با این حال، «ورودی فعلی» همه چیز نیست. این API همچنین راهی برای دسترسی به کل لیست ورودی‌هایی که کاربر هنگام استفاده از سایت شما از طریق فراخوانی navigation.entries() پیمایش کرده است، فراهم می‌کند که یک آرایه snapshot از ورودی‌ها را برمی‌گرداند. این می‌تواند برای مثال، نمایش یک رابط کاربری متفاوت بر اساس نحوه پیمایش کاربر به یک صفحه خاص یا فقط برای مشاهده URL های قبلی یا حالت‌های آنها استفاده شود. این کار با API History فعلی غیرممکن است.

همچنین می‌توانید به رویداد "dispose" در NavigationHistoryEntry های تکی گوش دهید، که وقتی ورودی دیگر بخشی از تاریخچه مرورگر نیست، اجرا می‌شود. این می‌تواند به عنوان بخشی از پاکسازی عمومی اتفاق بیفتد، اما هنگام پیمایش نیز اتفاق می‌افتد. به عنوان مثال، اگر 10 مکان به عقب برگردید و سپس به جلو حرکت کنید، آن 10 ورودی تاریخچه دفع می‌شوند.

مثال‌ها

همانطور که در بالا ذکر شد، رویداد "navigate" برای همه انواع ناوبری فعال می‌شود. (در واقع یک پیوست طولانی در مشخصات همه انواع ممکن وجود دارد.)

در حالی که برای بسیاری از سایت‌ها، رایج‌ترین حالت زمانی است که کاربر روی <a href="..."> کلیک می‌کند، دو نوع ناوبری قابل توجه و پیچیده‌تر وجود دارد که ارزش پوشش دادن را دارند.

ناوبری برنامه‌ریزی‌شده

اولین مورد، ناوبری برنامه‌نویسی شده است که در آن ناوبری توسط فراخوانی یک متد در کد سمت کلاینت شما ایجاد می‌شود.

شما می‌توانید از هر جایی در کد خود، تابع navigation.navigate('/another_page') را برای ایجاد یک ناوبری فراخوانی کنید. این کار توسط شنونده رویداد متمرکز ثبت شده در شنونده "navigate" مدیریت می‌شود و شنونده متمرکز شما به صورت همزمان فراخوانی خواهد شد.

این به عنوان یک تجمیع بهبود یافته از متدهای قدیمی‌تر مانند location.assign() و friends، به علاوه متدهای pushState() و replaceState() از API تاریخچه در نظر گرفته شده است.

متد navigation.navigate() یک شیء را برمی‌گرداند که شامل دو نمونه Promise در { committed, finished } . این به فراخوانی‌کننده اجازه می‌دهد تا زمانی که انتقال "committed" (URL قابل مشاهده تغییر کرده و یک NavigationHistoryEntry جدید در دسترس باشد) یا "finished" (تمام promise های برگردانده شده توسط intercept({ handler }) کامل شوند - یا به دلیل شکست یا به دلیل پیش‌دستی توسط navigation دیگری رد شوند) منتظر بماند.

متد navigate همچنین یک شیء options دارد که می‌توانید در آن موارد زیر را تنظیم کنید:

  • state : وضعیت ورودی جدید تاریخچه، که از طریق متد .getState() در NavigationHistoryEntry قابل دسترسی است.
  • history : که می‌توان آن را روی "replace" تنظیم کرد تا ورودی تاریخچه فعلی را جایگزین کند.
  • info : یک شیء برای ارسال به رویداد navigation از طریق navigateEvent.info .

به طور خاص، info می‌تواند برای مثال برای نشان دادن یک انیمیشن خاص که باعث نمایش صفحه بعدی می‌شود، مفید باشد. (گزینه دیگر می‌تواند تنظیم یک متغیر سراسری یا گنجاندن آن به عنوان بخشی از #hash باشد. هر دو گزینه کمی نامناسب هستند.) نکته قابل توجه این است که اگر کاربر بعداً از طریق دکمه‌های Back و Forward خود پیمایش کند، این info دوباره پخش نخواهند شد. در واقع، در این موارد همیشه undefined خواهد بود.

نسخه نمایشی باز شدن از چپ یا راست

navigation همچنین تعدادی متد navigation دیگر نیز دارد که همگی یک شیء حاوی { committed, finished } را برمی‌گردانند. من قبلاً traverseTo() (که key می‌پذیرد که نشان‌دهنده یک ورودی خاص در تاریخچه کاربر است) و navigate() را ذکر کرده‌ام. همچنین شامل back() ، forward() و reload() نیز می‌شود. همه این متدها - درست مانند navigate() - توسط شنونده رویداد متمرکز "navigate" مدیریت می‌شوند.

ارسال فرم

ثانیاً، ارسال <form> در HTML از طریق POST نوع خاصی از ناوبری است و API ناوبری می‌تواند آن را رهگیری کند. اگرچه شامل یک payload اضافی است، اما ناوبری همچنان به صورت مرکزی توسط شنونده "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" ، مشخصات فعلی API ناوبری، "navigate" در اولین بارگذاری صفحه فعال نمی‌کند. و برای سایت‌هایی که از رندر سمت سرور (SSR) برای همه حالت‌ها استفاده می‌کنند، این ممکن است خوب باشد - سرور شما می‌تواند حالت اولیه صحیح را برگرداند، که سریع‌ترین راه برای رساندن محتوا به کاربران شماست. اما سایت‌هایی که از کد سمت کلاینت برای ایجاد صفحات خود استفاده می‌کنند، ممکن است نیاز به ایجاد یک تابع اضافی برای مقداردهی اولیه صفحه خود داشته باشند.

یکی دیگر از انتخاب‌های عمدی طراحی API ناوبری این است که فقط در یک فریم واحد - یعنی صفحه سطح بالا یا یک <iframe> خاص - عمل می‌کند. این موضوع پیامدهای جالبی دارد که بیشتر در مشخصات مستند شده‌اند ، اما در عمل، سردرگمی توسعه‌دهنده را کاهش می‌دهد. API تاریخچه قبلی تعدادی موارد حاشیه‌ای گیج‌کننده، مانند پشتیبانی از فریم‌ها، دارد و API ناوبری بازطراحی‌شده این موارد حاشیه‌ای را از همان ابتدا مدیریت می‌کند.

در نهایت، هنوز اجماعی در مورد تغییر یا تنظیم مجدد فهرست ورودی‌هایی که کاربر پیمایش کرده است، از طریق برنامه‌نویسی وجود ندارد. این موضوع در حال حاضر در دست بررسی است، اما یک گزینه می‌تواند این باشد که فقط حذف‌ها مجاز باشند: یا ورودی‌های تاریخی یا «تمام ورودی‌های آینده». مورد دوم حالت موقت را مجاز می‌کند. به عنوان مثال، من به عنوان یک توسعه‌دهنده می‌توانم:

  • با رفتن به URL یا وضعیت جدید، از کاربر سؤالی بپرسید
  • به کاربر اجازه دهید کار خود را تکمیل کند (یا به عقب برگردد)
  • حذف یک ورودی تاریخچه پس از اتمام یک کار

این می‌تواند برای ماژول‌های موقت یا بینابینی‌ها عالی باشد: URL جدید چیزی است که کاربر می‌تواند از حرکت برگشت برای خروج از آن استفاده کند، اما نمی‌تواند به‌طور تصادفی برای باز کردن دوباره آن به جلو برود (زیرا ورودی حذف شده است). این کار با API History فعلی امکان‌پذیر نیست.

API ناوبری را امتحان کنید

API ناوبری در کروم ۱۰۲ بدون فلگ (flag) در دسترس است. همچنین می‌توانید نسخه آزمایشی (دمو) ساخته‌ی دومنیک دنیکولا (Domenic Denicola) را امتحان کنید .

اگرچه رابط برنامه‌نویسی History API کلاسیک ساده به نظر می‌رسد، اما خیلی خوب تعریف نشده است و مشکلات زیادی در مورد موارد فرعی و نحوه پیاده‌سازی متفاوت آن در مرورگرهای مختلف دارد. امیدواریم که در مورد رابط برنامه‌نویسی Navigation API جدید بازخورد خود را ارائه دهید.

منابع

تقدیرنامه‌ها

با تشکر از توماس اشتاینر ، دومنیک دنیکولا و نیت چاپین برای بررسی این پست.