先總結

前端 router 有兩種實現模式,一種使用 Hash 實現,另一種使用 HTML5 History API 實現。

  • 基於 hash 的路由:。當使用者在應用程式中切換路由(例如從 #/home 切換到 #/article)時,也不會向伺服器發送任何請求,瀏覽器僅根據 hash 的變化來變更頁面內容,

  • 基於 HTML5 History API 的路由:當使用者首次訪問某個路由(例如 http://example.com/about)時,瀏覽器會向伺服器發送請求,以獲取實際的靜態資源(例如 about.html)。但一旦頁面加載完成並切換到其他路由時(例如從 /about 切換到 /contact),瀏覽器不會再次向伺服器發送請求,而是僅根據 HTML5 History API 的變化來變更 URL 和頁面內容。

  • 相同點:

  • 不同點:

使用 Hash 實現 router

  • 原理:
  • 在內部傳遞的實際 URL 之前使用了一個哈希字元(#)。由於 URL 從未傳送到伺服器,所以不需要在伺服器層面上進行任何處理。
<!DOCTYPE html>
<html>
  <body>
    <div id="content"></div>
    <a href="#/">Home</a>
    <a href="#/article">Article</a>
    <div>window.location.hash =
        <span id="currentHash"></span>
    <div>
    <script>
      // 設定路由表映射不同頁面
      const routes = {
        "/": "Home",
        "/article": "Article",
      };

      // 更新頁面內容 (即更換 <div id="content"></div> 的內容)
      function updateContent(route) {
        const content = document.getElementById("content");
        content.innerHTML = routes[route] || "Page not found";
        const currentHash = document.getElementById("currentHash");
        currentHash.innerHTML = route;
      }

      // 監聽 hashchange 事件偵測 window.location.hash 變化,當路由改變觸發 updateContent 更新對應頁面內容
      window.addEventListener("hashchange", () => {
        updateContent(window.location.hash.slice(1));
      });

      // 初始加載路由,根據當前 hash 加載入頁面
      updateContent(window.location.hash.slice(1));
    </script>
  </body>
</html>

使用 History API 實現 router

  • 原理:

  • HTML5 History API 能夠操作瀏覽器歷史記錄(history)。使用此 API 前端路由可以在不刷新整個頁面的情況下改變 URL,並且可以監聽 URL 的變化,以便動態更新應用程式的內容。

  • 切換路由後重整 404 原因:

<!DOCTYPE html>
<html>
  <body>
    <div id="content"></div>
    <a href="/" onclick="navigateTo(event, '/')">Home</a>
    <a href="/article" onclick="navigateTo(event, '/article')">Article</a>
    <div>Current Path: <span id="currentPath"></span></div>
    <script>
      // 初始化路由
      window.onload = function () {
        renderPage(window.location.pathname);
      };

      // 監聽 popstate 事件,當 URL 變化時渲染不同頁面內容
      // 當使用者點擊瀏覽器的後退或前進按鈕時,或使用history.back()、history.forward()、history.go() 等方法時)
      window.onpopstate = function (event) {
        renderPage(window.location.pathname);
      };

      // 點擊切換路由,使用 history.pushState 改變 URL
      function navigateTo(event, path) {
        event.preventDefault();
        // history.pushState(state, title, url) 將新的紀錄添加到瀏覽器的歷史堆疊中。不會進行頁面刷新,只改變瀏覽器的 URL,並添加一條新的歷史紀錄。
        // 也可以用 replaceState(state, title, url), 該方法不會添加新的歷史紀錄,而是替換當前的歷史紀錄。
        history.pushState(null, null, path);
        renderPage(path);
      }

      // 渲染不同的頁面內容
      function renderPage(path) {
        var content = document.getElementById("content");
        var currentPath = document.getElementById("currentPath");
        currentPath.textContent = path;
        if (path === "/") content.innerHTML = "<h1>Home Page</h1>";
        else if (path === "/article") content.innerHTML = "<h1>About Page</h1>";
        else content.innerHTML = "<h1>Page Not Found</h1>";
      }
    </script>
  </body>
</html>

/etc/nginx/nginx.conf 配置:(假設使用 GCP 就在 GCE 虛擬機中調整此配置)

  • 以下設置將所有路徑轉發到 index.html
server {
    listen 80; # 指示 Nginx 監聽 80 port,接受來自這個端口的請求。
    server_name hean.tw; # 當有 HTTP 請求通過 80 端口訪問 hean.tw ,這個 Nginx 配置就會被觸發。

    root /home/hean/project-name/static; # 靜態文件部署位置,指示伺服器應該從此目錄下提供靜態資源。當用戶訪問 example.com 時,Nginx 會尋找該目錄下的文件來返回。

    index index.html; # 指示當用戶訪問根路徑時,Nginx 應該使用 index.html 文件作為默認文件。

    # location 處理所有路由,當收到請求先查找與請求的 URI 相對應的文件($uri),如果找不到,則查找與請求 URI 匹配的目錄($uri/),
    # 若還是找不到則返回 index.html 文件,相當於由前端路由器來處理路由。確保無論用戶訪問的是應用程式的哪個路徑,Nginx 都會返回 index.html 文件。
    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api { # 其他服務如 api
        proxy_pass http://localhost:8080;
    }
}