1. 程式人生 > 其它 >react 獲取完整路徑_React 進階: React Router

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 應用程式:

12368a31-0a5e-eb11-8da9-e4434bdf6706.svg一個簡單的網站原型

當你開始思考如何將這個原型分拆成可重用的部分時候,最後你可能會有如下的分拆:

14368a31-0a5e-eb11-8da9-e4434bdf6706.svg將一個簡單的 Web 原型分成多個部分

考慮在巢狀元件和佈局方面會讓我們建立可重用的部分。

突然,設計部門讓你知道應用程式需要需要一個搜尋部件頁,該頁由搜尋使用者頁面組成。User List和Widget List都需要搜尋頁面有相同的外觀,那麼現在將Search Layout作為一個單獨的元件就更有意義:

16368a31-0a5e-eb11-8da9-e4434bdf6706.svg搜尋元件取代搜尋使用者頁,但是父介面部分不變

Search Layout現在可以是所有搜尋頁面型別的父模板。並且在一些頁面需要Search Layout的同時,其他的頁面可以直接使用Main Layout,而不需要Search Layout:

17368a31-0a5e-eb11-8da9-e4434bdf6706.svg解耦了的佈局

這是一種常見的策略,如果用過任何模板系統,你可能也做過很相似的事情。現在我們開始寫 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,並且在渲染時會被渲染為

為簡潔起見,假設WidgetListUserList相似。

因為現在有兩個路徑了,使用者就可以訪問/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 錯誤:

dc4c9efd12a52ca6d199360fd5ed30b1.gif

當心 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 示例中,ReactReactDOMReactRouter都是來自 CDN 的全域性變數。ReactRouter物件內都是我們需要的各種東西,比如RouterRoute元件。所以我們可以像這樣使用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被棄用。看onEnter

  • willTransitionFrom被棄用。看onLeave

  • "Locations" 現在叫 "histories".

參見1.0.0和2.0.0完整列表。

總結

還有很多 React Router 的功能還沒有展示,所以要看看API 文件。React Router 的發明人也建立了一個循序漸進的 React Router 教程,還可以看看他在React.js Conf 上講解他是如何建立 React Router 的視訊。

鳴謝 Lynn Fisher 為本文做的插圖@lynnandtonic。