React 進階第一部分 : React Router
原文地址:https://css-tricks.com/learning-react-router/
本文是 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({n render: function() {n return (<h1>Welcome to the Home Page</h1>);n }n});nnReactDOM.render((n <Home />n), document.getElementById(root));n
如下是 Home 組件用 React Router 是如何渲染的:
...nnReactDOM.render((n <Router>n <Route path="/" component={Home} />n </Router>n), document.getElementById(root));n
注意,這裡 <Router> 和 <Route> 是兩個不同的東西。從技術上講,二者都是 React 組件,但是它們自己實際上都不會創建 DOM。看起來好像 <Router> 本身被渲染為 root,但是實際上我們只是定義應用程序如何工作的規則。繼續下去的話,你會經常看到這個概念:組件有時候並非為自己創建為 DOM 而存在,而是協調創建 DOM 的其它組件。
在本例中,<Route> 定義了一個規則:訪問主頁(/)的地方,會渲染 Home 組件為 root。
多個 Route
前面的示例中,只有一個路由,這很簡單。它並沒有給我們更多的價值,因為我們不用路由器就可以渲染 Home 組件。React Router 的強大來自於:我們可以使用多個路由來定義根據當前活動的路徑渲染哪個組件。
ReactDOM.render((n <Router>n <Route path="/" component={Home} />n <Route path="/users" component={Users} />n <Route path="/widgets" component={Widgets} />n </Router>n), document.getElementById(root));n
當 路徑(path)匹配 URL 時,每個 <Route> 會渲染各自的組件。這三個組件中只有一個會在任何給定時間渲染到 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">nn <!-- Main Layout -->n <div class="app">n <header class="primary-header"><header>n <aside class="primary-aside"></aside>n <main>nn <!-- Search Layout -->n <div class="search">n <header class="search-header"></header>n <div class="results">nn <!-- User List -->n <ul class="user-list">n <li>Dan</li>n <li>Ryan</li>n <li>Michael</li>n </ul>nn </div>n <div class="search-footer pagination"></div>n </div>nn </main>n </div>nn</div>n
記住,』root』元素總是存在的,因為它是 JavaScript 啟動前初始 HTML Body 唯一的元素。這個 root 是恰當的,因為整個 React 應用程序都會掛載到它上面。但是沒有恰當的名稱或者慣例來稱呼它,所以我選擇用 root,而且會在整個示例中繼續使用它。只是要注意:直接掛載到 <body> 元素是絕對不提倡的。
創建完靜態 HTML 之後,把它轉換為 React 組件:
var MainLayout = React.createClass({n render: function() {n // 注意這裡是 className 而不是 class。因為 class 是 JavaScript 的一個保留字,所以 JSX 用 className。最後,它會在 DOM 用 class 渲染n return (n <div className="app">n <header className="primary-header"><header>n <aside className="primary-aside"></aside>n <main>n {this.props.children}n </main>n </div>n );n }n});nnvar SearchLayout = React.createClass({n render: function() {n return (n <div className="search">n <header className="search-header"></header>n <div className="results">n {this.props.children}n </div>n <div className="search-footer pagination"></div>n </div>n );n }n});nnvar UserList = React.createClass({n render: function() {n return (n <ul className="user-list">n <li>Dan</li>n <li>Ryan</li>n <li>Michael</li>n </ul>n );n }n});n
不要被我稱為「布局」和「組件」這事上過於分心。這三個都是 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((n <Router>n <Route component={MainLayout}>n <Route component={SearchLayout}>n <Route path="users" component={UserList} />n </Route> n </Route>n </Router>n), document.getElementById(root));n
組件將會與路由器嵌套它的 route 一樣嵌套。當用戶訪問 /users 路由時,React Reater 會將 userList 組件放在 SearchLayout 裡面,然後二者都放在 MainLayout 裡面。訪問 /users 的最終結果是三個嵌套的組件放在 『根『 裡面。
注意,為簡化起見,前面我們還沒有為用戶訪問主頁路徑(/)或者想搜索部件時設置規則。現在我們可以把它們放進來:
ReactDOM.render((n <Router>n <Route component={MainLayout}>n <Route path="/" component={Home} />n <Route component={SearchLayout}>n <Route path="users" component={UserList} />n <Route path="widgets" component={WidgetList} />n </Route> n </Route>n </Router>n), document.getElementById(root));n
你可能已經注意到了,JSX 在某種程度上是遵循 XML 規則的,Route 組件要麼用 <Route /> 一個標記寫,要麼是用 <Route>...</Route> 兩個標記寫。所有的 JSX 都是這樣的,包括自定義組件和普通的 DOM 節點。比如,<div /> 是有效的 JSX,並且在渲染時會被渲染為 <div></div>。
為簡潔起見,假設 WidgetList 與 UserList 相似。
因為現在 <Route component={SearchLayout}> 有兩個路徑了,用戶就可以訪問 /users 或者 /widgets ,對應的 <Route> 會載入各自的組件到 SearchLayout 組件。
同時,注意到,Home 組件將會被直接放到 MainLayout 裡面,而沒有包含 SearchLayout,這是因為 <Route> 被嵌套的方式。你可能會想到通過重新安排 route,可以重新安排布局和組件的嵌套。
IndexRoutes
React Route 是很富有表現力的,並且經常有多種方法做相同的事情。例如,我們也可以像如下這樣寫上面的路由器:
ReactDOM.render((n <Router>n <Route path="/" component={MainLayout}>n <IndexRoute component={Home} />n <Route component={SearchLayout}>n <Route path="users" component={UserList} />n <Route path="widgets" component={WidgetList} />n </Route> n </Route>n </Router>n), document.getElementById(root));n
儘管這跟前面的看起來不同,但是二者都是以相同的方式工作的。
可選的 Route 屬性
有時,<Route> 沒有 path 屬性,但是有 component 屬性,就像上面 SearchLayout 中的路徑。有時,又需要 <Route> 有 path屬性,但是沒有 component 屬性。為什麼會這樣,我們來看一個示例:
<Route path="product/settings" component={ProductSettings} />n<Route path="product/inventory" component={ProductInventory} />n<Route path="product/orders" component={ProductOrders} />n
這裡 path 的 /product 部分是重複的。我們可以將所有三個路徑封裝到一個新的 <Route> 中,從而去掉重複:
<Route path="product">n <Route path="settings" component={ProductSettings} />n <Route path="inventory" component={ProductInventory} />n <Route path="orders" component={ProductOrders} />n</Route>n
這裡,React Router 再次展示了它的表現力。小測驗:你注意到這兩種解決方案的問題了么?當用戶訪問 /product 路徑時,沒有定義規則。
為修正這個問題,我們可以添加一個 IndexRoute:
<Route path="product">n <IndexRoute component={ProductProfile} />n <Route path="settings" component={ProductSettings} />n <Route path="inventory" component={ProductInventory} />n <Route path="orders" component={ProductOrders} />n</Route>n
用 <Link> 而不要用 <a>
當為路徑創建錨點時,必須用 <link to=""> 而不是 <a href="">。但是不要擔心,當使用 <link> 組件時,React Router 最終會在 DOM 中給一個普通的錨點。使用 <Link> 對於 React Router 發揮它的路由魔力來說是必須的。
下面我們給 MainLayout 添加點鏈接(錨點):
var MainLayout = React.createClass({n render: function() {n return (n <div className="app">n <header className="primary-header"></header>n <aside className="primary-aside">n <ul>n <li><Link to="/">Home</Link></li>n <li><Link to="/users">Users</Link></li>n <li><Link to="/widgets">Widgets</Link></li>n </ul>n </aside>n <main>n {this.props.children}n </main>n </div>n );n }n});n
<link> 組件上的屬性會被傳遞給它們創建的錨點上。所以這段 JSX:
<Link to="/users" className="users">n
會變成 DOM 中的:
<a href="/users" class="users">n
如果需要為非路由器路徑創建一個錨點,比如一個外部網站,那麼就用普通的錨點標記好了。更多信息,請參考IndexRoute 和 Link 的文檔.
活動鏈接
<link> 組件的一個很酷的功能是能夠知道什麼時候它是活動的:
<Link to="/users" activeClassName="active">Users</Link>n
如果用戶是在 /users 路徑上,那麼路由器就會查找 <link> 做的匹配的錨點,並且會切換它們的 active 類。更多功能在這裡.
瀏覽器歷史
為避免混淆,我把一些重要的細節留到現在。<Router> 需要知道要採用哪個 歷史 跟蹤策略。React Router 文檔 推薦的瀏覽器歷史 是按照如下的方法實現的:
var browserHistory = ReactRouter.browserHistory;nnReactDOM.render((n <Router history={browserHistory}>n ...n </Router>n), document.getElementById(root));n
在前面版本的 React Router 中,history 屬性不是必需的,默認是使用 hashHistory。如名字所建議的,它在 URL 中使用 # 哈希符號來管理前端 SPA 風格的路由,與在 Backbone.js 路由器中的類似。
使用 hashHistory,URL 看起來將會是這樣的:
http://example.com
http://example.com/#/users?_k=ckuvup
http://example.com/#/widgets?_k=ckuvup
但是這些醜陋的查詢字元串到底是什麼啊?
當 browserHistory 被實現時,這些路徑看起來更有組織:
http://example.com
http://example.com/users
http://example.com/widgets
但是當 browserHistory 被用在前端時,在伺服器上有一個告誡:如果用戶開始他們在 http://example.com 上的訪問,然後導航到 /users 和 /widgets,React Router 會像期待的那種處理這種場景;但是,如果用戶直接通過在瀏覽器中鍵入 http://example.com/widgets 或者在 http://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);n
路由匹配
React router 處理路由匹配 的方法與其它路由器相似:
<Route path="users/:userId" component={UserProfile} />n
這個路由會匹配當用戶訪問任何以 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((n <ReactRouter.Router>n <ReactRouter.Route ... />n </ReactRouter.Router>n), document.getElementById(root));n
這裡,我們不得不在路由器組件前面加上它們的父對象 ReactRouter 作為前綴。我們還可以像下面這樣,用 ES6 新的解構 語法:
var { Router, Route, IndexRoute, Link } = ReactRoutern
這樣子就把 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 的功能被棄用了。如下是一個簡單的列表:
- <Route name="" /> 被棄用。用 <Route path="" /> 替代。
- <Route handler="" /> 被棄用。用 <Route component="" /> 替代。
- <NotFoundRoute /> 被棄用。看可選的
- <RouteHandler /> 被棄用。
- willTransitionTo 被棄用。看 onEnter
- willTransitionFrom 被棄用。看 onLeave
- "Locations" 現在叫 "histories".
參見1.0.0 和 2.0.0 完整列表。
總結
還有很多 React Router 的功能還沒有展示,所以要看看 API 文檔。React Router 的發明人也創建了一個循序漸進的 React Router 教程,還可以看看他在 React.js Conf 上講解他是如何創建 React Router 的視頻。
推薦閱讀:
※前端架構技術選型?
※前端新人的迷茫?
※Youtube-dl-Download-Videos
※我對Flexbox布局模式的理解
※《深入 React 技術棧》章節試讀
TAG:React | reactrouter | reactrouterredux |