iT邦幫忙

2024 iThome 鐵人賽

DAY 4
0
JavaScript

可愛又迷人的 Web API系列 第 4

Day4. 認識 History API:SPA 的關鍵技術之一

  • 分享至 

  • xImage
  •  

在現代框架的單頁應用 (Single Page Application, SPA) 中,其中一個技術是在不重整頁面的情況下,讓使用者感受到順暢的頁面過渡效果,這個技術的背後就是 History API,今天要跟大家分享 History API 究竟幫我們做了哪些事情?他在 SPA 中又扮演了什麼角色?

History API 介紹

History API 可以讓開發者操作瀏覽器的歷史記錄。我們可以在不重整頁面的情況下更新 URL,還能讓瀏覽器的上一頁、下一頁正常運作。

History API 的特性包括:

  • 不用重整頁面即可更新網址
  • 可以正常使用瀏覽器的上一頁、下一頁功能
  • 每個頁面都能有獨立的網址

這些特性,其實就跟傳統的多頁面網站一樣,也就是說,透過使用 History API,我們可以建立像傳統多頁面網站的單頁應用,同時又保持單頁應用的速度和流暢性。

如何操作瀏覽器的歷史紀錄

History API 提供了幾個方法,讓開發者可以操作歷史紀錄

history.pushState()

這個方法讓我們向瀏覽歷史中增加一個新的狀態。它的語法如下:

history.pushState(state, title, url)
  • state: 一個 JavaScript 物件,包含與新歷史記錄相關的數據。
  • title:新頁面的標題。
  • url:新頁面的網址。

history.replaceState()

這個方法與 pushState() 類似,但他會替換當前的歷史記錄,而不是增加新的紀錄。

history.replaceState(state, title, url)

window.onpopstate

他會在使用者使用瀏覽器上一頁、下一頁功能時觸發,我們可以使用它來處理狀態的變化。

window.onpopstate = function(event) {
  // 處理狀態變化
  console.log(event.state);
}

實作 SPA 的頁面切換效果

1. 頁面內容

我放了三個連結,以及一個 id=content 的元素顯示對應頁面的內容

<nav>
  <a href="home">首頁</a>
  <a href="about">關於</a>
  <a href="contact">聯絡我們</a>
</nav>

<div id="content"></div>

2. 頁面跳轉時會做的事情

我們希望點不同連結,會顯示對應的內容,將這個動作包成一個函式 updateContent(),根據 page 參數顯示資料

function updateContent(page) {
  document.getElementById('content').innerHTML = `這是 ${page} 頁面的內容`;
}

3. 在相關事件呼叫 `updateContent()

呼叫 updateContent() 的時機點:

  1. 初始化頁面時:load()
  2. 使用者切換上下頁時:window.onpopstate
  3. 點擊連結時:navigateTo()
function navigateTo(page) {
  const state = { page: page };
  const title = page;
  const url = `./${page}`;
  history.pushState(state, title, url);
  updateContent(page);
}

window.onpopstate = function (event) {
  if (event.state) {
    updateContent(event.state.page);
  }
};

window.addEventListener('load', function () {
  const initialPage = window.location.pathname || 'home';
  updateContent(initialPage);
});

4. 不需重整就能切換頁面

querySelectorAll 取得所有 nav a 元素,並呼叫 navigateTo() 函式。而要實現不重整頁面的效果,記得用 e.preventDefault() 阻擋頁面預設的行為

document.querySelectorAll('nav a').forEach(link => {
  link.addEventListener('click', function (e) {
    // 阻擋頁面預設行為,防止頁面跳轉
    e.preventDefault();
    const page = this.getAttribute('href');
    navigateTo(page);
  });
});

navigateTo() 有用到 history.pushState(),我們在點擊連結時,會增加歷史瀏覽的紀錄,能有效切換上下頁。

function navigateTo(page) {
  const state = { page: page };
  const title = page;
  const url = `./${page}`;

  // 新增一筆歷史紀錄
  history.pushState(state, title, url);
  updateContent(page);
}

5. 頁面範例

https://mukiwu.github.io/web-api-demo/img/4-1.gif

深入的 SPA 應用

讓我們再更深入的做一些好玩的應用!

深度連結(Deep Link)與 SPA 中的網址處理

跟傳統的多頁面網站不同,SPA 的網址處理機制需要特別的設計。在傳統網站中,每個 URL 都對應一個實際頁面。然而,在 SPA 中,所有的內容都在一個頁面中動態載入,因此我們要手動處理 URL 的變化。

當使用者在瀏覽器中輸入一個 URL 時,我們需要確保應用能夠正確地載入對應的內容。這就是深度連結的核心概念。

前端需要監聽 load()事件,並根據 URL 載入對應的內容

function handleInitialLoad() {
  const path = window.location.pathname.substr(1);
  if (path) {
    navigateTo(path);
  } else {
    navigateTo('home');
  }
}

window.addEventListener('load', handleInitialLoad);

後端則需要協助修改伺服器設定,就跟大多數的 SPA 一樣,我們只會有一個入口頁面,所以要將路由指向同一份 HTML 檔案。以 Nginx 為例子,設定可能如下:

location / {
  try_files $uri $uri/ /index.html;
}

使用者輸入什麼 URL,伺服器都會根據設定返回 index.html,再從前端處理路由。

使用非同步載入頁面內容

這邊以我的部落格文章為例子,實作不用重新載入就能顯示文章內容。

https://mukiwu.github.io/web-api-demo/img/4-1.gif

一樣先處理 HTML 元素,連結是我部落格文章的實際 URL,我去爬文章內容,再將它渲染到 <div id="content"></div>

<nav>
  <a href="cdn-tailwindcss-vscode-enable-tailwindcss-intellisense/">文章一</a>
  <a href="introduction-singleton-design-pattern/">文章二</a>
  <a href="quill-react-ant-design-and-upload-image/">文章三</a>
</nav>

<div id="content"></div>

我修改了 navigateTo() 函式,用 async/await 語法處理非同步操作,再用 fetch() 取得我的文章內容

async function navigateTo(page) {
  showLoading();
  try {
    const res = await fetch(`https://muki.tw/${page}`);
    if (!res.ok) {
      throw new Error(`HTTP error! status: ${res.status}`);
    }
    const htmlContent = await res.text();
    const extractedContent = extractContent(htmlContent);
    history.pushState({ page, content: extractedContent }, page, `/${page}`);
    updateContent(extractedContent);
  } catch (error) {
    console.error('Fetch error:', error);
    updateContent('載入失敗,請稍後再試。');
  } finally {
    hideLoading();
  }
}

res.text() 會顯示從整份 HTML 文件,但我只想要文章內容,所以用 extractContent() 來處理最後要顯示的 HTML

function extractContent(htmlString) {
  const parser = new DOMParser();
  const doc = parser.parseFromString(htmlString, 'text/html');

  // 使用屬性選擇器來選擇目標 div
  const targetDiv = doc.querySelector('div[class*="min-h-[calc(100vh-70px)]"]');

  if (targetDiv) {
    return targetDiv.innerHTML;
  } else {
    // 如果找不到指定的 div,嘗試獲取 body 內容
    const bodyContent = doc.body.innerHTML;
    return bodyContent || '找不到指定的內容';
  }
}

載入文章時,可以適時地增添 loading 效果,讓體驗更順暢

function showLoading() {
  const loadingElement = document.createElement('div');
  loadingElement.className = 'loading';
  contentElement.appendChild(loadingElement);
}

function hideLoading() {
  const loadingElement = document.querySelector('.loading');
  if (loadingElement) {
    loadingElement.remove();
  }
}

以上雖是一個簡易的實作,但我們整合了 History API、非同步以及 loading 效果處理,讓大家了解 History API 可以有怎樣的運用,也許未來在使用現代框架時,可以更瞭解路由端的設計原理與實作方式。

小結

我們能使用 History API 實現頁面切換,同時支援深度連結,還能保持瀏覽器歷史的完整性。但在使用上,也需要注意一些潛在問題:

  1. 瀏覽器兼容性:雖然大部分的瀏覽器都支持 History API,但使用這些 Web API 時仍須考慮支援問題。
  2. SEO :SPA 最大的詬病就是對搜尋引擎不友好,這部分也要採取對應的措施。
  3. 需設定伺服器:要實現 SPA,需要前後端合作,後端也要協助設定伺服器,才能讓使用者輸入 URL 時,正確渲染畫面。

以上就是 History API 的介紹,有任何問題都歡迎留言討論唷。


上一篇
Day3. 用 Drag and Drop API 拖曳網頁元素
下一篇
Day5. 探索 Fullscreen API:從基礎到應用場景
系列文
可愛又迷人的 Web API20
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言