在前端框架層出不窮的今天,React 以其虛擬 DOM 、組件化開發思想等特性迅速佔據了主流位置,成爲前端開發工程師熱衷的 Javascript 庫。做爲 React 體系中的重要組成部分:React Router 也成爲開發者首選的路由庫,其主要功能是經過管理 url 實現組件的切換和狀態的變化。javascript
在 React Router 4.x 發佈以前,咱們在項目中使用的是 React Router 3.x。隨着第四版 React Router 的正式亮相,其精簡的 API 、語義化的路由匹配方案以及動態路由等變化,都彰顯着這次升級的顛覆性。因此,咱們決定在新的 React 項目中使用 4.x(React Router 4.x)開發,親身實踐以後發現: 4.x 不只是 API 的簡單改變,還有整個設計理念的變化;初次使用確實有一些彆扭,更可怕的是按照 API 文檔寫出的代碼,有時運行時會出現一些莫名其妙的紅色錯誤(此刻的心情 down 到谷底~)。爲了不你們在使用時遇到一樣的問題多繞彎子,咱們把開發過程當中遇到的問題以及相應的解決方法作了彙總。css
在以前的 3.x 中,在入口文件中定義路由時,咱們會這麼寫:html
import React from 'react'; import ReactDOM from 'react-dom'; import {Router, Route, browserHistory} from 'react-router'; import * as routePaths from './js/constants/routePaths'; import Index from './js/pages/Index'; ReactDOM.render(( <Router history={history}> <div className="container"> <Route path={routePaths.INDEX} component={Index} /> </div> </Router> ), document.getElementById('app') );
可是保存後運行發現頁面報錯了,以下圖所示:前端
解決方法:java
import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter, Route } from 'react-router-dom'; import * as routePaths from './js/constants/routePaths'; import Index from './js/pages/Index'; ReactDOM.render(( <BrowserRouter> <div className="container"> <Route path={routePaths.INDEX} component={Index} /> </div> </BrowserRouter> ), document.getElementById('app') );
頁面顯示正常,問題解決。接下來咱們對該解決方法進行詳細的解釋:node
4.x 中採用了單代碼倉庫模型架構,因此裏面包含了若干個相互獨立的包,以下所示:react
react-router
React Router 核心react-router-dom
用於 DOM 綁定的 React Routerreact-router-native
用於 React Native 的 React Routerreact-router-redux
React Router 和 Redux 的集成react-router-config
用於配置靜態路由所以,在實際的 web 項目開發中,react-router
和 react-router-dom
沒必要同時引用。在 react-router-dom
中包含相似 <BrowserRouter>
的 DOM 類組件,因此只須要引入 react-router-dom
包就能夠了。webpack
使用方法:git
import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
關於 <BrowserRouter>
是什麼?下面的問題中會有詳細介紹,莫急~github
咱們同時定義了多個模塊,當路由切換時發現這些模塊會同時渲染出來。好比像下面這樣定義:
ReactDOM.render(( <BrowserRouter> <div className="container"> <Route path="/" component={Index} /> <Route path="/card" component={Card} /> </div> </BrowserRouter> ), document.getElementById('app') );
當訪問 path="/card"
的頁面時,path="/"
的頁面也會被渲染出來。不一樣於 3.x 中路由匹配時的獨一無二特性,4.x 中有了一層包含關係:如匹配 path="/card"
的路由會匹配 path="/"
的路由。那麼這個問題怎麼解決呢?有如下兩種方法:
exact
關鍵字ReactDOM.render(( <BrowserRouter> <div className="container"> <Route path="/" exact component={Index} /> <Route path="/card" component={Card} /> </div> </BrowserRouter> ), document.getElementById('app') );
<Switch>
import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom'; ReactDOM.render(( <BrowserRouter> <Switch> <div className="container"> <Route path="/" exact component={Index} /> <Route path="/card" component={Card} /> </div> </Switch> </BrowserRouter> ), document.getElementById('app') );
完美!模塊能夠實現獨立渲染了。咱們來看下這樣解決問題的緣由:
首先須要瞭解下 <Router>
。它是全部路由組件共用的底層接口,在 4.x 中,你能夠將各類組件及標籤放進 <Router>
組件中。好比:
<Router> <div> <ul> <li><Link to="/">Home</Link></li> </ul> <hr/> <Route exact path="/" component={Home}/> <Route path="/about" component={About}/> </div> </Router>
須要注意的是: <Router>
下只容許存在一個子元素,如存在多個則會報錯。因此在上面的代碼中,須要使用 <div></div>
將其餘元素包裹起來。
在實際的項目中,咱們通常不會直接使用 <Router>
,而是使用以下所示的更高級的路由。在這裏將會介紹到第一個問題中用到的 <BrowserRouter>
以及其餘經常使用的高級路由。咳咳,集中精力了哈,重頭戲來了!
使用 HTML5 提供的 history API ( pushState
, replaceState
和 popstate
事件) 來保持 UI 和 url 的同步。下面介紹一下該路由組件中的 5 個屬性:
當前位置的基準 url
。若是你的頁面部署在服務器的二級(子)目錄,你須要將 basename
設置到此子目錄。 正確的 url
格式是前面有一個前導斜槓,但不能有尾部斜槓。
<BrowserRouter basename="/calendar"/> <Link to="/today"/> // 渲染爲 <a href="/calendar/today">
當導航須要確認時執行的函數。默認使用 window.confirm
。
// 使用默認的確認函數 const getConfirmation = (message, callback) => { const allowTransition = window.confirm(message) callback(allowTransition) } <BrowserRouter getUserConfirmation={getConfirmation}/>
當設置爲 true
時,在導航的過程當中整個頁面將會刷新。 只有當瀏覽器不支持 HTML5 的 history API 時,才設置爲 true
。
const supportsHistory = 'pushState' in window.history <BrowserRouter forceRefresh={!supportsHistory}/>
location.key
的長度,默認是 6。點擊同一個連接時,每次該路由下的 location.key
都會改變,能夠經過 key
的變化來刷新頁面。
<BrowserRouter keyLength={12}/>
渲染單一子組件(元素)。
HashRouter 是一種特定的 <Router>
, HashRouter 使用 url 的 hash
(例如: window.location.hash ) 來保持 UI 和 url 的同步。因爲使用 hash 的方式記錄導航歷史不支持 location.key
和 location.state
,該技術僅用於支持傳統的瀏覽器,此 API 再也不贅述。
這兩個是常用的,剩下的還有 <MemoryRouter>
、<NativeRouter>
、 <StaticRouter>
,有興趣的同窗能夠本身瞭解一下。
介紹完 <Router>
,固然也要說下 <Route>
,咱們使用最頻繁的組件,主要職責是當頁面的訪問地址與 Route
上的 path
匹配時渲染出對應的 UI 界面。
<Route> 屬性值主要有:
能夠被 path-to-regexp
解析的有效 url 路徑。若是沒有 path
,路由將老是被匹配。
<Route path="/users/:id" component={User}/>
爲 true
時,則要求路徑與 location.pathname
必須徹底匹配。經過下表解釋一下:
爲 true
時,有結尾斜線的路徑只能匹配有斜線的 location.pathname
。見下表:
第二個屬性值 exact
就是咱們用來解決多個模塊同時渲染問題的,默認值爲 true
。
在這裏簡要提下 <Route>
渲染的三種方法:
<Route component>
當訪問地址和路由匹配時,一個 React component
將會被渲染<Route render>
此方法適用於內聯渲染,並且不會產生重複裝載問題<Route children>
當須要判斷訪問地址與路由是否匹配時,可使用此方法。當不匹配時, match
爲 null。
咱們上面代碼示例中使用的是第一種方法 <Route component>
,這三種渲染方法都會用到 match
、location
、 history
這些屬性值。這裏再也不詳細介紹,使用時能夠查閱官方文檔。
須要注意的是 :每一種渲染方法都有其適用背景, <Route component> 的優先級比 <Route render> 高,而他們又都優先於 <Route children> ,因此在同一個 <Route> 應該只使用一種方法,咱們大多數使用的是 component 方法。
說完第一種解決方法的原理,來剖析下第二種方法: <Switch>
。
該組件只渲染第一個與當前訪問地址匹配的 <Route>
或 <Redirect>
。它和多個堆疊的 <Route> 組件之間的區別是: <Switch>
只渲染一個路由。
在解決方法 (2)中,當咱們訪問 /card
, <Switch>
將會開始尋找與之匹配的路由,查找到 <Route path="/card"/>
匹配後,<Switch>
將會中止尋找而後開始渲染 /card
。<Switch>
<Switch>
下的子節點只能是 <Route>
或 <Redirect>
元素且只有與當前訪問地址匹配的第一個子節點纔會被渲染。
當用戶手動修改 url 時,有時咱們須要定義一個默認頁面,從新引導用戶的操做,這個該如何實現呢?
使用 <Redirect>
。好比當用戶手動輸入 /test
以後,咱們須要跳轉至首頁,則代碼以下所示:
ReactDOM.render(( <BrowserRouter> <div className="container"> <Switch> <Route path={routePaths.INDEX} exact component={Index} /> <Route path={routePaths.CARD} component={Card} /> <Redirect to="/" /> </Switch> </div> </BrowserRouter> ), document.getElementById('app') );
方法解析:<Redirect>
渲染時將會導向一個新的地址,這個新的地址將會覆蓋掉 history
堆棧中的當前地址。
其經常使用的屬性是:
重定向的 url 地址。
<Redirect to="/somewhere/else"/>
重定向的 location
對象。
<Redirect to={{ pathname: '/login', search: '?utm=your+face', state: { referrer: currentLocation } }}/>
取值爲 true
時,重定向操做將會把新地址加入到訪問的歷史記錄裏面,並不會替換掉當前的地址。
<Redirect push to="/somewhere/else"/>
須要匹配的將要被重定向的路徑。
<Switch> <Redirect from='/old-path' to='/new-path'/> <Route path='/new-path' component={Place}/> </Switch>
須要注意的是:<Route>
元素使用 path
屬性進行匹配,<Redirect>
元素使用 from
屬性進行匹配。若是元素中沒有對應的 path
或 from
,那麼它們將匹配任何當前的訪問地址。
相信你們對於 <link>
組件都不陌生,在 3.x 中,當須要將當前點擊的理由置爲激活狀態時,咱們能夠這樣設置:
//經過Style進行配置 <li><Link to="/home" activeStyle={{ color: 'red' }}>Home</Link></li> //經過類名進行設置 <li><Link to="/home" activeClassName="active">Home</Link></li>
可是,通常狀況下只有主導航的連接才須要知道本身是否被激活。所以須要對主導航的 <Link>
進行一層包裝,這樣就沒必要記得哪些地方有 activeClassName
或 activeStyle
。所謂的對 <Link>
包裝就是自定義 <Link>
組件,經過自定義實現特別的 Style
。在 3.x 中,具體實現方法以下:
// modules/NavLink.js import React from 'react'; import { Link } from 'react-router'; export default class NavLink extends React.Component({ constructor(props) { super(props); } render() { return <Link {...this.props} activeClassName="active"/> } }) //使用方法: import NavLink from './NavLink' // ... <li><NavLink to="/home">Home</NavLink></li>
可是,在 4.x 中咱們不須要作這些包裝工做,由於它直接提供了 <NavLink>
組件供咱們使用。使用方法:
<NavLink to="/faq" activeClassName="selected" >FAQs</NavLink>
下面對 <NavLink>
作下簡單介紹:
該組件是 <Link>
的特殊版本,當遇到匹配的 URL 渲染元素時會添加樣式屬性,適用於頁面導航部分。
導航選中時的樣式名,默認樣式名爲 active
。
<NavLink to="/faq" activeClassName="selected" >FAQs</NavLink>
導航選中時的樣式。
<NavLink to="/faq" activeStyle={{ fontWeight: 'bold', color: 'red' }} >FAQs</NavLink>
若值爲 true
,當訪問地址嚴格匹配時激活樣式纔會生效。
<NavLink exact to="/profile" >Profile</NavLink>
若值爲 true
,只有當訪問地址後綴斜槓嚴格匹配(有或無)時激活樣式纔會生效。
<NavLink strict to="/events/" >Events</NavLink>
用於添加頁面激活時的操做邏輯。
const oddEvent = (match, location) => { if (!match) { return false } const eventID = parseInt(match.params.eventID) return !isNaN(eventID) && eventID % 2 === 1 } <NavLink to="/events/123" isActive={oddEvent} >Event 123</NavLink>
以上是 <NavLink>
組件的屬性介紹,供你們學習。
在 3.x 中,跳轉頁面可使用如下方式:
import { browserHistory } from 'react-router'; //第一種方法 browserHistory.push('/some/path'); //第二種方法 this.context.router.push('/some/path'); //第三種方法 <Link to="'/some/path'"></Link>
在 4.x 中使用 push
方法跳轉頁面時,若是按照上面的寫法,頁面會報錯:
針對該問題有如下兩種解決方法:
this.props.history.push('/some/path');
簡單介紹下 history
:
history
指的是 history
包,是 4.x 中的重要依賴之一。常見的 history
路由方案有三種形式,分別是:
React Native
)。history
對象包含的屬性和方法以下所示:
length
: number history 堆棧中的地址數目action
: string 當前的動做 (PUSH , REPLACE , 或者是 POP )location
: object 當前訪問地址信息組成的對象push(path, [state])
: func 在 history 堆棧信息里加入一個新路徑replace(path, [state])
: func 替換 history 堆棧信息裏的當前路徑go(n)
: func 將 history 堆棧中的指針向前移動 ngoBack()
: func 等同於 go(-1)goForward()
: func 等同於 go(1)block(prompt)
: func 阻止跳轉須要注意的是:history
對象是可變的,因此不要從 history.location
直接獲取,而是須要經過 <Route>
的prop
來獲取 location
。
Context
,得到 router
對象import React from "react"; import PropTypes from "prop-types"; class MyComponent extends React.Component { static contextTypes = { router: PropTypes.object } constructor(props, context) { super(props, context); } ... myFunction() { this.context.router.history.push("/some/Path"); } ... }
以上兩種方法中推薦使用第一種,由於 React
不推薦使用 context
, 在將來版本中有可能被拋棄哦。
由 A 頁面跳轉到 B 頁面,B 頁面停留在 A 頁面的位置,沒有返回到頂部。
問題分析:在 React Router
早期版本中你們可使用滾動恢復的開箱即用功能,可是在 4.x 中路由切換時並不會恢復滾動位置,用戶須要對 window 和獨立組件的滾動位置進行管理。可使用 withRouter
組件: withRouter
能夠訪問歷史對象的屬性和最近的 <Route>
匹配項,當路由的屬性值 { match
, location
, history
} 改變時,withRouter
都會從新渲染。該組件能夠攜帶組件的路由信息,避免組件之間一層層傳遞。使用方法以下:
withRouter(MyComponent)
這樣就能夠獲取到 MyComponent 組件的路由信息了。
解決方法:使用 withRouter
封裝 ScrollToTop
組件。這裏就用到了 withRouter
攜帶路由信息的特性,經過對比props
中 location
的變化,實現頁面的滾動。
ScrollToTop
組件,代碼以下:import React, { Component } from 'react'; import { Route, withRouter } from 'react-router-dom'; class ScrollToTop extends Component { componentDidUpdate(prevProps) { if (this.props.location !== prevProps.location) { window.scrollTo(0, 0) } } render() { return this.props.children } } export default withRouter(ScrollToTop);
ReactDOM.render(( <BrowserRouter> <ScrollToTop> <div className="container"> <Route path={routePaths.INDEX} exact component={Index} /> <Route path={routePaths.CARD} component={Card} /> </div> </ScrollToTop> </BrowserRouter> ), document.getElementById('app') );
這樣處理以後,當跳轉頁面時都會自動回到該頁面的頂部位置。
問題背景:當路由發生跳轉時咱們可能須要攜帶一些參數。
解決方法:使用 props
屬性,介紹如下三種傳值方法:
props.params
指定一個 path ,而後指定通配符能夠攜帶參數到指定的 path
:
<Route path='/user/:name' component={UserPage}></Route>
跳轉 UserPage 頁面時,能夠這樣寫:
//link方法 <Link to="/user/sam">用戶</Link> //push方法 this.props.history.push("/user/sam");
在 UserPage 頁面中經過 this.props.params.name
獲取值。
上面的方法能夠傳遞一個或多個值,可是每一個值的類型都是字符串,無法傳遞一個對象。若是要傳的話能夠將 json 對象轉換爲字符串,傳遞過去以後再將 json 字符串轉換爲對象。
let data = {id:3,name:sam,age:36}; data = JSON.stringify(data); let path = '/user/${data}'; //在頁面中獲取值時 let data = JSON.parse(this.props.params.data);
query
query
方式能夠傳遞任意類型的值,可是頁面的 url
也是由 query
的值拼接的,url
很長且是明文傳輸。
//定義路由 <Route path='/user' component={UserPage}></Route> //數據定義 let data = {id:3,name:sam,age:36}; let path = { pathname: '/user', query: data, } //頁面跳轉 <Link to={path}>用戶</Link> this.props.history.push(path); //頁面取值 let data = this.props.location.query; let {id,name,age} = data;
state
state
方式相似於 post
,依然能夠傳遞任意類型的數據,並且能夠不以明文方式傳輸。
//定義路由 <Route path='/user' component={UserPage}></Route> //數據定義 let data = {id:3,name:sam,age:36}; let path = { pathname: '/user', state: data, } //頁面跳轉 <Link to={path}>用戶</Link> this.props.history.push(path); //頁面取值 let data = this.props.location.state; let {id,name,age} = data;
在實際的項目開發中,你們能夠根據項目須要選擇合適的傳值方法。
<BrowserRouter>
配置路由,上傳頁面至服務器後頁面出現 404問題背景:項目中控制路由跳轉使用的是 <BrowserRouter>
,代碼以下:
ReactDOM.render(( <BrowserRouter> <div className="container"> <Route path={routePaths.INDEX} exact component={Index} /> <Route path={routePaths.CARD} component={Card} /> </div> </BrowserRouter> ), document.getElementById('app') );
在開發過程當中使用是沒有問題的,可是將頁面上傳至服務器以後,問題就來了:用戶訪問的資源不存在,頁面是空白的。
問題分析:<BrowserRouter>
是使用 React-Router 應用推薦的 history
方案。它使用瀏覽器中的 History API 用於處理 url,建立一個像 example.com/list/123
這樣真實的 url。當經過真實 url 訪問網站的時候,因爲路徑是指向服務器的真實路徑,但該路徑下並無相關資源,因此用戶訪問的資源不存在。
解決方法:
<HashRouter>
。它使用 url 中的 hash(#)部分去建立路由,舉例來講,用戶訪問 http://www.example.com/
,實際會看到的是 http://www.example.com/#/
。
爲何本地開發時沒有問題呢?那是由於咱們的 React
腳手架中使用 webpack-dev-server
作了配置。
webpackConfig.devServer = { disableHostCheck: true, contentBase: path.resolve(__dirname, 'build'), compress: true, //gzip壓縮 historyApiFallback: true };
<BrowserRouter>
的話,服務器須要進行相關路由配置,方法見擴展閱讀 [1]。問題背景:當訪問首頁時會一次性請求全部的 js 資源,這會大大影響頁面的加載速度和用戶體驗。因此添加按需加載功能是必要的。
問題分析:4.x 官網中推薦使用 bundle loader
實現代碼拆分,因此咱們選擇使用 bundle loader
。
解決方法:配置按需加載的步驟以下:
bundle-loader
。npm install --save-dev bundle-loader
Bundle.js
。<Bundle>
組件會接受一個名爲 load
的 props
, load
是一個組件異步加載的方法,該方法須要傳入一個回調函數做爲參數,而後回調函數會在方法內異步接收加載完的組件,詳細代碼以下:
import React, { Component } from 'react'; export default class Bundle extends React.Component { constructor(props) { super(props); this.state = { // short for "module" but that's a keyword in js, so "mod" mod: null } } componentWillMount() { this.load(this.props) } componentWillReceiveProps(nextProps) { if (nextProps.load !== this.props.load) { this.load(nextProps) } } load(props) { this.setState({ mod: null }) props.load((mod) => { this.setState({ // handle both es imports and cjs mod: mod.default ? mod.default : mod }) }) } render() { if (!this.state.mod) return false return this.props.children(this.state.mod) } }
app.jsx
配置。使用 bundle-loader
爲每一個頁面組件配置按需加載,默認打開的首頁能夠直接引入,不須要使用 <Bundle>
組件進行處理,代碼以下所示:
import React from 'react'; import ReactDOM from 'react-dom'; import { HashRouter, Route } from 'react-router-dom'; import * as routePaths from './js/constants/routePaths'; import Bundle from './js/constants/Bundle.js'; //默認打開頁面直接引入 import Index from './js/pages/Index'; //其餘頁面異步引入 import CardContainer from 'bundle-loader?lazy&name=app-[name]!./js/pages/Card'; import './assets/css/index.scss'; const Card = () => ( <Bundle load={CardContainer}> {(Card) => <Card />} </Bundle> ) ReactDOM.render(( <HashRouter> <div className="container"> <Route path={routePaths.INDEX} exact component={Index} /> <Route path='/card' component={Card} /> </div> </HashRouter> ), document.getElementById('app') );
webpack.config.js
修改,配置子文件名稱。webpackConfig.output = { path: path.resolve(__dirname, 'build/' + config.ftpTarget), publicPath: config.publicPath + '/', filename: 'js/[name].js', chunkFilename: 'js/[id].js' }
這樣配置以後,打包出來的 js 文件已經進行了拆分,每一個頁面對應一個本身的 js ,當訪問該頁面時才加載相應的資源。
this.props.history.push()
進行路由跳轉時報錯問題背景:配置按需加載後,首頁能夠正常跳轉子頁面,而在子頁面中進行頁面跳轉時出現報錯:this.props.history.push中'push'is undefined
。
問題分析:項目中使用 this.props.history.push()
方法實現路由跳轉,未進行文件拆分時能夠正常使用,可是當進行文件拆分以後,子文件中沒法獲取 this.props.history
對象。
解決方法:使用 withRouter
封裝頁面組件。示例代碼以下:
import React from 'react'; import { withRouter } from 'react-router-dom'; class Card extends React.Component { constructor(props) { super(props); } render() { return ( <div className="wrapper"> <p>這是第二頁:測試</p> </div> ); } } export default withRouter(Card);
這樣封裝以後,就無需一級級傳遞 React Router
的屬性,子文件內也能夠拿到須要的路由信息。
以上列出的內容就是咱們在使用 4.x 開發時遇到的一些問題,針對每一個問題不只給出瞭解決方法,還介紹瞭解決方法中涉及到的知識點。對於一些未介紹到的內容,你們能夠移步官網進行深度學習(見擴展閱讀 [2])。
對於新項目來講,使用 React Router 4.x 進行開發是一個不錯的選擇。由於其組件化的思想和 React 很像,學習起來比較容易、上手快。除了官網 [2] ,擴展閱讀中咱們還給出了 React Router 4.x 的其餘學習文檔 [3][4],供你們參考。值得注意的是,上述問題解決方法中的示例代碼使用的是 React v16 版本,在實際使用時須要結合本身的環境進行適當修改。
React Router 4.x 這次帶來的改變是顛覆性的,對於咱們使用者來講是一種挑戰,同時也是一種知識的有趣探索。組件化路由設計理念和動態路由的配置給咱們的項目開發帶來了更大的靈活性。文中在介紹問題解決方法時對一些 4.x 的 API 作了簡要的介紹,對於一些更細節化的東西,須要咱們移步官網進行深刻的學習,而後再慢慢的探索。React Router 的做者說過版本號要永遠和 React 保持一致,因此相信咱們的 React Router 探索之旅將會一直進行着……同志們,Hold On!
好了,關於 React Router 4.x 開發實踐中遇到的問題就介紹到這裏了,若有任何疑問,歡迎留言。
[1] https://www.thinktxt.com/react/2017/02/26/react-router-browserHistory-refresh-404-solution.html
[3] https://github.com/ReactTraining/react-router
[4] https://www.sitepoint.com/react-router-v4-complete-guide/
有興趣歡迎關注咱們的公衆號:全棧探索。歡迎交流。