一場升級 React-Router 帶來的‘血案’
一 前言
在前端開發(fā)過程中,有一種風險開發(fā)者值得警惕,就是正常情況下沒有問題,但是因為一次小上線,或者一次服務(wù)器部署,造成的線上 bug 的情況,更有甚者線上的 bug 和上線的內(nèi)容毫不相干,那么今天筆者就給大家分享一個真實案例。
本次案例覆蓋的知識點如下:
1 項目中安裝依賴包的規(guī)范。 2 context 的消費訂閱。 3 react-routerv5.2.0 版本變化。4 本地和線上事故排查。
二 問題背景
接下來介紹一下具體問題,最近有同學(化名小明)在開發(fā)中遇到了一個問題,就是使用 React-Router 帶來的線上事故。事故的發(fā)生源頭就是因為一個全局組件內(nèi)使用了 React-Router 中的自定義 hooks —— useHistory,具體細節(jié)是這樣的。
import?{?useHistory?}?from?'react-router'
function?Index(){
????/*?獲取?histroy?對象?*/
????const?history?=?useHistory()
????console.log(history)
????return?<div>
????????{/*?展示?history?里面信息,期望當?history?中?location?信息變化的時候,組件也能更新?*/}
????div>
}
小明用 React-Router 中的 useHistory 來獲取 history 對象里面的狀態(tài)。并期望:
展示 location 里面的字段。 當路由跳轉(zhuǎn),history 發(fā)生變化,期望組件 Index 也重新渲染,更新展示內(nèi)容。
這個功能在項目中是一直沒有問題的。但是最近小明開發(fā)了一個和當前組件毫無關(guān)系的新功能,并上了線。
結(jié)果在線上就出現(xiàn)了事故:當路由改變的時候,Index組件不再像原來一樣更新了。
更讓人匪夷所思的是,小明在本地環(huán)境下,不會出現(xiàn)問題。所以這個問題也就伴隨著上了線。也就是說這個問題只出現(xiàn)在線上。
這個突如其來的問題,讓小明一臉懵逼,頓時慌了手腳。那么是什么原因造成的呢?

三 解決問題
本地和線上不一樣
接下來我們來幫助小明解決這個問題。那么首先??思考一下:為什么會出現(xiàn)本地和線上不一致的情況發(fā)生?
線上和本地不一致,那么這種情況下,第一個應(yīng)該想到的就是,是不是線上的依賴包和本地的有區(qū)別。那么驗證也很簡單,就是升級本地的所有包,因為線上部署的包,一般都是 install 一個的新的包。那么可以通過如下方式驗證一下:
下載本地 node_modules;重新安裝 npm install
經(jīng)過上述方案折騰之后,發(fā)現(xiàn)本地現(xiàn)象和線上的一樣了。那么又引出了一個新的問題,小明壓根兒沒有更新過項目依賴,那么為什么會造成依賴包的差別呢?
這個本質(zhì)上和 npm 包安裝機制有關(guān)系,也就是比如你的項目依賴了 x.x.x 版本的 a 模塊,那么部署上線后項目中就一定安裝 x.x.x 版本的 a 嗎?答案是否定的,具體 npm 怎樣處理,一會會重點介紹。通過上述情況,首先分析出,問題出現(xiàn)在 React-Router 庫上,于是看一下小明項目中 package.json
"react-router":?"^5.1.2",
如上可以看到,小明項目中用的 react-router是5.1.2版本的,那么問題就在 ^ 上。
npm 版本安裝機制
^ 在package.json中代表什么意思,原來在 package.json 中 ^ 會匹配最新的大版本依賴包。打個比方:
如果依賴版本這么寫 ^1.2.3,表示安裝1.x.x的最新版本(不低于1.2.3,包括1.3.0),但是不安裝2.x.x,也就是說安裝時不改變大版本號。
那么小明在項目中 ^5.1.2 這么寫,那么如果有更高版本的 react-router 比如 5.2.x,5.3.x,那么會下載最新安裝包,一直到 6.0.0 為止(不會安裝 6.0.0 )。
需要注意的是,如果大版本號為0,則插入號的行為與波浪號相同,這是因為此時處于開發(fā)階段,即使是次要版本號變動,也可能帶來程序的不兼容。(主版本)
比如 ^0.2.3 那么代表安裝的版本范圍是 >=0.2.3 <0.3.0。
依賴版本對應(yīng)關(guān)系
| 符號 | 例子 | 范圍 | 說明 |
|---|---|---|---|
| ^會匹配最新的大版本依賴包 | ^1.2.3 | >=1.2.3 <2.0.0、 | 表示安裝1.x.x的最新版本(不低于1.2.3,包括1.3.0),但是不安裝2.x.x,也就是說安裝時不改變大版本號。 |
| ~會匹配最近的小版本依賴包 | ~1.2.3 | >=1.2.3 <1.3.0 | 表示安裝1.2.x的最新版本(不低于1.2.3),但是不安裝1.3.x,也就是說安裝時不改變大版本號和次要版本號。 |
| >= | >=2.1.0 | >=2.1.0 | 大于等于2.1.0 |
| <= | <=2.0.0 | <=2.0.0 | 小于等于2.0.0 |
| laster | -- | -- | 安裝最新的版本 |
| * | -- | -- | 任何版本 |
| - | 1.2.3 - 2.3.4 | >=1.2.3 <=2.3.4 | 兩個版本之間 |
那么我們回到小明遇到的問題的上,既然知道了原因是自動升級了,那么如果解決這個問題呢?
現(xiàn)在到了解決問題的時候了,如果出現(xiàn)線上和本地版本差異帶來的 bug ,那么最直接快速的方式就是固定版本。
"react-router":?"5.1.2",
版本號前面不加任何符號,固定版本 5.1.2,最根本有效的解決了問題。
顯然這個不是最佳答案,首先我們應(yīng)該從問題的本質(zhì)入手,為什么 react-router 不能通過 useHistory 訂閱路由信息了。那么本質(zhì)上到底改了些什么呢?我們找到 react-routerV5.1.2源碼,
export?function?useHistory()?{
??return?useContext(Context).history;
}
如上可以看到 useHistory本質(zhì)上調(diào)用了useContext,使用了整個路由庫中Context的history對象。Context 上保存了整個路由狀態(tài)信息,每次路由改變,就是通過 Context 變化來通知路由組件渲染對應(yīng)視圖的。對于 React Router 還不熟悉的同學,可以看一下筆者的另外一篇文章:「源碼解析 」這一次徹底弄懂react-router路由原理
如果對 context 的訂閱消費機制不熟悉的話,請往下??看。
context 消費機制
useHistory 本質(zhì)上用的是 useContext , useContext 本質(zhì)上是訂閱了新版本的 React Context 對象。這里有必要介紹一下 React Context 訂閱更新機制。
新版本的 Context 對象包括提供者 Provider 和訂閱者 Consumer:
Provider: 傳遞 context value 值。Consumer: 消費 Provider 提供的 value 值。類組件 contextType和函數(shù)組件的useContext也可以訂閱消費 context value ?,并且 context value 改變的時候,它們會重新渲染,而且不受到PureComponent,memo,shouldComponentUpdate優(yōu)化策略的影響。
我們回到小明遇到的問題,之前小明用 useHistory 來訂閱路由變化,當路由更新,那么使用 useHistory 的組件會重新渲染,因為之前的邏輯是,路由更新就會更新 history 對象 。我們來模擬一下流程。
const?Context?=?React.createContext()
function?useName?(){
????return?React.useContext(Context).name
}
const?Child?=?()=>{
????const?name?=?useName()
????return?<div>
????????{name}
????div>
}
const?Index?=?memo(function(){
????return?<div>
????????<p>root?組件?p>
????????<Child/>
????div>
})
export?default?function?App(){
????const?[?value?,?changeValue???]?=?React.useState({?name:'列表'?,?path:'/list'??})
????return?<div>
????????<Context.Provider?value={value}?>
????????????<Index?/>
????????Context.Provider>
????????<button?onClick={()=>?changeValue({?name:'首頁',path:'/detail'?})}?>改變?value?button>
????div>
}
效果:

切換路由相當于調(diào)用 changeValue,改變了Provider中的value。小明使用的組件就是 Child ,而使用的 useHistory類似于useName。當點擊按鈕改變 value 。Child 更新視圖。
react-router改版
上面知道了 context 的訂閱更新機制,那么為什么現(xiàn)在的 useHistory ,那么新版本的 react-router 改動了些什么呢?后來查看更新日志發(fā)現(xiàn),在 react-router v5.2.0 的時候,已經(jīng)把 history 的 Context 中抽離出來,而且已經(jīng)有了自己的 Context 。
這個是 Releases 記錄:

然后我們又去看了下源碼:
export?function?useHistory()?{
??return?useContext(HistoryContext);
}
export?function?useLocation()?{
??return?useContext(Context).location;
}
通過上面可以看到:
useHistory 已經(jīng)不再訂閱 Context,而是HistoryContext。useLocation 依舊訂閱 Context。當我們改變路由的時候,本質(zhì)上改變的是 Context,所以使用useLocation的組件會更新,使用useHistory的組件不會更新。
到這里恍然大悟,真相終于浮出了水面。

四 總結(jié)
通過本文的學習,可以收獲如下內(nèi)容:
線上和本地不一致問題排查。 package.json版本號問題。useHistory 原理。 context 訂閱更新流程。
覺得有收獲的同學可以給筆者 點贊 + 關(guān)注,持續(xù)分享前端好文。
參考資源
package.json詳解 「源碼解析 」這一次徹底弄懂react-router路由原理
