- 原文地址:All About React Router 4
- 原文做者:BRAD WESTFALL
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:undead25
- 校對者:sunui、LouisaNikita
我在 React Rally 2016 大會上第一次遇到了 Michael Jackson,不久以後便寫了一篇 an article on React Router 3。Michael 與 Ryan Florence 都是 React Router 的主要做者。遇到一位我很是喜歡的工具的建立者是激動人心的,但當他這麼說的時候,我感到很震驚。「讓我向大家展現咱們在 React Router 4 的想法,它的方式是大相徑庭的!」。老實說,我真的不明白新的方向以及爲何它須要如此大的改變。因爲路由是應用程序架構的重要組成部分,所以這可能會改變一些我喜歡的模式。這些改變的想法讓我很焦慮。考慮到社區凝聚力以及 React Router 在這麼多的 React 應用程序中扮演着重要的角色,我不知道社區將如何接受這些改變。css
幾個月後,React Router 4 發佈了,僅僅從 Twitter 的嗡嗡聲中我便得知,你們對於這個重大的重寫存在着不一樣的想法。這讓我想起了第一個版本的 React Router 針對其漸進概念的推回。在某些方面,早期版本的 React Router 符合咱們傳統的思惟模式,即一個應用的路由「應該」將全部的路由規則放在一個地方。然而,並非每一個人都接受使用嵌套的 JSX 路由。但就像 JSX 自身說服了批評者同樣(至少是大多數),許多人轉而相信嵌套的 JSX 路由是很酷的想法。html
如是,我學習了 React Router 4。無能否認,第一天是掙扎的。掙扎的倒不是其 API,而更多的是使用它的模式和策略。我使用 React Router 3 的思惟模式並無很好地遷移到 v4。若是要成功,我將不得不改變我對路由和佈局組件之間的關係的見解。最終,出現了對我有意義的新模式,我對路由的新方向感到很是高興。React Router 4 不只包含 v3 的全部功能,並且還有新的功能。此外,起初我對 v4 的使用過於複雜。一旦我得到了一個新的思惟模式,我就意識到這個新的方向是驚人的!前端
本文的意圖並非重複 React Router 4 已經寫得很好的文檔。我將介紹最多見的 API,但真正的重點是我發現的成功模式和策略。react
對於本文,如下是一些你須要熟悉的 JavaScript 概念:android
若是你喜歡跳轉到演示區的話,請點這裏:ios
查看演示git
React Router 的早期版本將路由規則集中在一個位置,使它們與佈局組件分離。固然,路由能夠被劃分紅多個文件,但從概念上講,路由是一個單元,基本上是一個美化的配置文件。github
或許瞭解 v4 不一樣之處的最好方法是用每一個版本編寫一個簡單的兩頁應用程序並進行比較。示例應用程序只有兩個路由,對應首頁和用戶頁面。web
這裏是 v3 的:npm
import { Router, Route, IndexRoute } from 'react-router'
const PrimaryLayout = props => (
<div className="primary-layout">
<header>
Our React Router 3 App
</header>
<main>
{props.children}
</main>
</div>
)
const HomePage =() => <div>Home Page</div>
const UsersPage = () => <div>Users Page</div>
const App = () => (
<Router history={browserHistory}>
<Route path="/" component={PrimaryLayout}>
<IndexRoute component={HomePage} />
<Route path="/users" component={UsersPage} />
</Route>
</Router>
)
render(<App />, document.getElementById('root'))複製代碼
如下是 v3 中的一些核心思想,但在 v4 中是不正確的:
<Route>
組件的嵌套而來的。React Router 4 再也不主張集中式路由了。相反,路由規則位於佈局和 UI 自己之間。例如,如下是 v4 中的相同的應用程序:
import { BrowserRouter, Route } from 'react-router-dom'
const PrimaryLayout = () => (
<div className="primary-layout">
<header>
Our React Router 4 App
</header>
<main>
<Route path="/" exact component={HomePage} />
<Route path="/users" component={UsersPage} />
</main>
</div>
)
const HomePage =() => <div>Home Page</div>
const UsersPage = () => <div>Users Page</div>
const App = () => (
<BrowserRouter>
<PrimaryLayout />
</BrowserRouter>
)
render(<App />, document.getElementById('root'))複製代碼
新的 API 概念:因爲咱們的應用程序是用於瀏覽器的,因此咱們須要將它封裝在來自 v4 的 BrowserRouter
中。還要注意的是咱們如今從 react-router-dom
中導入它(這意味着咱們安裝的是 react-router-dom
而不是 react-router
)。提示!如今叫作 react-router-dom
是由於還有一個 native 版本。
對於使用 React Router v4 構建的應用程序,首先看到的是「路由」彷佛丟失了。在 v3 中,路由是咱們的應用程序直接呈現給 DOM 的最巨大的東西。 如今,除了 <BrowserRouter>
外,咱們首先拋給 DOM 的是咱們的應用程序自己。
另外一個在 v3 的例子中有而在 v4 中沒有的是,使用 {props.children}
來嵌套組件。這是由於在 v4 中,<Route>
組件在何處編寫,若是路由匹配,子組件將在那裏渲染。
在前面的例子中,你可能已經注意到了 exact
這個屬性。那麼它是什麼呢?V3 的路由規則是「排他性」的,這意味着只有一條路由將獲勝。V4 的路由默認爲「包含」的,這意味着多個 <Route>
能夠同時進行匹配和渲染。
在上一個例子中,咱們試圖根據路徑渲染 HomePage
或者 UsersPage
。若是從示例中刪除了 exact
屬性,那麼在瀏覽器中訪問 /users
時,HomePage
和 UsersPage
組件將同時被渲染。
要更好地瞭解匹配邏輯,請查看 path-to-regexp,這是 v4 如今正在使用的,以肯定路由是否匹配 URL。
爲了演示包容性路由是有幫助的,咱們在標題中包含一個 UserMenu
,但前提是咱們在應用程序的用戶部分:
const PrimaryLayout = () => (
<div className="primary-layout">
<header>
Our React Router 4 App
<Route path="/users" component={UsersMenu} />
</header>
<main>
<Route path="/" exact component={HomePage} />
<Route path="/users" component={UsersPage} />
</main>
</div>
)複製代碼
如今,當用戶訪問 /users
時,兩個組件都會渲染。相似這樣的事情在 v3 中經過特定的匹配模式也是可行的,但它更復雜。得益於 v4 的包容性路由,如今可以很輕鬆地實現。
若是你只須要在路由列表裏匹配一個路由,則使用 <Switch>
來啓用排他路由:
const PrimaryLayout = () => (
<div className="primary-layout">
<PrimaryHeader />
<main>
<Switch>
<Route path="/" exact component={HomePage} />
<Route path="/users/add" component={UserAddPage} />
<Route path="/users" component={UsersPage} />
<Redirect to="/" />
</Switch>
</main>
</div>
)複製代碼
在給定的 <Switch>
路由中只有一條將渲染。在 HomePage
路由上,咱們仍然須要 exact
屬性,儘管咱們會先把它列出來。不然,當訪問諸如 /users
或 /users/add
的路徑時,主頁路由也將匹配。事實上,戰略佈局是使用排他路由策略(由於它老是像傳統路由那樣使用)時的關鍵。請注意,咱們在 /users
以前策略性地放置了 /users/add
的路由,以確保正確匹配。因爲路徑 /users/add
將匹配 /users
和 /users/add
,因此最好先把 /users/add
放在前面。
固然,若是咱們以某種方式使用 exact
,咱們能夠把它們放在任何順序上,但至少咱們有選擇。
若是遇到,<Redirect>
組件將會始終執行瀏覽器重定向,可是當它位於 <Switch>
語句中時,只有在其餘路由不匹配的狀況下,纔會渲染重定向組件。想了解在非切換環境下如何使用 <Redirect>
,請參閱下面的受權路由。
儘管在 v4 中已經沒有 <IndexRoute>
了,但可使用 <Route exact>
來達到一樣的效果。若是沒有路由解析,則可使用 <Switch>
與 <Redirect>
重定向到具備有效路徑的默認頁面(如同我對本示例中的 HomePage
所作的),甚至能夠是一個「未找到頁面」。
你可能開始期待嵌套子佈局,以及如何實現它們。我本來不認爲我會糾結這個概念,但我確實糾結了。React Router v4 給了咱們不少選擇,這使它變得很強大。可是,選擇意味着有選擇不理想策略的自由。表面上看,嵌套佈局很簡單,但根據你的選擇,可能會由於你組織路由的方式而遇到阻礙。
爲了演示,假設咱們想擴展咱們的用戶部分,因此咱們會有一個「用戶列表」頁面和一個「用戶詳情」頁面。咱們也但願產品也有相似的頁面。用戶和產品都須要其個性化的子佈局。例如,每一個可能都有不一樣的導航選項卡。有幾種方法能夠解決這個問題,有的好,有的很差。第一種方法不是很好,但我想告訴你,這樣你就不會掉入這個陷阱。第二種方法要好不少。
第一種方法,咱們修改 PrimaryLayout
,以適應用戶和產品對應的列表及詳情頁面:
const PrimaryLayout = props => {
return (
<div className="primary-layout">
<PrimaryHeader />
<main>
<Switch>
<Route path="/" exact component={HomePage} />
<Route path="/users" exact component={BrowseUsersPage} />
<Route path="/users/:userId" component={UserProfilePage} />
<Route path="/products" exact component={BrowseProductsPage} />
<Route path="/products/:productId" component={ProductProfilePage} />
<Redirect to="/" />
</Switch>
</main>
</div>
)
}複製代碼
雖然這在技術上可行的,但仔細觀察這兩個用戶頁面就會發現問題:
const BrowseUsersPage = () => (
<div className="user-sub-layout"> <aside> <UserNav /> </aside> <div className="primary-content"> <BrowseUserTable /> </div> </div>
)
const UserProfilePage = props => (
<div className="user-sub-layout"> <aside> <UserNav /> </aside> <div className="primary-content"> <UserProfile userId={props.match.params.userId} /> </div> </div> )複製代碼
新 API 概念:props.match
被賦到由 <Route>
渲染的任何組件。你能夠看到,userId
是由 props.match.params
提供的,瞭解更多請參閱 v4 文檔。或者,若是任何組件須要訪問 props.match
,而這個組件沒有由 <Route>
直接渲染,那麼咱們可使用 withRouter() 高階組件。
每一個用戶頁面不只要渲染其各自的內容,並且還必須關注子佈局自己(而且每一個子佈局都是重複的)。雖然這個例子很小,可能看起來微不足道,但重複的代碼在一個真正的應用程序中多是一個問題。更不用說,每次 BrowseUsersPage
或 UserProfilePage
被渲染時,它將建立一個新的 UserNav
實例,這意味着全部的生命週期方法都將從新開始。若是導航標籤須要初始網絡流量,這將致使沒必要要的請求 —— 這都是咱們決定使用路由的方式形成的。
這裏有另外一種更好的方法:
const PrimaryLayout = props => {
return (
<div className="primary-layout">
<PrimaryHeader />
<main>
<Switch>
<Route path="/" exact component={HomePage} />
<Route path="/users" component={UserSubLayout} />
<Route path="/products" component={ProductSubLayout} />
<Redirect to="/" />
</Switch>
</main>
</div>
)
}複製代碼
與每一個用戶和產品頁面相對應的四條路由不一樣,咱們爲每一個部分的佈局提供了兩條路由。
請注意,上述示例沒有使用 exact
屬性,由於咱們但願 /users
匹配任何以 /users
開頭的路由,一樣適用於產品。
經過這種策略,渲染其它路由將成爲子佈局的任務。UserSubLayout
可能以下所示:
const UserSubLayout = () => (
<div className="user-sub-layout">
<aside>
<UserNav />
</aside>
<div className="primary-content">
<Switch>
<Route path="/users" exact component={BrowseUsersPage} />
<Route path="/users/:userId" component={UserProfilePage} />
</Switch>
</div>
</div>
)複製代碼
新策略中最明顯的勝出在於全部用戶頁面之間的不重複佈局。這是一個共贏,由於它不會像第一個示例那樣具備相同生命週期的問題。
有一點須要注意的是,即便咱們在佈局結構中深刻嵌套,路由仍然須要識別它們的完整路徑才能匹配。爲了節省重複輸入(以防你決定將「用戶」改成其餘內容),請改用 props.match.path
:
const UserSubLayout = props => (
<div className="user-sub-layout">
<aside>
<UserNav />
</aside>
<div className="primary-content">
<Switch>
<Route path={props.match.path} exact component={BrowseUsersPage} />
<Route path={`${props.match.path}/:userId`} component={UserProfilePage} />
</Switch>
</div>
</div>
)複製代碼
到目前爲止,props.match
對於知道詳情頁面渲染的 userId
以及如何編寫咱們的路由是頗有用的。match
對象給咱們提供了幾個屬性,包括 match.params
、match.path
、match.url
和其餘幾個。
起初這二者之間的區別彷佛並不清楚。控制檯日誌有時會顯示相同的輸出,這使得它們之間的差別更加模糊。例如,當瀏覽器路徑爲 /users
時,它們在控制檯日誌將輸出相同的值:
const UserSubLayout = ({ match }) => {
console.log(match.url) // 輸出:"/users"
console.log(match.path) // 輸出:"/users"
return (
<div className="user-sub-layout">
<aside>
<UserNav />
</aside>
<div className="primary-content">
<Switch>
<Route path={match.path} exact component={BrowseUsersPage} />
<Route path={`${match.path}/:userId`} component={UserProfilePage} />
</Switch>
</div>
</div>
)
}複製代碼
ES2015 概念: match
在組件函數的參數級別將被解構。
雖然咱們看不到差別,但 match.url
是瀏覽器 URL 中的實際路徑,而 match.path
是爲路由編寫的路徑。這就是爲何它們是同樣的,至少到目前爲止。可是,若是咱們更進一步,在 UserProfilePage
中進行一樣的控制檯日誌操做,並在瀏覽器中訪問 /users/5
,那麼 match.url
將是 "/users/5"
而 match.path
將是 "/users/:userId"
。
若是你要使用其中一個來幫助你構建路由路徑,我建議你選擇 match.path
。使用 match.url
來構建路由路徑最終會致使你不想看到的場景。下面是我遇到的一個情景。在一個像 UserProfilePage
(當用戶訪問 /users/5
時渲染)的組件中,我渲染了以下這些子組件:
const UserComments = ({ match }) => (
<div>UserId: {match.params.userId}</div>
)
const UserSettings = ({ match }) => (
<div>UserId: {match.params.userId}</div>
)
const UserProfilePage = ({ match }) => (
<div>
User Profile:
<Route path={`${match.url}/comments`} component={UserComments} />
<Route path={`${match.path}/settings`} component={UserSettings} />
</div>
)複製代碼
爲了說明問題,我渲染了兩個子組件,一個路由路徑來自於 match.url
,另外一個來自 match.path
。如下是在瀏覽器中訪問這些頁面時所發生的事情:
/users/5/comments
渲染 "UserId: undefined"。/users/5/settings
渲染 "UserId: 5"。那麼爲何 match.path
能夠幫助咱們構建路徑 而 match.url
則不能夠呢?答案就是這樣一個事實:{${match.url}/comments}
基本上就像和硬編碼的 {'/users/5/comments'}
同樣。這樣作意味着後續組件將沒法正確地填充 match.params
,由於路徑中沒有參數,只有硬編碼的 5
。
直到後來我看到文檔的這一部分,才意識到它有多重要:
match:
- path - (
string
) 用於匹配路徑模式。用於構建嵌套的<Route>
- url - (
string
) URL 匹配的部分。 用於構建嵌套的<Link>
假設咱們製做的應用程序是一個儀表版,因此咱們但願可以經過訪問 /users/add
和 /users/5/edit
來新增和編輯用戶。可是在前面的例子中,users/:userId
已經指向了 UserProfilePage
。那麼這是否意味着帶有users/:userId
的路由如今須要指向另外一個子子佈局來容納編輯頁面和詳情頁面?我不這麼認爲,由於編輯和詳情頁面共享相同的用戶子佈局,因此這個策略是可行的:
const UserSubLayout = ({ match }) => (
<div className="user-sub-layout">
<aside>
<UserNav />
</aside>
<div className="primary-content">
<Switch>
<Route exact path={props.match.path} component={BrowseUsersPage} />
<Route path={`${match.path}/add`} component={AddUserPage} />
<Route path={`${match.path}/:userId/edit`} component={EditUserPage} />
<Route path={`${match.path}/:userId`} component={UserProfilePage} />
</Switch>
</div>
</div>
)複製代碼
請注意,爲了確保進行適當的匹配,新增和編輯路由須要戰略性地放在詳情路由以前。若是詳情路徑在前面,那麼訪問 /users/add
時將匹配詳情(由於 "add" 將匹配 :userId
)。
或者,若是咱們這樣建立路徑 ${match.path}/:userId(\\d+)
,來確保 :userId
必須是一個數字,那麼咱們能夠先放置詳情路由。而後訪問 /users/add
將不會產生衝突。這是我在 path-to-regexp 的文檔中學到的技巧。
在應用程序中,一般會根據用戶的登陸狀態來限制用戶訪問某些路由。對於未經受權的頁面(如「登陸」和「忘記密碼」)與已受權的頁面(應用程序的主要部分)看起來不同也是常見的。爲了解決這些需求,須要考慮一個應用程序的主要入口點:
class App extends React.Component {
render() {
return (
<Provider store={store}>
<BrowserRouter>
<Switch>
<Route path="/auth" component={UnauthorizedLayout} />
<AuthorizedRoute path="/app" component={PrimaryLayout} />
</Switch>
</BrowserRouter>
</Provider>
)
}
}複製代碼
使用 react-redux 與 React Router v4 很是相似,就像以前同樣,只需將 BrowserRouter
包在 <Provider>
中便可。
經過這種方法能夠獲得一些啓發。第一個是根據咱們所在的應用程序的哪一個部分,在兩個頂層佈局之間進行選擇。像訪問 /auth/login
或 /auth/forgot-password
這樣的路徑會使用 UnauthorizedLayout
—— 一個看起來適於這種狀況的佈局。當用戶登陸時,咱們將確保全部路徑都有一個 /app
前綴,它使用 AuthorizedRoute
來肯定用戶是否登陸。若是用戶在沒有登陸的狀況下,嘗試訪問以 /app
開頭的頁面,那麼將被重定向到登陸頁面。
雖然 AuthorizedRoute
不是 v4 的一部分,可是我在 v4 文檔的幫助下本身寫了。v4 中一個驚人的新功能是可以爲特定的目的建立你本身的路由。它不是將 component
的屬性傳遞給 <Route>
,而是傳遞一個 render
回調函數:
class AuthorizedRoute extends React.Component {
componentWillMount() {
getLoggedUser()
}
render() {
const { component: Component, pending, logged, ...rest } = this.props
return (
<Route {...rest} render={props => {
if (pending) return <div>Loading...</div>
return logged
? <Component {...this.props} />
: <Redirect to="/auth/login" />
}} />
)
}
}
const stateToProps = ({ loggedUserState }) => ({
pending: loggedUserState.pending,
logged: loggedUserState.logged
})
export default connect(stateToProps)(AuthorizedRoute)複製代碼
可能你的登陸策略與個人不一樣,我是使用網絡請求來 getLoggedUser()
,並將 pending
和 logged
插入 Redux 的狀態中。pending
僅表示在路由中請求仍在繼續。
點擊此處查看 CodePen 上完整的身份驗證示例。
React Router v4 還有不少其餘很酷的方面。最後,必定要提幾件小事,以避免到時它們讓你措手不及。
<Link>
vs <NavLink>
在 v4 中,有兩種方法能夠將錨標籤與路由集成:<Link>
和 <NavLink>
<NavLink>
與 <Link>
同樣,但若是 <NavLink>
匹配瀏覽器的 URL,那麼它能夠提供一些額外的樣式能力。例如,在示例應用程序中,有一個<PrimaryHeader>
組件看起來像這樣:
const PrimaryHeader = () => (
<header className="primary-header"> <h1>Welcome to our app!</h1> <nav> <NavLink to="/app" exact activeClassName="active">Home</NavLink> <NavLink to="/app/users" activeClassName="active">Users</NavLink> <NavLink to="/app/products" activeClassName="active">Products</NavLink> </nav> </header>
)複製代碼
使用 <NavLink>
可讓我給任何一個激活的連接設置一個 active
樣式。並且,須要注意的是,我也能夠給它們添加 exact
屬性。若是沒有 exact
,因爲 v4 的包容性匹配策略,那麼在訪問 /app/users
時,主頁的連接將處於激活中。就我的經歷而言,NavLink
帶 exact
屬性等價於 v3 的 <link>
,並且更穩定。
再也沒法從 React Router v4 中獲取 URL 的查詢字符串了。在我看來,作這個決定是由於沒有關於如何處理複雜查詢字符串的標準。因此,他們決定讓開發者去選擇如何處理查詢字符串,而不是將其做爲一個選項嵌入到 v4 的模塊中。這是一件好事。
就我的而言,我使用的是 query-string,它是由 sindresorhus 大神寫的。
關於 v4 最好的部分之一是幾乎全部的東西(包括 <Route>
)只是一個 React 組件。路由再也不是神奇的東西了。咱們能夠隨時隨地渲染它們。想象一下,當知足某些條件時,你的應用程序的整個部分均可以路由到。當這些條件不知足時,咱們能夠移除路由。甚至咱們能夠作一些瘋狂並且很酷的遞歸路由。
由於它 Just Components™,React Router 4 更簡單了。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。