淺談React Router關於history的那些事

如果你想理解React Router,那麼應該先理解history。更確切地說,是history這個為React Router提供核心功能的包。它能輕松地在客戶端為項目添加基於location的導航,這種對於單頁應用至關重要的功能。

npm install --save history

存在三類history,分別時browser,hash,與 memory。history包提供每種history的創建方法。

import {
 createBrowserHistory,
 createHashHistory,
 createMemoryHistory
} from 'history'

如果你使用React Router,他會為你自動創建history對象,所以你並不需要與history進行直接的交互。不過,理解不同類型的history依舊很重要,這樣你能在項目中決定究竟是用哪個。

history是什麼?

無論你創建哪種history,你最終都會得到一個幾乎擁有相同屬性與方法的對象。

location

history對象中最重要的屬性就是location。location對象反映瞭當前應用所在的”位置”。其包含瞭pathname,search[註1],hash這種由’URL’派生出的屬性。

此外,每一個location都擁有一個與之關聯且獨一無二的key。’key’用於特定location的識別,向特定location存儲數據。

最後,location可以擁有與之相關的狀態。這是一些固定的數據,並且不存在於URL之中。

{
 pathname: '/here',
 search: '?key=value',
 hash: '#extra-information',
 state: { modal: true },
 key: 'abc123'
}

當創建一個history對象後,需要初始化location。對於不同類型history這一過程也不相同。例如,browser history會解析當前URL。

一個location控制所有?

誠然我們隻能訪問當前location,history對象持續追蹤著一組location。正因為擁有添加location並能夠訪問數組中任意location的能力,history才能被稱為“歷史”。如果history隻能記錄當前location,那就應該叫它“present”。

除瞭一組location外,history也保存一個索引值,用來指向當前所對應的location。

對於memory history,它們被直接定義。而對於browser history與hash history,數組與索引被瀏覽器所控制,並不能直接訪問[註2]。

navigation方法
可以說navigation方法是擁有location屬性的history體系的點睛之筆。navigation允許你改變當前location。

push方法
push方法使能你跳轉到新的location。通過在當前location後添加新的location時,任意的’未來’location會被清除(之前由後退按鈕而形成的在當前location後的location)。

默認情況下,當你點擊<Link>時,會調用history.push方法進行導航。

history.push({ pathname: '/new-place' })

replace
replace方法與push相似,但它並非添加location,而是替換當前索引上的位置。’未來’location將不會被清除。

重定向時要使用replace。這也是React Router的<Redirect>組件中使用的方法。

例如,當你在頁面1通過點擊link按鈕導航到頁面2,頁面2可能會重定向到頁面3。如果使用push方法,點擊放回按鈕將從頁面3返回到頁面2(這裡有潛在的可能再重定向到頁面3)。如果使用replace方法,會從頁面三直接返回頁面1。

history.replace({ pathname: '/go-here-instead' })

go, go, go
最後有三個帶‘go’的方法,它們分別是goBack,goForward與go。
goBack返回一層頁面。實際上是將history的索引值減1。

history.goBack()

goForward與goBack相對。向前一層頁面。這僅在擁有’未來’location生效,即當用戶點擊瞭後退按鈕。

history.goForward()

go是一個強大的方法,並包含瞭goForward與goBack的功能。傳入負數則退後,傳入正數則向前。

history.go(-3)

監聽!

采用觀察者模式,在location改變時,history會發出通知。每一個history對象都有listen方法,接受一個函數作為參數。這個函數會被添加到history儲存的監聽函數數組中。當location變化時(如代碼調用history方法或用戶點擊瀏覽器按鈕),history對象將會調用所有listener方法。這能讓你在location變化時來設置代碼更新。

const youAreHere = document.getElementById('youAreHere')
history.listen(function(location) {
 youAreHere.textContent = location.pathname
})

React Router的router組件將會訂閱history對象,這樣當location變化時,其能重新渲染。

鏈接事物Linking things together

每一類history都擁有createHref方法,其使用location對象,輸出URL。
內部,history通過location對象進行導航。然而像錨點元素(a),它並不知道history這個包,也不知道location對象是什麼。為瞭能讓生成的HTML 在不需要history的情況下,依舊能夠導航。我們必須生成真的URL。

const location = {
 pathname: '/one-fish',
 search: '?two=fish',
 hash: '#red-fish-blue-fish'
}
const url = history.createHref(location)
const link = document.createElement('a')
a.href = url
// <a href='/one-fish?two=fish#red-fish-blue-fish'></a>

以上涵蓋瞭基礎的history API。雖然還有其他未介紹的屬性與方法,但上述方法能夠是你明白history對象是如何運作的。

結合在一起

不同類型的history間還是存在差異的,這需要你去考慮選擇一個適合你項目的history。
Between the three of them, any use case should be covered.

在瀏覽器中

browser history與hash history都被用於瀏覽器環境。它們與history和location的web API進行交互,因此當前location與瀏覽器地址欄中展示的是相同的。

const browserHistory = createBrowserHistory()
const hashHistory = createHashHistory()

它們兩者的最大區別在於從URL創建location的方式。browser history使用完整URL[註3],而hash history隻使用在第一個hash後的那部分URL。

// 提供如下URL
url = 'http://www.example.com/this/is/the/path?key=value#hash'
// browser history創建的location對象:
{
 pathname: '/this/is/the/path',
 search: '?key=value',
 hash: '#hash'
}
//hash history創建的location對象:
{
 pathname: 'hash',
 search: '',
 hash: ''
}

使用哈希

為何你需要hash history?理論上來說當你導航到一個URL時,服務端必有一個相應文件與之對應。對於動態服務,請求文件並不需要真實存在。相反,服務端會檢查請求的URL並決定返回的HTML。

然而,靜態文件服務可以直接返回存在磁盤中的文件。靜態服務能做的最動態的事就是當URL制定目錄時,從目錄中返回index.html文件。

由於靜態文件服務的這種限制,最簡單的解決方案[註4]就是在服務端僅使用一個真實的location來返回用戶端的獲取需求。當然,僅有一個location意味著你的應用隻有一個URL,這樣就無法使用history。為瞭解決這一問題,hash history使用使用URL的哈希部分來讀寫location。

// 如果 example.com 使用靜態資源服務, 這三個URL都將從
// /my-site/index.html獲取相同數據
http://www.example.com/my-site#/one
http://www.example.com/my-site#/two
// 然而由於使用hash history,應用中三者的location是不同的,
// 因為location取決於URL的哈希部分
{ pathname: '/one' }
{ pathname: '/two' }

縱然hash history運作良好,但由於其依賴將所有路徑信息存在URL的哈希中,它被認為有可能遭到黑客攻擊。因此當網站沒有動態服務時再考慮使用它吧。

memory:緩存所有history

使用memory location最棒的體驗就是你可以在能使用JavaScript的地方隨意使用。

一個簡單的例子你可以通過運行Node在單元測試中使用它。這允許你能在不依賴瀏覽器運行的情況下測試代碼。

更牛逼的是,memory history可以被使用在app中。在react-nativeapp中react-router-native使用memory history來實現基於location的導航。

你可以在瀏覽器中使用使用memory history,如果你願意的話。(雖然這樣你會失去與地址欄的交互能力)。

這memory history與其他兩類history最大的區別在於其維護著自己的location。當創建memory history後你可以傳入信息進行初始化狀態。這個狀態是一個location數組與當前location的索引[註5]。這與其他兩類history是不同的,它們依賴瀏覽器來存儲這個location數組。

const history = createMemoryHistory({
 initialEntries: ['/', '/next', '/last'],
 initialIndex: 0
})

使用history代替你來處理哪些相對繁瑣且易錯的是一個行之有效的方法。

無論你選擇瞭何種類型的history,他們都是極易使用,並且擁有強大的能力進行導航與基於loaction的渲染。

註釋

[1] search屬性是一個字符串而非被解析對象。由於大多數字符串解析包在使用上各不相同。所以history把選擇權留給瞭開發者而不是強制使用某種字符串解析包。如果你想瞭解更多,這裡推薦一些流行的:query-string,querystring與原生的URLSearchParams

[2] 這是出於安全性的限制。在瀏覽器中history的location數組不僅包涵瞭訪問過的location信息。如果開放瀏覽會泄漏使用者的瀏覽器歷史信息,因此無法開放訪問。

[3] m默認情況下,browser history創建的location對象,它的路徑名是URL全路徑名。當然,你可以為history定一個基礎名,這樣路徑名中的這部分將會被忽略。

const history = createBrowserHistory({ basename: '/path' })
// 給出的路徑 url: http://www.example.com/path/here
// history對象將會創建如下location
{ pathname: '/here', ... }

[4] 理論上,可以讓應用中的每個有效URL返回相同的HTML文件。雖然這可以事項,但如果所有的URL都是靜態的,會產生大量冗餘文件。不過任意地址都使用參數大量來匹配大量可能址是不可行的。
[5] 如果並未提供memory history的初始化location數組與索引,則會生成如下默認值:

entries = [{ pathname: '/' }]
index = 0

對於大部分應用這已經足夠好瞭,但提前寫入history對於恢復內容還是一個非常有用的方法。

到此這篇關於淺談React Router關於history的那些事的文章就介紹到這瞭,更多相關React Router history內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!