react 獲取完整路徑_React 進階: React Router
技術標籤:react 獲取完整路徑
原文連結:css-tricks.com
本文是Brad Westfall編寫的 React 系列三篇教程中的第一篇。Brad 將本文投遞給我時指出:React 初級教程有很多,但是晉級教程卻不多。如果你是 React 新手,我推薦你觀看這個介紹視訊。本系列教程在這個視訊的基礎上繼續。
系列文章
第一部分: React Router (即本文!)
第二部分:容器元件
第三部分:Redux
在開始學習 React 時,我找到了很多新手指南(比如1、2、3、4)。這些教程大多是展示如何建立簡單的元件,如何將它們渲染到 DOM。對於教授 JSX 和 props 這種基礎知識,這些教程還不錯,但是我竭力想搞清楚 React 在更寬的視野上是如何工作的 - 比如實際的單頁應用程式(SPA)。因為本系列教程涵蓋了很多素材,所以這裡我就不講解完全初學者概念了,而是假定你已經理解了如何建立和渲染至少一個元件。
這裡還有一些很好的針對初學者的指南:
React.js and How Does It Fit In With Everything Else?
Rethinking (Industry) Best Practices
React.js Introduction For People Who Know Just Enough jQuery To Get By
程式碼
本系列相關程式碼放在GitHub上。整個系列中,我們將建立一個以使用者和元件為焦點的基礎 SPA。
為簡潔起見,本系列的示例會從假設 React 和 React Router 都是從 CDN 獲取的開始。所以你不會在下面的中級示例中看到require()
import
。不過,到本課程結束前,我會引入 Webpack 和 Babel,這時候就都用 ES6了。
React-Router
React 不是一個框架,而是一個庫。因此,它不會解決一個應用程式的所有需求。React 對於建立元件,並在提供管理狀態的系統方面做的很好。但是,建立一個更復雜的 SPA 需要一些配角。這裡我們要研究的就是配角之一:React Router.
如果以前你曾經用過任何前端路由器,那麼應該已經熟悉了很多概念。但是 React Router 與我以前曾經用過的任何其它路由器都不同,它用 JSX,這玩意開始看起來會有點奇怪。
作為入門,如下是如何渲染一個元件的示例程式碼:
var Home = React.createClass({
render: function() {
return (<h1>Welcome to the Home Pageh1>);
}
});
ReactDOM.render((
<Home />
), document.getElementById('root'));
如下是Home
元件用 React Router 是如何渲染的:
...
ReactDOM.render((
<Router>
<Route path="/" component={Home} />
Router>
), document.getElementById('root'));
注意,這裡和
是兩個不同的東西。從技術上講,二者都是 React 元件,但是它們自己實際上都不會建立 DOM。看起來好像
本身被渲染為
'root'
,但是實際上我們只是定義應用程式如何工作的規則。繼續下去的話,你會經常看到這個概念:元件有時候並非為自己建立為 DOM 而存在,而是協調建立 DOM 的其它元件。
在本例中,定義了一個規則:訪問主頁(
/)
的地方,會渲染Home
元件為'root'
。
多個 Route
前面的示例中,只有一個路由,這很簡單。它並沒有給我們更多的價值,因為我們不用路由器就可以渲染Home
元件。React Router 的強大來自於:我們可以使用多個路由來定義根據當前活動的路徑渲染哪個元件。
ReactDOM.render((
<Router>
<Route path="/" component={Home} />
<Route path="/users" component={Users} />
<Route path="/widgets" component={Widgets} />
Router>
), document.getElementById('root'));
當 路徑(path)匹配 URL 時,每個會渲染各自的元件。這三個元件中只有一個會在任何給定時間渲染到
'root'
中。使用這種策略,我們一次就把路由器掛載到 DOM 的'root'
上,然後路由器就根據路徑改變切換元件的進出。
還要指出的是,路由器不用向伺服器發起請求就會切換路由,所以可以把每個元件假想為一個完整的新頁面。
可重用的佈局
我們現在看到的是單頁應用程式最寒磣的開始。但是,它依然不能解決實際的問題。確實,你可以建立這三個元件來組成完整的 HTML 頁面,但是要程式碼重用該怎麼辦?機會是,這三個元件共享相同的部件,比如 header 和 sidebar,所以我們如何防止每個元件中的 HTML 重複呢?
假設我們正在建立一個由如下介面原型組成的 Web 應用程式:
一個簡單的網站原型
當你開始思考如何將這個原型分拆成可重用的部分時候,最後你可能會有如下的分拆:
將一個簡單的 Web 原型分成多個部分
考慮在巢狀元件和佈局方面會讓我們建立可重用的部分。
突然,設計部門讓你知道應用程式需要需要一個搜尋部件頁,該頁由搜尋使用者頁面組成。User List和Widget List都需要搜尋頁面有相同的外觀,那麼現在將Search Layout作為一個單獨的元件就更有意義:
搜尋元件取代搜尋使用者頁,但是父介面部分不變
Search Layout現在可以是所有搜尋頁面型別的父模板。並且在一些頁面需要Search Layout的同時,其他的頁面可以直接使用Main Layout,而不需要Search Layout:
解耦了的佈局
這是一種常見的策略,如果用過任何模板系統,你可能也做過很相似的事情。現在我們開始寫 HTML。開始我們只寫靜態的 HTML,不用考慮 JavaScript:
<div id="root">
<div class="app">
<header class="primary-header"><header>
<aside class="primary-aside">aside>
<main>
<div class="search">
<header class="search-header">header>
<div class="results">
<ul class="user-list">
<li>Danli>
<li>Ryanli>
<li>Michaelli>
ul>
div>
<div class="search-footer pagination">div>
div>
main>
div>
div>
記住,’root’元素總是存在的,因為它是 JavaScript 啟動前初始 HTML Body 唯一的元素。這個 'root' 是恰當的,因為整個 React 應用程式都會掛載到它上面。但是沒有恰當的名稱或者慣例來稱呼它,所以我選擇用 'root',而且會在整個示例中繼續使用它。只是要注意:直接掛載到元素是絕對不提倡的。
建立完靜態 HTML 之後,把它轉換為 React 元件:
var MainLayout = React.createClass({
render: function() {
// Note the `className` rather than `class`
// `class` is a reserved word in JavaScript, so JSX uses `className`
// Ultimately, it will render with a `class` in the DOM
return (
{this.props.children}
);
}
});
var SearchLayout = React.createClass({
render: function() {
return (
{this.props.children}
);
}
});
var UserList = React.createClass({
render: function() {
return (
Dan
Ryan
Michael
);
}
});
不要被我稱為“佈局”和“元件”這事上過於分心。這三個都是 React 元件。我稱其中兩個為“佈局”,只是因為這是它們執行的職責。
最終我們會用巢狀的 route 將UserList
放到SearchLayout
中去,然後將SearchLayout
放到MainLayout
中去。但是首先,注意到當UserList
被放到它的父元件SearchLayout
中時,父元件會用this.props.children
來判斷UserList
的位置。所有的元件都有this.props.children
作為一個 prop,但是隻有元件是巢狀的時,父元件才會被 React 自動填充這個 prop。對於沒有父元件的元件,this.props.children
將是null
。
巢狀的 Route
那麼,我們如何才能讓這些元件巢狀呢?當我們巢狀 route 時,router 就為我們做了:
ReactDOM.render((
<Router>
<Route component={MainLayout}>
<Route component={SearchLayout}>
<Route path="users" component={UserList} />
Route>
Route>
Router>
), document.getElementById('root'));
元件將會與路由器巢狀它的 route 一樣巢狀。當用戶訪問/users
路由時,React Reater 會將userList
元件放在SearchLayout
裡面,然後二者都放在MainLayout
裡面。訪問/users
的最終結果是三個巢狀的元件放在‘根‘
裡面。
注意,為簡化起見,前面我們還沒有為使用者訪問主頁路徑(/
)或者想搜尋部件時設定規則。現在我們可以把它們放進來:
ReactDOM.render((
<Router>
<Route component={MainLayout}>
<Route path="/" component={Home} />
<Route component={SearchLayout}>
<Route path="users" component={UserList} />
<Route path="widgets" component={WidgetList} />
Route>
Route>
Router>
), document.getElementById('root'));
你可能已經注意到了,JSX 在某種程度上是遵循 XML 規則的,Route
元件要麼用一個標記寫,要麼是用
...
兩個標記寫。所有的 JSX 都是這樣的,包括自定義元件和普通的 DOM 節點。比如,是有效的 JSX,並且在渲染時會被渲染為
。
為簡潔起見,假設WidgetList
與UserList
相似。
因為現在有兩個路徑了,使用者就可以訪問
/users
或者/widgets
,對應的會載入各自的元件到
SearchLayout
元件。
同時,注意到,Home
元件將會被直接放到MainLayout
裡面,而沒有包含SearchLayout
,這是因為被巢狀的方式。你可能會想到通過重新安排 route,可以重新安排佈局和元件的巢狀。
IndexRoutes
React Route 是很富有表現力的,並且經常有多種方法做相同的事情。例如,我們也可以像如下這樣寫上面的路由器:
ReactDOM.render((
<Router>
<Route path="/" component={MainLayout}>
<IndexRoute component={Home} />
<Route component={SearchLayout}>
<Route path="users" component={UserList} />
<Route path="widgets" component={WidgetList} />
Route>
Route>
Router>
), document.getElementById('root'));
儘管這跟前面的看起來不同,但是二者都是以相同的方式工作的。
可選的 Route 屬性
有時,沒有
path
屬性,但是有component
屬性,就像上面SearchLayout
中的路徑。有時,又需要有
path
屬性,但是沒有component
屬性。為什麼會這樣,我們來看一個示例:
<Route path="product/settings" component={ProductSettings} />
<Route path="product/inventory" component={ProductInventory} />
<Route path="product/orders" component={ProductOrders} />
這裡path
的/product
部分是重複的。我們可以將所有三個路徑封裝到一個新的中,從而去掉重複:
<Route path="product">
<Route path="settings" component={ProductSettings} />
<Route path="inventory" component={ProductInventory} />
<Route path="orders" component={ProductOrders} />
Route>
這裡,React Router 再次展示了它的表現力。小測驗:你注意到這兩種解決方案的問題了麼?當用戶訪問/product
路徑時,沒有定義規則。
為修正這個問題,我們可以新增一個IndexRoute
:
<Route path="product">
<IndexRoute component={ProductProfile} />
<Route path="settings" component={ProductSettings} />
<Route path="inventory" component={ProductInventory} />
<Route path="orders" component={ProductOrders} />
Route>
用
而不要用
當為路徑建立錨點時,必須用而不是
。但是不要擔心,當使用
元件時,React Router 最終會在 DOM 中給一個普通的錨點。使用
對於 React Router 發揮它的路由魔力來說是必須的。
下面我們給MainLayout
新增點連結(錨點):
var MainLayout = React.createClass({
render: function() {
return (
<div className="app"><header className="primary-header">header><aside className="primary-aside"><ul><li><Link to="/">HomeLink>li><li><Link to="/users">UsersLink>li><li><Link to="/widgets">WidgetsLink>li>ul>aside><main>
{this.props.children}main>div>
);
}
});
元件上的屬性會被傳遞給它們建立的錨點上。所以這段 JSX:
`to="/users" className="users">`
會變成 DOM 中的:
`"/users" class="users">`
如果需要為非路由器路徑建立一個錨點,比如一個外部網站,那麼就用普通的錨點標記好了。更多資訊,請參考IndexRoute 和 Link 的文件.
活動連結
元件的一個很酷的功能是能夠知道什麼時候它是活動的:
`to="/users" activeClassName="active">Users`
如果使用者是在/users
路徑上,那麼路由器就會查詢做的匹配的錨點,並且會切換它們的
active
類。更多功能在這裡.
瀏覽器歷史
為避免混淆,我把一些重要的細節留到現在。需要知道要採用哪個歷史跟蹤策略。React Router 文件推薦的瀏覽器歷史是按照如下的方法實現的:
var browserHistory = ReactRouter.browserHistory;
ReactDOM.render((
<Router history={browserHistory}>
...Router>
), document.getElementById('root'));
在前面版本的 React Router 中,history
屬性不是必需的,預設是使用hashHistory。如名字所建議的,它在 URL 中使用#
雜湊符號來管理前端 SPA 風格的路由,與在 Backbone.js 路由器中的類似。
使用hashHistory
,URL 看起來將會是這樣的:
example.com
example.com/#/users?_k=ckuvup
example.com/#/widgets?_k=ckuvup
但是這些醜陋的查詢字串到底是什麼啊?
當browserHistory
被實現時,這些路徑看起來更有組織:
example.com
example.com/users
example.com/widgets
但是當browserHistory
被用在前端時,在伺服器上有一個告誡:如果使用者開始他們在example.com
上的訪問,然後導航到/users
和/widgets
,React Router 會像期待的那種處理這種場景;但是,如果使用者直接通過在瀏覽器中鍵入example.com/widgets
或者在example.com/widgets
上重新整理來開始他們的訪問,那麼瀏覽器至少會發起一次為/widgets
對伺服器的請求。但是如果這不是一個伺服器端的路由器,這就會得到一個 404 錯誤:
當心 URL。你可能會需要一個伺服器端路由器。
要解決來自伺服器的 404 問題,React Router推薦在伺服器端使用一個萬用字元路由器。使用這種策略的話,不管呼叫的是什麼伺服器端路由,伺服器會總是提供相同的 HTML 檔案。然後,如果使用者直接從example.com/widgets
開始,即使返回的是相同的 HTML 檔案,React Router 也會足夠聰明地載入正確的元件。
使用者是不會注意到任何怪異的事情的,但是你也許會介意總是返回相同的 HTML 檔案。在程式碼示例中,本系列教程會繼續使用"萬用字元路由器"策略,但是這取決於你以你認為合適的方式來處理伺服器端路由。
那麼 React Router 能不能以一種同型(isomorphic)的方式用在伺服器端和客戶端?它當然能,但是這超出來本教程的範圍。
用browserHistory
重定向
browserHistory
是一個單例物件,所以你可以將它包含在任何檔案中。如果你需要在任何程式碼中手動重定向使用者,你可以使用它的push
方法來實現:
`browserHistory.push('/some/path');`
路由匹配
React router 處理路由匹配的方法與其它路由器相似:
`<Route path="users/:userId" component={UserProfile} />`
這個路由會匹配當用戶訪問任何以users/
開頭,後面跟著任意值的路徑。它會匹配/users/
、/users/143
,甚至是/users/abc
(如果是這樣你將需要自己校驗)。
React Router 會將:userId
的值作為 prop 傳遞給UserProfile
。這個屬性可以通過UserProfile
內的this.props.params.userId
訪問。
路由器演示
至此,我們有足夠的程式碼來演示。
檢視CodePen上,Brad Westfall (@bradwestfall) 的React-Router Demo。
如果點選示例中的一些路由,你會注意到瀏覽器的後退和前進按鈕對路由器是起作用的。這也是這些history
策略存在的一個主要原因。此外,記住對於你訪問的每個路由,除了最開始要獲取初始 HTML 外,就沒有其它向伺服器發起的請求。很酷是吧?
ES6
在我們的 CodePen 示例中,React
、ReactDOM
和ReactRouter
都是來自 CDN 的全域性變數。ReactRouter
物件內都是我們需要的各種東西,比如Router
和Route
元件。所以我們可以像這樣使用ReactRouter
:
ReactDOM.render((
<ReactRouter.Router>
<ReactRouter.Route ... />
ReactRouter.Router>
), document.getElementById('root'));
這裡,我們不得不在路由器元件前面加上它們的父物件ReactRouter
作為字首。我們還可以像下面這樣,用 ES6 新的解構語法:
`var { Router, Route, IndexRoute, Link } = ReactRouter`
這樣子就把ReactRouter
的各部分提取到普通變數中,這樣我們就可以直接訪問它們了。
從現在開始,本系列教程中的示例就開始使用 ES6 語法了,包括解構、擴充套件運算子、import、export,或許還有其它的。。本系列文章中,每個新語法出現的時候就會有一個簡要的解釋,本系列的附帶的 GitHub 程式碼庫中也有很多 ES6 解釋。
用 Webpack 和 Babel 打包
如前所述,本系列教程帶有一個GitHub程式碼庫,這樣你就可以體驗一下程式碼。因為它會類似於真實 SPA 的建立,所以會使用webpack和Babel這樣的工具。
webpack將多個 JS 檔案為瀏覽器打包到一個檔案。
Babel會將 ES6(ES2015)程式碼轉換為 ES5,因為很多瀏覽器還不能理解 ES6。
如果你對使用這些工具感到不舒服,不要擔心,示例程式碼已經把所有事情設定好了,你只需要關注 React 就行了。但是確保要檢視示例程式碼的readme.md檔案,看看附加的工作流文件。
小心已經被棄用的語法
網上很多有關 React Router 的文章都是 pre-1.0 版本的。現在很多 pre-1.0 的功能被棄用了。如下是一個簡單的列表:
被棄用。用
替代。
被棄用。用
替代。
被棄用。看可選的
被棄用。
willTransitionTo
被棄用。看onEnterwillTransitionFrom
被棄用。看onLeave"Locations" 現在叫 "histories".
參見1.0.0和2.0.0完整列表。
總結
還有很多 React Router 的功能還沒有展示,所以要看看API 文件。React Router 的發明人也建立了一個循序漸進的 React Router 教程,還可以看看他在React.js Conf 上講解他是如何建立 React Router 的視訊。
鳴謝 Lynn Fisher 為本文做的插圖@lynnandtonic。