React 進階第一部分 : React Router

原文地址:css-tricks.com/learning

本文是 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 |