>我們是[袋鼠雲數棧 UED 團隊](http://ued.dtstack.cn/),致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。 >本文作者:[霜序](https://luckyfbb.github.io/blog) ## 前言 在[前一篇文章 ...
我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。
本文作者:霜序
前言
在前一篇文章中,我們詳細的說了 [email protected] 升級到 @6.x 需要註意的問題以及變更的使用方式。
react-router 版本更新非常快,但是它的底層實現原理確是萬變不離其中,在本文中會從前端路由出發到 react-router 原理總結與分享。
前端路由
在 Web 前端單頁面應用 SPA(Single Page Application)中,路由是描述 URL 和 UI 之間的映射關係,這種映射是單向的,即 URL 的改變會引起 UI 更新,無需刷新頁面
如何實現前端路由
實現前端路由,需要解決兩個核心問題
- 如何改變 URL 卻不引起頁面刷新?
- 如何監測 URL 變化?
在前端路由的實現模式有兩種模式,hash 和 history 模式,分別回答上述兩個問題
hash 模式
- hash 是 url 中 hash(#) 及後面的部分,常用錨點在頁面內做導航,改變 url 中的 hash 部分不會引起頁面的刷新
- 通過 hashchange 事件監聽 URL 的改變。改變 URL 的方式只有以下幾種:通過瀏覽器導航欄的前進後退、通過
<a>
標簽、通過window.location
,這幾種方式都會觸發hashchange
事件
history 模式
- history 提供了
pushState
和replaceState
兩個方法,這兩個方法改變 URL 的 path 部分不會引起頁面刷新 - 通過 popchange 事件監聽 URL 的改變。需要註意只在通過瀏覽器導航欄的前進後退改變 URL 時會觸發
popstate
事件,通過<a>
標簽和pushState
/replaceState
不會觸發popstate
方法。但我們可以攔截<a>
標簽的點擊事件和pushState
/replaceState
的調用來檢測 URL 變化,也是可以達到監聽 URL 的變化,相對hashchange
顯得略微複雜
JS 實現前端路由
基於 hash 實現
由於三種改變 hash 的方式都會觸發hashchange
方法,所以只需要監聽hashchange
方法。需要在DOMContentLoaded
後,處理一下預設的 hash 值
// 頁面載入完不會觸發 hashchange,這裡主動觸發一次 hashchange 事件,處理預設hash
window.addEventListener('DOMContentLoaded', onLoad);
// 監聽路由變化
window.addEventListener('hashchange', onHashChange);
// 路由變化時,根據路由渲染對應 UI
function onHashChange() {
switch (location.hash) {
case '#/home':
routerView.innerHTML = 'This is Home';
return;
case '#/about':
routerView.innerHTML = 'This is About';
return;
case '#/list':
routerView.innerHTML = 'This is List';
return;
default:
routerView.innerHTML = 'Not Found';
return;
}
}
基於 history 實現
因為 history 模式下,<a>
標簽和pushState
/replaceState
不會觸發popstate
方法,我們需要對<a>
的跳轉和pushState
/replaceState
做特殊處理。
- 對
<a>
作點擊事件,禁用預設行為,調用pushState
方法並手動觸發popstate
的監聽事件 - 對
pushState
/replaceState
可以重寫 history 的方法並通過派發事件能夠監聽對應事件
var _wr = function (type) {
var orig = history[type];
return function () {
var e = new Event(type);
e.arguments = arguments;
var rv = orig.apply(this, arguments);
window.dispatchEvent(e);
return rv;
};
};
// 重寫pushstate事件
history.pushState = _wr('pushstate');
function onLoad() {
routerView = document.querySelector('#routeView');
onPopState();
// 攔截 <a> 標簽點擊事件預設行為
// 點擊時使用 pushState 修改 URL並更新手動 UI,從而實現點擊鏈接更新 URL 和 UI 的效果。
var linkList = document.querySelectorAll('a[href]');
linkList.forEach((el) =>
el.addEventListener('click', function (e) {
e.preventDefault();
history.pushState(null, '', el.getAttribute('href'));
onPopState();
}),
);
}
// 監聽pushstate方法
window.addEventListener('pushstate', onPopState());
// 頁面載入完不會觸發 hashchange,這裡主動觸發一次 popstate 事件,處理預設pathname
window.addEventListener('DOMContentLoaded', onLoad);
// 監聽路由變化
window.addEventListener('popstate', onPopState);
// 路由變化時,根據路由渲染對應 UI
function onPopState() {
switch (location.pathname) {
case '/home':
routerView.innerHTML = 'This is Home';
return;
case '/about':
routerView.innerHTML = 'This is About';
return;
case '/list':
routerView.innerHTML = 'This is List';
return;
default:
routerView.innerHTML = 'Not Found';
return;
}
}
React-Router 的架構
- history 庫給 browser、hash 兩種 history 提供了統一的 API,給到 react-router-dom 使用
- react-router 實現了路由的最核心能力。提供了
<Router>
、<Route>
等組件,以及配套 hook - react-router-dom 是對 react-router 更上一層封裝。把 history 傳入
<Router>
並初始化成<BrowserRouter>
、<HashRouter>
,補充了<Link>
這樣給瀏覽器直接用的組件。同時把 react-router 直接導出,減少依賴
History 實現
history
在上文中說到,BrowserRouter
使用 history 庫提供的createBrowserHistory
創建的history
對象改變路由狀態和監聽路由變化。
❓ 那麼 history 對象需要提供哪些功能訥?
- 監聽路由變化的
listen
方法以及對應的清理監聽unlisten
方法 - 改變路由的
push
方法
// 創建和管理listeners的方法
export const EventEmitter = () => {
const events = [];
return {
subscribe(fn) {
events.push(fn);
return function () {
events = events.filter((handler) => handler !== fn);
};
},
emit(arg) {
events.forEach((fn) => fn && fn(arg));
},
};
};
BrowserHistory
const createBrowserHistory = () => {
const EventBus = EventEmitter();
// 初始化location
let location = {
pathname: '/',
};
// 路由變化時的回調
const handlePop = function () {
const currentLocation = {
pathname: window.location.pathname,
};
EventBus.emit(currentLocation); // 路由變化時執行回調
};
// 定義history.push方法
const push = (path) => {
const history = window.history;
// 為了保持state棧的一致性
history.pushState(null, '', path);
// 由於push並不觸發popstate,我們需要手動調用回調函數
location = { pathname: path };
EventBus.emit(location);
};
const listen = (listener) => EventBus.subscribe(listener);
// 處理瀏覽器的前進後退
window.addEventListener('popstate', handlePop);
// 返回history
const history = {
location,
listen,
push,
};
return history;
};
對於 BrowserHistory 來說,我們的處理需要增加一項,當我們觸發 push 的時候,需要手動通知所有的監聽者,因為 pushState 無法觸發 popState 事件,因此需要手動觸發
HashHistory
const createHashHistory = () => {
const EventBus = EventEmitter();
let location = {
pathname: '/',
};
// 路由變化時的回調
const handlePop = function () {
const currentLocation = {
pathname: window.location.hash.slice(1),
};
EventBus.emit(currentLocation); // 路由變化時執行回調
};
// 不用手動執行回調,因為hash改變會觸發hashchange事件
const push = (path) => (window.location.hash = path);
const listen = (listener: Function) => EventBus.subscribe(listener);
// 監聽hashchange事件
window.addEventListener('hashchange', handlePop);
// 返回的history上有個listen方法
const history = {
location,
listen,
push,
};
return history;
};
在實現 hashHistory 的時候,我們只是對hashchange進行了監聽,當該事件發生時,我們獲取到最新的 location 對象,在通知所有的監聽者 listener 執行回調函數
React-Router@6 丐版實現
- 綠色為 history 中的方法
- 紫色為 react-router-dom 中的方法
- 橙色為 react-router 中的方法