路由跳轉原理 之 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.hash
和 history.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.newURL
和 e.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
使其功能更加強大:
- 無法傳參
- 無法實現子路由
- 無法通過函數跳轉路由
- ...