[JavaScript進階] 路由跳轉原理 之 Hash 模式

来源:https://www.cnblogs.com/Yiero/archive/2023/11/09/17818698.html
-Advertisement-
Play Games

路由跳轉原理 之 Hash 一. 路由跳轉的原理 首先講講路由跳轉的原理, 其實沒有什麼神秘的, 以變數類比: // 首先定義一個變數名為 container , 賦予初始值 'index' let container = 'index'; // 監聽一個點擊事件 window.addEventLi ...


路由跳轉原理 之 Hash

一. 路由跳轉的原理

首先講講路由跳轉的原理, 其實沒有什麼神秘的, 以變數類比:

// 首先定義一個變數名為 container , 賦予初始值 'index'
let container = 'index';

// 監聽一個點擊事件
window.addEventListener('click', (e) => {
  // 當點擊事件的觸發元素的 id 為 'index' 的時候
	if (e.target.id === 'index') {
    // 改變變數的值為 'index'
  	container = 'index';
  }
  // 當點擊事件的觸發元素的 id 為 'news' 的時候
  else if (e.target.id === 'news') {
		// 改變變數的值為 'news'
		container = 'news';
  }
});

在上文的代碼中, 在監聽到點擊事件的時候, 會改變變數的值. 那麼, 如果不再監聽點擊事件, 而是 監聽頁面路徑改變 ; container 也不是一個變數而是一個HTML元素, 當監聽回調觸發時, 修改的是這個 container 元素內部的HTML片段, 那麼其實就是路由跳轉了:

// 定義一套路由
const ComponentIndex = `<div>Index Page</div>`;
const ComponentNews = `<div>News Page</div>`;

// 獲取 `container` 容器
let container = document.querySelector('#container');
// 賦予 `container` 容器初始值
container.innerHTML = ComponentIndex;

// 監聽一個頁面跳轉事件 (不存在 pageURLChange 事件, 僅作為一個示例)
window.addEventListener('pageURLChange', (e) => {
  // 假設傳入的回調參數就是跳轉的頁面
  
  // 當跳轉的頁面是 'index' 時
	if (e === 'index') {
    // 將 container 容器的內部HTML片段修改為Index路由的內容
  	container.innerHTML = ComponentIndex;
  }
  // 當跳轉的頁面是 'news' 時
  else if (e === 'news') {
		// 將 container 容器的內部HTML片段修改為News路由的內容
		container.innerHTML = ComponentNews;
  }
});

二. Hash跳轉的實現原理

路由跳轉目前主要有兩種, hash 模式和 history 模式, 這其實是對應了 JavaScript 中兩種無刷新改變網頁URL的方式: location.hashhistory.pushState . 本文主要講的就是第一種: Hash模式 .

Hash 模式和 History 模式原理都是一樣的, 不過是監聽頁面路徑跳轉的方式不同而已.

History模式等有空了會寫的...(咕

2.1 Hash是什麼

URL 路徑中可以存在 錨點 , 通過一個符號 # 表示. 當 URL 中存在錨點的時候, 錨點後面的字元串將不會被請求上伺服器, 僅作為本地瀏覽器數據訪問, 這個值被稱為 Hash 值.

比如, 下麵這兩串網址訪問百度伺服器的時候, 百度都只會接收到 https://www.baidu.com/ 這一串地址請求, Hash 值並不會通過網路請求發送給伺服器.

https://www.baidu.com
https://www.baidu.com#12345

關於錨點的概念, 其實在初學 HTML 的時候就接觸到了: 在學習錨元素 <a> 的時候其實就已經瞭解過了, 當時講的是 a 元素可以通過 #id 跳轉至頁面的某一個 id 的位置, 這本質上就是利用到了錨點.

參考文檔: [MDN - 文本片段]

2.2 改變 hash 的原理

通過 location.hash 屬性, 可以更改頁面的 Hash , 並且不會刷新頁面只改變 URL 路徑; 當 Hash 改變的時候, 會觸發一個 hashchange 事件, 以此我們就可以通過監聽 hashchange 事件事件去改變頁面的內容了.

同時, 當頁面 Hash 改變的時候, 也會向瀏覽器的訪問歷史添加一個記錄, 所以也可能通過 history.go() 去控制頁面訪問歷史.

因為這兩個API都比較簡單所以就不單獨列出來說明瞭, 可以自行參照文檔閱讀. 直接看下文代碼也是可以的, 都是一些很基礎的應用並且我會作出一定的說明.

參考文檔:

2.3 通過 hashchange 事件監聽頁面路徑改變

2.3.1 location.hash

當 URL 中沒有錨點的時候, 直接輸出會輸出空字元串:

/* URL: www.baidu.com */

console.log(location.hash);
// -> ''

當向沒有錨點的 URL 改變 hash 時, 會自動添加錨點:

輸入時不用添加錨點, 但是輸出時會輸出錨點(見下例).

/* URL: www.baidu.com */

location.hash = 'index';
/* URL: www.baidu.com#index */

console.log(location.hash);
// -> '#index'

通常為了讓路由跳轉後的 URL 更像一個地址, 我們會在 Hash 前添加一個斜杠 / :

/* URL: www.baidu.com */

location.hash = '/index';
/* URL: www.baidu.com#/index */

2.3.2 hashchange 事件

當頁面中的 Hash 發生改變的時候, 會觸發事件 hashchange , 在事件的回調參數 e 中有兩個可以利用到的屬性: e.newURLe.oldURL . 見名思意, 分別是改變後的 URL 和改變前的 URL .

// 監聽 hash 改變事件
window.addEventListener('hashchange', (e) => {
  // 防止重覆跳轉
  if (e.newURL === e.oldURL) {
      return;
  }
  
  /*
   * 判斷完重覆跳轉的情況之後, 直接使用頁面的 `location.hash` 就可以了
   * e.newURL 是一個字元串, 獲取 hash 還需要額外處理
   * 之前說過輸出 hash 的時候會輸出錨點 所以通過 `.slice()` 方法將第一個錨點符號刪除.
   */
  console.log(location.hash.slice(1))
}

/* URL: www.baidu.com */

location.hash = '/index';
/* URL: www.baidu.com#/index */
// -> /index

其實這個回調參數是可以不使用的, 因為如果 e.newURL === e.oldURL , Hash 根本不會發生改變, hashchange 事件也不會觸發, 這裡僅僅只是作為一個 示例 .

2.4 通過 history.go() 控制頁面訪問歷史

每一次調用 location.hash 都會往瀏覽器中寫入一條歷史記錄, 理所應當 history.go() 也能控制 Hash 改變產生的歷史記錄:

/* URL: www.baidu.com */

location.hash = '/index';
/* URL: www.baidu.com#/index */

location.hash = '/news';
/* URL: www.baidu.com#/news */

history.go(-1);
/* URL: www.baidu.com#/index */

history.go(-1);
/* URL: www.baidu.com */

三. 實現一個 HashRouter 庫

在前文我們已經對 Hash 模式的路由跳轉進行了簡單的剖析, 現在可以試著做一個簡易的 Router 路由跳轉庫了.

3.1 規範

首先, 我們需要對一些格式進行一定的規範, 這樣我們就可以基於這些規範寫一個標準庫:

3.1.1 HTML 規範

對於 HTML , 我們使用 dataset 進行標記:

  • data-router-link-container: 表示一個路由跳轉容器.
    • data-router-link: 表示一個路由跳轉鏈接, 該屬性的值就是跳轉的路由地址.
  • data-router-view: 表示一個路由內容顯示容器, 路由跳轉後顯示的內容會在該元素內顯示.

3.1.2 JavaScript 規範

編寫一個 HashRouter 類, 構造函數的參數 options 的類型為:

options = {
	routes: Array<{
    path: string, 
    component: {
    	template: string,
    }
  }>
}
  • routes: 路由數組
    • path: 路由的路徑
    • component: 路由組件的內容
      • template: 路由組件的 HTML 片段

參考文檔:

3.2 編寫庫

這部分內容就簡單講了, 內容都在代碼塊中, 主要就是一個元素獲取以及 dataset 的值獲取.

下麵的方法都是類 HashRouter 中的方法:

3.2.1 跳轉路由

/**
 * 監聽路由跳轉容器點擊, 跳轉路由
 *  綁定一個具有 [data-router-link-container] 屬性的容器, 監聽這個容器內冒泡出來的 `click` 事件
 *  當 `click` 事件觸發時, 判斷觸發的元素是否有 `data-router-link` 屬性
 *  如果存在, 則改變當前頁面的 Hash 為 `data-router-link` 屬性的值
 */
bindRouterLinkEvent() {
    // 找到具有 [data-router-link-container] 屬性的容器
    document.querySelector('[data-router-link-container]')
        // 監聽容器內冒泡出來的 click 事件
        ?.addEventListener('click', (e) => {
            // 排除非 [data-router-link] 屬性的容器
            if (!e.target.dataset['routerLink']) {
                return;
            }

            // 阻止標簽跳轉
            e.preventDefault();

            // 更改頁面 Hash
            window.location.hash = `${e.target.dataset['routerLink']}`
        });
}

3.2.2 監聽路由跳轉

/**
 * 監聽 URL hash 的改變, 並且更新 [data-router-view] 容器內的 HTML 片段.
 */
listenHashChange() {
    window.addEventListener('hashchange', () => {
        // 尋找跳轉路徑
        const route = this.routes.find(
            route => route.path === window.location.hash.slice(1)
        );

        // 如果找不到跳轉路徑, 報錯
        if (!route) {
            console.error('找不到跳轉路徑');
            return;
        }

        // 改變 [data-router-view] 容器內的 HTML 片段
        const viewContainer = document.querySelector('[data-router-view]');
        if (viewContainer) {
            viewContainer.innerHTML = route.component.template;
        }
    })
}

3.2.3 瀏覽歷史操作

/**
 * 歷史記錄跳轉
 *
 * @param {number} step - 跳轉的步數
 *
 * @example HashRouter.go(1) 前進一步歷史
 * @example HashRouter.go(-1) 後退一步歷史
 */
go(step) {
    history.go(step);
}

/**
 * 歷史記錄跳轉 - 後退一步
 */
back(){
    this.go(-1);
}

/**
 * 歷史記錄跳轉 - 前進一步
 */
forward(){
    this.go(1);
}

3.3 HashRouter.js

/* HashRouter.js */
class HashRouter {
    /**
     * @constructor
     * @param {{routes: [{path: string, component: {template: string}}]}} options
     * */
    constructor(options) {
        this.routes = options.routes;
        this.bindRouterLinkEvent();
        this.listenHashChange();
    }

    /**
     * 匹配路由
     *  綁定一個具有 [data-router-link-container] 屬性的容器, 監聽這個容器內冒泡出來的 `click` 事件
     *  當 `click` 事件觸發時, 判斷觸發的元素是否有 `data-router-link` 屬性
     *  如果存在, 則改變當前頁面的 Hash 為 `data-router-link` 屬性的值
     */
    bindRouterLinkEvent() {
        // 找到具有 [data-router-link-container] 屬性的容器
        document.querySelector('[data-router-link-container]')
            // 監聽容器內冒泡出來的 click 事件
            ?.addEventListener('click', (e) => {
                // 排除非 [data-router-link] 屬性的容器
                if (!e.target.dataset['routerLink']) {
                    return;
                }

                // 阻止標簽跳轉
                e.preventDefault();

                // 更改頁面 Hash
                window.location.hash = `${e.target.dataset['routerLink']}`
            });
    }

    /**
     * 監聽 URL hash 的改變, 並且更新 [data-router-view] 容器內的 HTML 片段.
     */
    listenHashChange() {
        window.addEventListener('hashchange', () => {
            // 尋找跳轉路徑
            const route = this.routes.find(
                route => route.path === window.location.hash.slice(1)
            );

            // 如果找不到跳轉路徑, 報錯
            if (!route) {
                console.error('找不到跳轉路徑');
                return;
            }

            // 改變 [data-router-view] 容器內的 HTML 片段
            const viewContainer = document.querySelector('[data-router-view]');
            if (viewContainer) {
                viewContainer.innerHTML = route.component.template;
            }
        })
    }

    /**
     * 歷史記錄跳轉
     *
     * @param {number} step - 跳轉的步數
     *
     * @example HashRouter.go(1) 前進一步歷史
     * @example HashRouter.go(-1) 後退一步歷史
     */
    go(step) {
        history.go(step);
    }

    /**
     * 歷史記錄跳轉 - 後退一步
     */
    back(){
        this.go(-1);
    }

    /**
     * 歷史記錄跳轉 - 前進一步
     */
    forward(){
        this.go(1);
    }
}

export default HashRouter;

3.4 示例

目錄結構:

| HashRouter.js

| index.html

<!-- index.html -->
<nav class="route-nav" data-router-link-container>
    <a class="toPageIndex" data-router-link="/index">Index</a>
    <a class="toPageNews" data-router-link="/news">News</a>
</nav>
<hr>
<div data-router-view></div>
<script type="module">
  	// 引入 HashRouter.js
    import HashRouter from './HashRouter.js';

    // 聲明路由模板
    const IndexPage = {
        template: '<div>IndexPage</div>'
    }
    const NewsPage = {
        template: '<div>NewsPage</div>'
    }

    // 註冊路由
    new HashRouter({
        routes: [
            {
                path: '/',
                component: IndexPage
            },
            {
                path: '/index',
                component: IndexPage
            },
            {
                path: '/news',
                component: NewsPage
            }
        ]

    })
</script>

3.5 一些問題

HashRouter.js 存在的一些問題, 提供思考, 感興趣的也可以想一想如何改造 HashRouter.js 使其功能更加強大:

  • 無法傳參
  • 無法實現子路由
  • 無法通過函數跳轉路由
  • ...

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 有的時候我們會對程式進行單元測試, 為了測試的效果以及後期的維護, 我一般會將各個測試拆開, 根據需要測試的類分到各個類型中, 不過在實際操作的時候就出現了一些意想不到的問題, 各個測試的執行是亂序的, 按照我自己寫測試的習慣, 假如我需要測試新寫的增刪改查的功能, 我會將增刪改查分開測試, 會按照 ...
  • 接上篇 docker-bind 的使用搭建了一個 dns 服務,本篇將介紹另外一款 DnsServer 的部署和使用,更專註,更輕量。 ...
  • MongoDB+SignalR+Hangfire+Vue2+百度地圖實現GPS實時定位 一、實現效果 二、安裝MongoDB 可以自行參考菜鳥鏈接:MongoDB 教程 | 菜鳥教程 (runoob.com) 1.下載mongodb資料庫安裝包: 網盤鏈接:https://pan.baidu.com ...
  • 目錄String簡單介紹常見命令應用場景Hash簡單介紹常見命令應用場景List簡單介紹常見命令應用場景Set簡單介紹常見命令應用場景Sorted Set(Zset)簡單介紹常見命令應用場景Bitmap簡單介紹常見命令應用場景附錄 Redis支持多種數據類型,比如String、hash、list、S ...
  • 在構建數據倉庫或做數據分析時,需要對原始數據的結構進行一定的處理,有時涉及到“行轉列”,有時涉及到“列轉行”,那麼這兩個轉換的方式具體是什麼,有什麼差異,怎麼實現。 ...
  • 本文主要以介紹方法為主,落地過程可以歸納為方案->收益測算->數據安全驗證->系統穩定性驗證->灰度與回滾。文中的賬單系統通過step1大表壓縮32%,step2大JSON欄位序列化12%,step3刪除無效數據10%,3個方案的順利落地,有效的減少了50.7%的磁碟空間,成本下降也非常顯著。最後,... ...
  • 本文分享自華為雲社區《GaussDB(DWS)性能調優:表掃描時過濾行數過多引起的性能瓶頸問題案例》,作者: O泡果奶~ 。 1、【問題描述】 SQL語句執行過程中,對12億數據量的大表進行掃描,過濾99%的數據僅留617行數據,性能瓶頸位於掃描該表這裡。 2、【原始語句】 set search_p ...
  • 本文采用一張簡單的架構圖說明瞭MySQL查詢中使用的組件和組件間關係。解析了一條sql語句從客戶端請求mysql伺服器到返回給客戶端的整個生命周期流程。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...