【前端路由】這可能是最容易理解的一篇了

隨著 ajax 的流行,異步數據請求體驗極具提升,用戶得以在不刷新瀏覽器的情況下進行頁面交互,而異步交互體驗的更高級版本就是 SPA —— 單頁應用。

單頁應用不僅僅是在頁面交互時無刷新,連頁面跳轉都是無刷新的,為了實現單頁應用,就有了前端路由

常用的兩種模式

類似于服務端路由解析對應的 url 路徑,返回對應的頁面/資源的方式,前端路由實現起來其實也很簡單,就是匹配不同的 url 路徑,進行解析,然后動態的渲染出區域 html 內容。

這樣自然 url 每次變化的時候,都會造成頁面的刷新。

那么在改變 url 的情況下,如何保證頁面的不刷新?

hash 模式

在 2014 年之前,大家是通過 hash 來實現路由,url hash 就是類似于:

https://www.xxx.com/#/login

這種 # 后面 hash 值的變化,并不會導致瀏覽器向服務器發出請求,瀏覽器不發出請求,也就不會刷新頁面。

為什么改變 hash 不刷新頁面?——URL的井號‘#’

‘#’ 代表網頁中的一個位置,它后面的字符,就是該位置的標識符,它只對瀏覽器有用,服務器不識別,因此 HTTP 請求不會包含 #

(想要請求 url 包含 # ,可使用 encodeURIComponent()
進行部分轉義)

改變 hash ,只會讓瀏覽器滾動到相應位置,不會重載網頁

每次 hash 值的變化,會觸發 hashchange 事件,通過window.onhashchange監聽該事件我們就可以檢測變化的 hash 值來做相應的頁面操作。

簡易實現

接下來我們用最簡單的代碼實現 hash 模式,僅為了解其思想(你可以直接復制到一個 html 上并通過靜態服務器如 http-server 查看):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Hash 路由</title>
  </head>
  <body>
    <ul>
      <li><a href="#red">紅色背景</a></li>
      <li><a href="#green">綠色背景</a></li>
      <li><a href="#grey">灰色背景</a></li>
    </ul>
    <script>
      function watchHash() {
        const hash = window.location.hash.slice(1) || '/';
        switch (hash) {
          case "red":
            document.body.style.background = "red";
            break;
          case "green":
            document.body.style.background = "green";
            break;
          case "grey":
            document.body.style.background = "grey";
            break;
        }
      }
      window.addEventListener("hashchange", watchHash, false);
      window.addEventListener("load", watchHash, false);
    </script>
  </body>
</html>

如何實現最基礎的前進后退?

這里我們簡單實現一下后退功能,前進思路類似:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Hash 路由</title>
  </head>
  <body>
    <ul>
      <li><a href="#red">紅色背景</a></li>
      <li><a href="#green">綠色背景</a></li>
      <li><a href="#grey">灰色背景</a></li>
    </ul>
    <button id="back" disabled="true">后退</button>
    <script>
      var isGoBack = false;
      const history = [];
      function watchHash() {
        const hash = window.location.hash.slice(1) || "/";
        // 防止后退時也記錄 hash
        if (!isGoBack && window.location.hash) {
          history.push(window.location.hash);
        }
        back.disabled = history.length > 0 ? false : true;
        console.log(history);
        switch (hash) {
          case "red":
            document.body.style.background = "red";
            break;
          case "green":
            document.body.style.background = "green";
            break;
          case "grey":
            document.body.style.background = "grey";
            break;
          default:
            document.body.style.background = "#fff";
        }
        isGoBack = false;
      }
      back.onclick = goBack;
      function goBack() {
        isGoBack = true;
        if (history.length > 0) {
          history.pop(1);
          window.location.hash = history[history.length - 1] || "";
        } else {
          back.disabled = true;
        }
      }
      window.addEventListener("load", watchHash, false);
      window.addEventListener("hashchange", watchHash, false);
    </script>
  </body>
</html>

思路就是通過一個數組記錄每次 hashchange 事件的 hash 值,點擊后退時取出上一次 hash 值覆蓋當前頁面的 hash。

需要注意的是需要區別當前 hash 是后退生成(后退時的 hash 變化不應記錄)的還是跳轉生成,避免重復記錄。

image

history 模式

可以看到,在早期 hash 模式雖然可以實現前端路由,但其后退前進操作就十分麻煩。

2014 年后,HTML5 引入了 History API,讓我們能夠快速訪問頁面歷史。

其中 history.pushState()history.replaceState() 方法,它們分別可以添加和修改歷史記錄條目,通過這兩個 API 可以改變 url 地址而無須重新加載頁面。

同時還有 popstate 事件:
通過window.onpopstate可以監聽在瀏覽器點擊后退、前進按鈕(或者在 JavaScript 中調用 history.back()、history.forward()、history.go() 方法) 觸發的 popstate 事件。

通過這些就能用另一種方式來實現前端路由了,但原理都是跟 hash 實現相同的。

用 history 實現上面 hash 代碼

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>History 路由</title>
  </head>
  <body>
    <ul id="ul">
      <li><a href="/red">紅色背景</a></li>
      <li><a href="/green">綠色背景</a></li>
      <li><a href="/grey">灰色背景</a></li>
    </ul>
    <script>
      const path = window.location.pathname;
      history.replaceState({ path: path }, null, path);
      ul.addEventListener("click", (e) => {
        if (e.target.tagName === "A") {
          e.preventDefault();
          const path = e.target.getAttribute("href");
          history.pushState({ path: path }, null, path);
          watchHistory(path);
        }
      });
      function watchHistory() {
        const path = window.location.pathname;
        switch (path) {
          case "/red":
            document.body.style.background = "red";
            break;
          case "/green":
            document.body.style.background = "green";
            break;
          case "/grey":
            document.body.style.background = "grey";
            break;
          default:
            document.body.style.background = "#fff";
        }
      }
      window.addEventListener("popstate", watchHistory, false);
    </script>
  </body>
</html>

用了 HTML5 的實現,單頁路由的 url 就不會多出一個 #,變得更加美觀。

但因為沒有 # 號,所以當用戶刷新頁面之類的操作時,瀏覽器還是會給服務器發送請求。

為了避免出現這種情況,history 模式需要服務器的支持,把所有路由都重定向到根頁面。

如何監聽 pushState 和 replaceState 的變化

經過理論及實踐我們知道 replaceState(),pushState() 兩個 API 不會觸發 popstate 監聽事件。

我們可以生成全新的 window 監聽事件監聽其變化:

function addListen(type) {
  const source = history[type];
  return function () {
    const event = new Event(type);
    event.arguments = arguments;
    window.dispatchEvent(event);
    return source.apply(this, arguments);
  };
}

history.pushState = addListen("pushState");
history.replaceState = addListen("replaceState");

window.addEventListener("replaceState", (e) => {
  console.log("我監聽了 replaceState");
});
window.addEventListener("pushState", (e) => {
  console.log("我監聽了 pushState");
});

兩種模式對比

  1. 無 # 的 history 模式更自然
  2. history 模式需要 IE9 以上,相對于 hash 模式的 IE8 兼容性差
  3. history 模式需服務器端配合,反過來說 hash 模式不支持服務端渲染

結語

以上就是前端路由的 hash 和 history 兩種模式的主要原理及實現思路了,如果你覺得不錯,別忘了點個贊??!


本文參考:

面試官: 你了解前端路由嗎?

阿里P7:你了解路由嗎?

[實踐系列]前端路由

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容