React躬行記(13)——React Router

  在網絡工程中,路由能保證信息從源地址傳輸到正確地目的地址,避免在互聯網中迷失方向。而前端應用中的路由,其功能與之相似,也是保證信息的準確性,只不過來源變成URL,目的地變成HTML頁面。html

  在傳統的前端應用中,每一個HTML頁面都會對應一條URL地址,當訪問某個頁面時,會先請求服務器,而後服務器根據發送過來的URL作出處理,再把響應內容回傳給瀏覽器,最終渲染整個頁面。這是典型的多頁面應用的訪問過程,由服務器控制頁面的路由,而其中最使人詬病的是整頁刷新,不只存在着資源的浪費(像導航欄、側邊欄等通用部分不須要每次加載),而且讓用戶體驗也變得再也不流暢。前端

  爲了彌補多頁面應用的不足,有人提出了另外一種網站模型:單頁面應用(Single Page Application,簡稱SPA)。SPA相似於一個桌面應用程序,能根據URL分配控制器(即由JavaScript負責路由),動態加載適當的內容到頁面中,減小與服務器之間的通訊次數,再也不由於頁面切換而打斷用戶體驗。雖然名稱中包含「單頁」兩字,但瀏覽器中的URL地址仍是會發生改變,在視覺上與多頁面保持同步。而實現SPA的關鍵就是路由系統,在React的技術棧中,官方給出了支持的路由庫:React Router,後文將會着重分析該庫。react

  固然,SPA也存在着自身的缺陷,例如不利於SEO、增長開發成本等,使用與否仍是得看具體項目。git

1、版本

  在2015年的11月,官方發佈了React Router的第一個版本,實現了聲明式的路由。隨後在2016年,主版本號進行了兩次升級,一次是在2月的v2;另外一次是在10月的v3。v3可以兼容v2,刪除了一些會引發警告的棄用代碼,在將來只修復錯誤,全部的新功能都被添加到了2017年3月發佈的v4版本中。github

  v4不能兼容v3,在內部徹底重寫,推崇組件式應用開發,放棄了以前的靜態路由而改爲動態路由的設計思路。所謂靜態路由是指事先定義好一堆路由配置,在應用啓動時,再將其加載,從而構建出一張路由表,記錄URL和組件之間的映射關係。雖然v4版本精簡了許多API,下降了學習成本,可是增長了項目升級的難度。正則表達式

  目前最新的版本已到v5,但官方團隊原本只是想發佈v4.4版本。因爲人爲的操做失誤,致使不得不撤銷v4.4,直接改爲v5,所以其API能徹底兼容v4.x版本。React Router被拆分紅了4個庫(包),如表3所列。redux

表3  React Router的四個庫瀏覽器

描述
react-router 提供核心的路由組件、對象與函數
react-router-dom 提供瀏覽器所需的特定組件
react-router-native 提供React Native所需的特定組件
react-router-config 提供靜態路由的配置

  當運行在瀏覽器環境中時,只須要安裝react-router-dom便可。由於react-router-dom會依賴react-router,因此默認就能使用react-router提供的API。服務器

  v5版本的React Router提供了三大類組件:路由器、路由和導航,將它們組合起來就能實現一套完整的路由系統,如圖11所示。首先根據URL導航到路由器中相應的路由,而後再渲染出指定的組件。網絡

圖11  路由系統

2、路由器

  Router是React Router提供的基礎路由器組件,通常不會直接使用。在瀏覽器運行環境中,一般引用的是封裝了Router的高級路由器組件:BrowserRouter或HashRouter。以BrowserRouter爲例,其部分源碼以下所示。

class BrowserRouter extends React.Component {
  history = createBrowserHistory(this.props);
  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

  在v4.x的版本中,路由器組件能夠包裹任意類型的子元素,但數量只能是一個,而在v5.0版本中已經解除了這個限制。下面的BrowserRouter組件包含了兩個子元素,若是將其執行於v4.x中,那麼將拋出錯誤。

<BrowserRouter>
  <div>1</div>
  <div>2</div>
</BrowserRouter>

1)history

  每一個路由器組件都會建立一個history對象,由它來管理會話歷史。history不但會監聽URL的變化,還能將其解析成location對象,觸發路由的匹配和相應組件的渲染。

  history有三種形式,各自對應一種建立函數,應用於不一樣的路由器組件,具體如表4所示。其中MemoryRouter適用於非瀏覽器環境,例如React Native。

表4  history的三種形式

形式 建立函數 路由器組件
browserHistory createBrowserHistory() BrowserRouter
hashHistory createHashHistory() HashRouter
memoryHistory createMemoryHistory() MemoryRouter

  history會將瀏覽過的頁面組織成有序的堆棧,不管使用哪一種history,其屬性和方法大部分都能保持一致。表5列出了history通用的API。

表5  history的屬性和方法

屬性和方法 描述
length 堆棧長度
action 執行的動做,例如PUSH、REPLACE等
location 一個對象,保存着當前頁面的狀態和URL信息,形式以下代碼所示,其中state屬性來自於push()或replace()的state參數。注意,在hashHistory中沒有key和state屬性
push(path, [state]) 在棧頂添加一條新的頁面記錄
replace(path, [state]) 替換當前的頁面記錄
go(number) 跳轉到指定頁
goBack() 上一頁,至關於go(-1)
goForward() 下一頁,至關於go(1)
block(prompt) 阻止跳轉
{
  key: "z4ihbf",            //惟一標識
  pathname: "/libs/d.html"  //路徑和文件名
  search: "?page=1",        //查詢字符串
  hash: "#form",            //錨點
  state: {                 //狀態對象
    count: 10            
  }
}

2)BrowserRouter

  此組件會經過HTML5提供的History來保持頁面和URL的同步,其建立的URL格式以下所示。

http://pwstrick.com/page.html

  若是使用BrowserRouter組件,那麼須要服務器配合部署。以上面的URL爲例,當頁面刷新時,瀏覽器會向服務器請求根目錄下的page.html,但根本就沒有這個文件,因而頁面就會報404的錯誤。若要避免這種狀況,就須要配置Web服務器軟件(例如Nginx、自建的Node服務器等),具體參數的配置可參考網上的資料。

  BrowserRouter組件包含5個屬性,接下來將一一講解。

  (1)basename屬性用於設置根目錄,URL的首部須要一個斜槓,而尾部則省略,例如「/pwstrick」,以下所示。

<BrowserRouter basename="/pwstrick" />
<Link to="/article" />                //渲染爲<a href="/pwstrick/article">

  (2)forceRefresh是一個布爾屬性,只有當瀏覽器不支持HTML5的History時,纔會設爲true,從而可刷新整個頁面。

  (3)keyLength屬性是一個數字,表示location.key的長度。

  (4)children屬性保存着組件的子元素,這是全部的React組件都自帶的屬性。

  (5)getConfirmation屬性是一個確認函數,可攔截Prompt組件,注入自定義邏輯。如下面代碼爲例,當點擊連接企圖離開當前頁面時,會執行action()函數,彈出裏面的確認框,其提示就是Prompt組件message屬性的值,只有點擊肯定後才能進行跳轉(即導航)。

const action = (message, callback) => {
  const allowTransition = window.confirm(message);
  callback(allowTransition);
}
<BrowserRouter getUserConfirmation={action}>
  <div>
    <Prompt message="確認要離開嗎?" />
    <Link to="page.html">首頁</Link>
  </div>
</BrowserRouter>

3)HashRouter

  此組件會經過window.location.hash來保持頁面和URL的同步,其建立的URL格式比較特殊,須要包含井號(#),以下所示。

http://pwstrick.com/#/page.html

  在使用HashRouter時,不須要配置服務器。由於服務器會忽略錨點(即#/page.html),只會處理錨點以前的部分,因此刷新上面的URL也不會報404的錯誤。

  HashRouter組件包含4個屬性,其中3個與BrowserRouter組件相同,分別是basename、children和getUserConfirmation。獨有的hashType屬性用來設置hash類型,有三個關鍵字可供選擇,以下所列。

  (1)slash:默認值,井號後面跟一個斜槓,例如「#/page」。

  (2)noslash:井號後面沒有斜槓,例如「#page」。

  (3)hashbang:採用Google風格,井號後面跟感嘆號和斜槓,例如「#!/page」。

3、路由

  Route是一個配置路由信息的組件,其職責是當頁面的URL能匹配Route組件的path屬性時,就渲染出對應的組件,而渲染方式有三種。接下來會講解Route組件的屬性、渲染方式以及其它的相關概念。

1)路徑

  與路徑相關的屬性有3個,分別是path、exact和strict,接下來會一一講解。

  (1)path是一個記錄路由匹配路徑的屬性,當路由器是BrowserRouter時,path會匹配location中的pathname屬性;而當路由器是HashRouter時,path會匹配location中的hash屬性。

  path屬性的值既能夠是普通字符串,也能夠是能被path-to-regexp解析的正則表達式。下面是一個示例,若是沒有特殊說明,默認使用的路由器是BrowserRouter。

<Route path="/main" component={Main} />
<Route path="/list/:page+" component={List} />

  第一個Route組件能匹配「/main」或以「/main」爲前綴的pathname屬性,下面兩條URL能正確匹配。

http://www.pwstrick.com/main
http://www.pwstrick.com/main/article

  第二個Route組件能匹配以「/list」爲前綴的pathname屬性,下面兩條URL只能匹配第二條。

http://www.pwstrick.com/list
http://www.pwstrick.com/list/1

  React Router內部依賴了path-to-regexp庫,此庫定義了一套正則語法,例如命名參數、修飾符(*、+或?)等,具體規則可參考官方文檔,本文不作展開。

  在「/list/:page+」中,帶冒號前綴的「:page」是命名參數,相似於一個函數的形參,能夠傳遞任何值;正則末尾的加號要求至少匹配一個命名參數,沒有命名參數就匹配失敗。

  注意,若是省略path屬性,那麼路由將老是匹配成功。

  (2)exact是一個布爾屬性,當設爲true時,路徑要與pathname屬性徹底匹配,如表6所示。

表6  exact屬性匹配說明

路徑 pathname屬性 exact屬性 是否匹配
/main /main/article true
/main /main/article false

  (3)strict也是一個布爾屬性,當設爲true時,路徑末尾若是有斜槓,那麼pathname屬性匹配到的部分也得包含斜槓。在表7的第三行中,雖然pathname屬性的末尾沒有斜槓,可是依然能正確匹配。

表7  strict屬性匹配說明

路徑 pathname屬性 strict屬性 是否匹配
/main/ /main true
/main/ /main/ true
/main/ /main/article true

  若是將strict和exact同時設爲true,那麼就可強制pathname屬性的末尾不能包含斜槓。例如pathname屬性的值爲「/main/」,路徑爲「/main」,此時匹配會失敗。

2)渲染方式

  Route組件提供了3個用來渲染組件的屬性:component、render和children,每一個屬性對應一種渲染方式,每種方式傳遞的props都會包含3個路由屬性:match、location和history。

  (1)component屬性的值是一個組件(以下代碼所示),當路由匹配成功時,會建立一個新的React元素(調用了React.createElement()方法)。

<Route path="/name" component={Name} />

  若是組件之內聯函數的方式傳給component屬性,那麼會產生沒必要要的從新掛載。對於內聯渲染,能夠用render屬性替換。

  (2)render屬性的值是一個返回React元素的內聯函數,當路由匹配成功時,會調用這個函數,此時能夠傳遞額外的參數進來,以下代碼所示。因爲React元素不會被反覆建立,所以不會出現從新掛載的狀況。

<Route path="/name" render={(props) => {
  return <Name {...props} age="30">Strick</Name>
}}/>

  (3)children屬性的值也是一個返回React元素的內聯函數,它的一大特色是不管路由是否匹配成功,這個函數都會被調用,該屬性的工做方式與render屬性基本一致。注意,當匹配不成功時,props的match屬性的值爲null。

  不要將3個渲染屬性應用於同一個Route組件,由於三種渲染方式有前後順序,component的優先級最高,其次是render,最後是children。

  三個路由屬性除了match以外,另外兩個location和history已在前文作過講解,接下來將重點分析match屬性。

  Route會將路由匹配後的信息記錄到match對象中,而後將此對象做爲props的match屬性傳遞給被渲染的組件。match對象包含4個屬性,在表8中,不只描述了各個屬性的做用,還在第三列記錄了點擊read連接後,各個屬性被賦的值。

<Link to="/list/article/1">read</Link>
<Route path="/list/:type" component={Name} />

表8  match對象的屬性

屬性 描述 示例中的值
params 由路徑的參數名和解析URL匹配到的值組成的對象 {type: "article"}
isExact 是否徹底匹配,等同於Route的exact屬性 false
path 要匹配的路徑,等同於Route的path屬性 "/list/:type"
url 匹配到的URL部分 "/list/article"

3)Switch

  若是將一堆Route組件放在一塊兒(以下代碼所示),那麼會對每一個Route組件依次進行路由匹配,例如當前pathname的屬性值是「/age」,那麼被渲染的組件是Age1和Age3。

<Route path='/' component={Age1} />
<Route path='/article' component={Age2} />
<Route path='/:list' component={Age3} />

  而若是將這三個Route用Switch組件包裹(以下代碼所示),那麼只會對第一個路徑匹配的組件進行渲染。

<Switch>
  <Route path='/' component={Age1} />
  <Route path='/article' component={Age2} />
  <Route path='/:list' component={Age3} />
</Switch>

  Switch的子元素既能夠是Route,也能夠是Redirect。其中Route元素匹配的是path屬性,而Redirect元素匹配的是from屬性。

4)嵌套路由

  從v4版本開始,嵌套路由再也不經過多個Route組件相互嵌套實現,而是在被渲染的組件中聲明另外的Route組件,以這種方式實現嵌套路由。下面用一個例子來演示嵌套路由,首先用Switch組件包裹兩個Route組件,第一個只有當處在根目錄時纔會渲染Main組件,第二個路徑匹配成功渲染的是Children組件。

<Switch>
  <Route exact path='/' component={Main} />
  <Route path='/list/:article' component={Children} />
</Switch>

  而後定義Children組件,它也包含一個Route組件,從而造成了嵌套路由。注意,其路徑讀取了match對象的path屬性,經過沿用父路由中要匹配的路徑,可減小許多重複代碼。

let Children = (props) => {
  return <Route path={`${props.match.path}/:id`} component={Article} />;
};
let Article = (props) => {
  return <h5>文章內容</h5>;
};

  當pathname的屬性值是「/list/article/1」時,就能成功渲染出Article組件。

4、導航

  當須要在頁面之間進行切換時,就該輪到Link、NavLink和Redirect三個導航組件登場了。其中Link和NavLink組件最終會被解析成HTML中的<a>元素。

1)Link

  當點擊Link組件時會渲染匹配路由中的組件,而且能在更新URL時,不重載頁面。它有兩個屬性:to和replace,其中to屬性用於定義導航地址,其值的類型既能夠是字符串,也能夠是location對象(包含pathname、search等屬性),以下所示。

<Link to="/main">字符串</Link>
<Link to={{pathname: "/main", search: "?type=1"}}>對象</Link>

  replace是一個布爾屬性,默認值爲false,當設爲true時,能用新地址替換掉會話歷史裏的原地址。

2)NavLink

  它是一個封裝了的Link組件,其功能包括定義路徑匹配成功後的樣式、限制匹配規則、優化無障礙閱讀等,接下來將依次講解多出的屬性。

  首先是activeClassName和activeStyle,兩個屬性都會在路徑匹配成功時,賦予元素樣式(以下代碼所示)。其中前者定義的是CSS類,默認值爲「active」;後者定義的是內聯樣式,書寫規則可參照React元素的style屬性。

<style>
  .btn {
    color: blue;
  }
</style>
<NavLink to="/list" activeClassName="btn">CSS類</NavLink>
<NavLink to="/list" activeStyle={{color: "blue"}}>內聯樣式</NavLink>

  而後是exact和strict,兩個布爾屬性的功能可分別參考Route元素的exact和strict,它們的用法相同。若是將exact和strict設爲true(以下代碼所示),那麼匹配規則會改變,其中前者要路徑徹底匹配,後者得符合strict的路徑匹配規則。只有當匹配成功時,才能將activeClassName或activeStyle屬性的值賦予元素。

<NavLink to="/list" exact>徹底</NavLink>
<NavLink to="/list" strict>斜槓</NavLink>

  接着是函數類型的isActive屬性,此函數能接收2個對象參數:match和location,返回一個布爾值。在函數體中可添加路徑匹配時的額外邏輯,當返回值是true時,才能賦予元素定義的匹配樣式。注意,不管匹配是否成功,isActive屬性中的函數都會被回調一次,所以若是要使用match參數,那麼須要作空值判斷(以下代碼所示),以避免出錯。

let fn = (match, location) => {
  if (!match) {
    return false
  }
  return match.url.indexOf("article") >= 0;
};
<NavLink to="/list" isActive={fn}>函數</NavLink>

  最後是兩個特殊功能的屬性:location和aria-current,前者是一個用於比對的location對象;後者是一個爲存在視覺障礙的用戶服務的ARIA屬性,用於標記屏幕閱讀器可識別的導航類型,例如頁面、日期、位置等。可供選擇的關鍵字包括page、step、location、date、time和true,默認值爲page。

3)Redirect

  此組件用於導航到一個新地址,相似於服務端的重定向(HTTP的狀態碼爲3XX),其屬性如表9所示。

表9  Redirect元素的屬性

屬性 描述
to 重定向的目標地址,既能夠是字符串,也能夠是location對象
from 要重定向的路徑,只有匹配成功時,才能跳轉到to屬性中的目標地址
push 布爾屬性,當設爲true時,重定向的新地址將會加入到會話歷史中

  Redirect可與Switch搭配使用,以下代碼所示,當URL與「/main」匹配時,重定向到「/page」,並渲染Page組件。

<Switch>
  <Redirect from="/main" to="/page" />
  <Route path="/page" component={Page} />
</Switch>

5、集成Redux

  第11篇中對Redux作過詳細講解,本節將經過一個示例分三步來描述React Router集成Redux的過程,第一步是建立Redux的三個組成部分:Action、Reducer和Store,以下所示。

function caculate(previousState = {digit: 0}, action) {        //Reducer
  let state = Object.assign({}, previousState);
  switch (action.type) {
    case "ADD":
      state.digit += 1;
      break;
    case "MINUS":
      state.digit -= 1;
  }
  return state;
}
function add() {                      //Action建立函數
  return {type: "ADD"};
}
let store = createStore(caculate);        //Store

1)withRouter

  在說明第二步以前,須要先了解一下React Router提供的一個高階組件:withRouter。它能將history、location和match三個路由對象傳遞給被包裝的組件,其中match對象來自於離它最近的父級Route組件的match屬性。

  正常狀況下,只有Route要渲染的組件(例以下面的List)會自帶這三個對象,但若是List組件還有一個子組件,那麼這個子組件就沒法自動獲取到這三個對象了,除非顯式地傳遞。

<Route path="/" component={List} />

  在使用withRouter後,就能避免逐級傳遞。而且當把withRouter應用於react-redux庫中的connect()函數後(以下代碼所示),就能讓函數返回的容器組件監聽到路由的變化。

withRouter(connect(...)(MyComponent))

2)路由

  第二步就是建立路由,並自定義三個組件:Btn、List和Article。在Btn組件中聲明瞭Link和Route兩個組件,其中路由匹配成功後會渲染List組件;在List組件中聲明瞭WithArticle組件,而WithArticle就是經過withRouter包裝後的Article組件。

class Btn extends React.Component {
  render() {
    return (
      <div>
        <Link to="/list">列表</Link>
        <Route path="/list" component={List} />
        <button onClick={this.props.add}>提交</button>
      </div>
    );
  }
}
let List = (props) => {
  return <WithArticle content="內容"/>;
};
let Article = (props) => {
  const { match, location, history } = props;
  return <h5>{props.content}</h5>;
};
let WithArticle = withRouter(Article);    //withRouter包裝後的Article組件

3)渲染

  第三步就是用react-redux庫中的Provider組件包裹BrowserRouter組件(即鏈接路由器),並注入Store,最後將衆組件渲染到頁面中。

let Smart = connect(state => state, { add })(Btn);        //容器組件
let Router = <Provider store={store}>
  <BrowserRouter>
    <Smart />
  </BrowserRouter>
</Provider>;
ReactDOM.render(Router, document.getElementById("container"));
相關文章
相關標籤/搜索