监听router_深入揭秘前端路由本质,手写 mini-router
前言
前端路由一直是一個很經典的話題,不管是日常的使用還是面試中都會經常遇到。本文通過實現一個簡單版的 react-router 來一起揭開路由的神秘面紗。
通過本文,你可以學習到:
- 前端路由本質上是什么。
- 前端路由里的一些坑和注意點。
- hash 路由和 history 路由的區別。
- Router 組件和 Route 組件分別是做什么的。
路由的本質
簡單來說,瀏覽器端路由其實并不是真實的網頁跳轉(和服務器沒有任何交互),而是純粹在瀏覽器端發生的一系列行為,本質上來說前端路由就是:
對 url 進行改變和監聽,來讓某個 dom 節點顯示對應的視圖。
僅此而已。新手不要被路由這個概念給嚇到。
路由的區別
一般來說,瀏覽器端的路由分為兩種:
我們已經講過了路由的本質,那么實際上只需要搞清楚兩種路由分別是如何 改變,并且組件是如何監聽并完成視圖的展示,一切就真相大白了。
不賣關子,先分別談談兩種路由用什么樣的 api 實現前端路由:
hash
通過 location.hash = 'foo' 這樣的語法來改變,路徑就會由 baidu.com 變更為 baidu.com/#foo。
通過 window.addEventListener('hashchange') 這個事件,就可以監聽到 hash 值的變化。
history
其實是用了 history.pushState 這個 API 語法改變,它的語法乍一看比較怪異,先看下 mdn 文檔里對它的定義:
“
history.pushState(state, title[, url])
”
其中 state 代表狀態對象,這讓我們可以給每個路由記錄創建自己的狀態,并且它還會序列化后保存在用戶的磁盤上,以便用戶重新啟動瀏覽器后可以將其還原。
title 當前沒啥用。
url 在路由中最重要的 url 參數反而是個可選參數,放在了最后一位。
通過 history.pushState({}, '', foo),可以讓 baidu.com 變化為baidu.com/foo。
為什么路徑更新后,瀏覽器頁面不會重新加載?
這里我們需要思考一個問題,平常通過 location.href = 'baidu.com/foo' 這種方式來跳轉,是會讓瀏覽器重新加載頁面并且請求服務器的,但是 history.pushState 的神奇之處就在于它可以讓 url 改變,但是不重新加載頁面,完全由用戶決定如何處理這次 url 改變。
因此,這種方式的前端路由必須在支持 histroy API 的瀏覽器上才可以使用。
為什么刷新后會 404?
本質上是因為刷新以后是帶著 baidu.com/foo 這個頁面去請求服務端資源的,但是服務端并沒有對這個路徑進行任何的映射處理,當然會返回 404,處理方式是讓服務端對于"不認識"的頁面,返回 index.html,這樣這個包含了前端路由相關js代碼的首頁,就會加載你的前端路由配置表,并且此時雖然服務端給你的文件是首頁文件,但是你的 url 上是 baidu.com/foo,前端路由就會加載 /foo 這個路徑相對應的視圖,完美的解決了 404 問題。
history 路由的監聽也有點坑,瀏覽器提供了window.addEventListener('popstate') 事件,但是它只能監聽到瀏覽器回退和前進所產生的路由變化,對于主動的 pushState 卻監聽不到。解決方案當然有,下文實現 react-router 的時候再細講~
實現 react-mini-router
本文實現的 react-router 基于 history 版本,用最小化的代碼還原路由的主要功能,所以不會有正則匹配或者嵌套子路由等高階特性,回歸本心,從零到一實現最簡化的版本。
實現 history
對于 history 難用的官方 API,我們專門抽出一個小文件對它進行一層封裝,對外提供:
這兩個 API,減輕用戶的心智負擔。
我們利用觀察者模式封裝了一個簡單的 listen API,讓用戶可以監聽到history.push 所產生的路徑改變。
//?存儲?history.listen?的回調函數let?listeners:?Listener[]?=?[];function?listen(fn:?Listener)?{??listeners.push(fn);??return?function()?{????listeners?=?listeners.filter(listener?=>?listener?!==?fn);??};}這樣外部就可以通過:
history.listen(location?=>?{??console.log('changed',?location);});這樣的方式感知到路由的變化了,并且在 location 中,我們還提供了 state、pathname、search 等關鍵的信息。
實現改變路徑的核心方法 push 也很簡單:
function?push(to:?string,?state?:?State)?{??//?解析用戶傳入的?url??//?分解成?pathname、search?等信息??location?=?getNextLocation(to,?state);??//?調用原生?history?的方法改變路由??window.history.pushState(state,?'',?to);??//?執行用戶傳入的監聽函數??listeners.forEach(fn?=>?fn(location));}在 history.push('foo') 的時候,本質上就是調用了 window.history.pushState 去改變路徑,并且通知 listen 所掛載的回調函數去執行。
當然,別忘了用戶點擊瀏覽器后退前進按鈕的行為,也需要用 popstate 這個事件來監聽,并且執行同樣的處理:
//?用于處理瀏覽器前進后退操作window.addEventListener('popstate',?()?=>?{??location?=?getLocation();??listeners.forEach(fn?=>?fn(location));});接下來我們需要實現 Router 和 Route 組件,你就會看到它們是如何和這個簡單的 history 庫結合使用了。
實現 Router
Router 的核心原理就是通過 Provider 把 location 和 history 等路由關鍵信息傳遞給子組件,并且在路由發生變化的時候要讓子組件可以感知到:
import?React,?{?useState,?useEffect,?ReactNode?}?from?'react';import?{?history,?Location?}?from?'./history';interface?RouterContextProps?{??history:?typeof?history;??location:?Location;}export?const?RouterContext?=?React.createContext(??null,);export?const?Router:?React.FC?=?({?children?})?=>?{??const?[location,?setLocation]?=?useState(history.location);??//?初始化的時候?訂閱?history?的變化??//?一旦路由發生改變?就會通知使用了?useContext(RouterContext)?的子組件去重新渲染??useEffect(()?=>?{????const?unlisten?=?history.listen(location?=>?{??????setLocation(location);????});????return?unlisten;??},?[]);??return?(??????????{children}??????);};注意看注釋的部分,我們在組件初始化的時候利用 history.listen 監聽了路由的變化,一旦路由發生改變,就會調用 setLocation 去更新 location 并且通過Provider 傳遞給子組件。
并且這一步也會觸發 Provider 的 value 值的變化,通知所有用 useContext 訂閱了history 和 location 的子組件去重新 render。
實現 Route
Route 組件接受 path 和 children 兩個 prop,本質上就決定了在某個路徑下需要渲染什么組件,我們又可以通過 Router 的 Provider 傳遞下來的 location 信息拿到當前路徑,所以這個組件需要做的就是判斷當前的路徑是否匹配,渲染對應組件。
import?{?ReactNode?}?from?'react';import?{?useLocation?}?from?'./hooks';interface?RouteProps?{??path:?string;??children:?ReactNode;}export?const?Route?=?({?path,?children?}:?RouteProps)?=>?{??const?{?pathname?}?=?useLocation();??const?matched?=?path?===?pathname;??if?(matched)?{????return?children;??}??return?null;};這里的實現比較簡單,路徑直接用了全等,實際上真正的實現考慮的情況比較復雜,使用了 path-to-regexp 這個庫去處理動態路由等情況,但是核心原理其實就是這么簡單。
實現 useLocation、useHistory
這里就很簡單了,利用 useContext 簡單封裝一層,拿到 Router 傳遞下來的history 和 location 即可。
import?{?useContext?}?from?'react';import?{?RouterContext?}?from?'./Router';export?const?useHistory?=?()?=>?{??return?useContext(RouterContext)!.history;};export?const?useLocation?=?()?=>?{??return?useContext(RouterContext)!.location;};實現驗證 demo
至此為止,以下的路由 demo 就可以跑通了:
import?React,?{?useEffect?}?from?'react';import?{?Router,?Route,?useHistory?}?from?'react-mini-router';const?Foo?=?()?=>?'foo';const?Bar?=?()?=>?'bar';const?Links?=?()?=>?{??const?history?=?useHistory();??const?go?=?(path:?string)?=>?{????const?state?=?{?name:?path?};????history.push(path,?state);??};??return?(???????????go('foo')}>foo???????go('bar')}>bar??????);};export?default?()?=>?{??return?(????結語
通過本文的學習,相信小伙伴們已經搞清楚了前端路由的原理,其實它只是對瀏覽器提供 API 的一個封裝,以及在框架層去聯動做對應的渲染,換個框架 vue-router 也是類似的原理。
喜歡的老鐵,加個關注!今后會分享更多的前端干貨,歡迎點贊轉發關注[比心]
本文源碼地址:https://github.com/sl1673495/react-mini-router
總結
以上是生活随笔為你收集整理的监听router_深入揭秘前端路由本质,手写 mini-router的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SQLite内置表SQLITE_MAST
- 下一篇: oracle什么时候用in,Oracle