開始一個React專案(四)路由例項(v4)
前言
在開始一個React專案(三)路由基礎(v4)中我大概總結了一下web應用的路由,這一篇我會接著上一篇分享一些例子。
簡單的路由示例
一個最簡單的網站結構是首頁和幾個獨立的二級頁面,假如我們有三個獨立的二級頁面分別為:新聞頁、課程頁、加入我們,路由配置如下:
index.js
:
import React from 'react'
import ReactDom from 'react-dom'
import {
BrowserRouter as Router,
Route,
NavLink,
Switch
} from 'react-router-dom'
import Home from './pages/Home'
import News from './pages/News'
import Course from './pages/Course'
import JoinUs from './pages/JoinUs'
const App = () => (
<Router >
<div>
<header>
<nav>
<ul>
<li><NavLink exact to="/">首頁</NavLink></li>
<li><NavLink to="/news">新聞</NavLink></li>
<li ><NavLink to='/course'>課程</NavLink></li>
<li><NavLink to="/joinUs">加入我們</NavLink></li>
</ul>
</nav>
</header>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/news" component={News}/>
<Route path="/course" component={Course}/>
<Route path="/joinUs" render={(props) => <JoinUs {...props}/>}/>
</Switch>
</div>
</Router>
)
ReactDom.render(
<App />,
document.getElementById('root')
)
一個簡單的路由,我們可以將<NavLink>
和<Route>
都寫在index.js
裡面,但這會讓每一個頁面都渲染出導航欄。
抽離導航的路由
假如現在新增了登入頁,要求登入頁沒有導航欄,其它頁面有導航欄。
index.js
const App = () => (
<Router>
<div>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/login" component={Login}/>
<Route path="/news" component={News}/>
<Route path="/course" component={Course}/>
<Route path="/joinUs" render={(props) => <JoinUs {...props}/>}/>
</Switch>
</div>
</Router>
)
ReactDom.render(
<App />,
document.getElementById('root')
)
components/Header.js
import {
NavLink
} from 'react-router-dom'
class Header extends Component {
render() {
return (
<header>
<nav>
<ul>
<li><NavLink exact to="/">首頁</NavLink></li>
<li><NavLink to="/news">新聞</NavLink></li>
<li><NavLink to='/course'>課程</NavLink></li>
<li><NavLink to="/joinUs">加入我們</NavLink></li>
</ul>
</nav>
</header>
)
}
}
每個頁面根據需要選擇是否引入<Header>
元件
新增404頁面
利用<Switch>
元件的特性,當前面所有的路由都匹配不上時,會匹配最後一個path="*"
的路由,該路由再重定向到404頁面。
index.js
import {
BrowserRouter as Router,
Route,
Switch,
Redirect
} from 'react-router-dom'
const App = () => (
<Router>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/login" component={Login}/>
<Route path="/news" component={News}/>
<Route path="/course" component={Course}/>
<Route path="/joinUs" render={(props) => <JoinUs {...props}/>}/>
<Route path="/error" render={(props) => <div><h1>404 Not Found!</h1></div>}/>
<Route path="*" render={(props) => <Redirect to='/error'/>}/>
</Switch>
</Router>
)
巢狀路由
假如課程頁下有三個按鈕分別為:前端開發、大資料、演算法。
前面我提到過match
是實現巢狀路由的物件,當我們在某個頁面跳轉到它的下一級子頁面時,我們不會顯示地寫出當前頁面的路由,而是用match
物件的path
和url
屬性。
pages/Course.js
class Course extends Component {
render() {
let { match } = this.props;
return(
<div className="list">
<Header />
<NavLink to={`${match.url}/front-end`}>前端技術</NavLink>
<NavLink to={`${match.url}/big-data`}>大資料</NavLink>
<NavLink to={`${match.url}/algorithm`}>演算法</NavLink>
<Route path={`${match.path}/:name`} render={(props) => <div>{props.match.params.name}</div>}/>
</div>
)
}
}
match物件的params物件可以獲取到/:name的name值
帶參的巢狀路由
假如新聞頁是一個新聞列表,點選某一條新聞時展示該條新聞詳情。與上一個示例不同的是,新聞列表頁需要將該條新聞的內容傳遞給新聞詳情頁,傳遞引數可以有三種方式:
- search: ”, //會新增到url裡面,形如”?name=melody&age=20”
- hash: ”, //會新增到url裡面,形如”#tab1”
- state: {},//不會新增到url裡面
pages/News.js
import React, { Component } from 'react'
import {
Route,
NavLink
} from 'react-router-dom'
import Header from '../components/Header'
//模擬資料
const data = [
{
id: 1,
title: '春運地獄級搶票模式開啟',
content: '春運地獄級搶票模式開啟,你搶到回家的票了嗎?反正我還沒有,難受'
},
{
id: 2,
title: '寒潮來襲,你,凍成狗了嗎?',
content: '寒潮來襲,你,凍成狗了嗎?被子是我親人,我不想離開它'
}
]
class News extends Component {
render() {
return(
<div className="news">
<Header />
<h1 className="title">請選擇一條新聞:</h1>
{data.map((item) => (
<div key={item.id}>
<NavLink to={{
pathname: `${this.props.match.url}/${item.id}`,
state: {data: item}
}}>
{item.title}
</NavLink>
</div>
))}
<Route path={`${this.props.match.path}/:id`} render={(props) => {
let data = props.location.state && props.location.state.data;
return (
<div>
<h1>{data.title}</h1>
<p>{data.content}</p>
</div>
)
}}/>
</div>
)
}
}
export default News
<NavLink>
傳遞的引數是通過location物件獲取的。
優化巢狀路由
前面兩種巢狀路由,子路由都渲染出了父元件,如果不想渲染出父元件,有兩種方法。
方法一:將配置子路由的<Route>
寫在index.js裡面
index.js
<Route exact path="/news" component={News}/>
<Route path="/news/:id" component={NewsDetail}/>
pages/News.js
class News extends Component {
render() {
return(
<div className="news">
<Header />
<h1 className="title">請選擇一條新聞:</h1>
{data.map((item) => (
<div key={item.id}>
<NavLink to={{
pathname: `${this.props.match.url}/${item.id}`,
state: {data: item}
}}>
{item.title}
</NavLink>
</div>
))}
</div>
)
}
}
pages/NewsDetail.js
import React, { Component } from 'react'
import Header from '../components/Header'
class NewsDetail extends Component {
constructor(props) {
super(props)
this.data = props.location.state.data; //獲取父元件傳遞過來的資料
}
render() {
return(
<div className="news">
<Header />
<h1>{this.data.title}</h1>
<p>{this.data.content}</p>
</div>
)
}
}
export default NewsDetail
方法二:仍然將子路由配置寫在News.js裡面
index.js
<Route path="/news" component={News}/>
注意:這裡一定不能加exact,否則子元件永遠渲染不出來。
pages/News.js
class NewsPage extends Component {
render() {
return(
<div className="news">
<Header />
<h1 className="title">請選擇一條新聞:</h1>
{data.map((item) => (
<div key={item.id}>
<NavLink to={{
pathname: `${this.props.match.url}/${item.id}`,
state: {data: item}
}}>
{item.title}
</NavLink>
</div>
))}
</div>
)
}
}
const News = ({match}) => {
return (
<div>
<Route path={`${match.path}/:id`} component={NewsDetail}/>
<Route exact path={match.path} render={(props) => <NewsPage {...props} />}/>
</div>
)
}
export default News
注意:這裡的寫法其實就是將新聞頁也看作一個元件,然後重新定義一個News元件,根據路由來渲染不同的元件,exact引數是加在這裡的,並且匯出的是News而不是NewsPage。
頁面間傳參的一些注意點
在巢狀路由和帶參的巢狀路由兩小節可以看到兩種傳參方式,如果僅僅是獲取url裡面的參,比如<Route path={`${match.path}/:name`}/>
的name屬性,子元件可以通過this.props.match.params.name
取得,如果還需要多餘的引數,比如選中的某一條資料,則父元件通過<NavLink>
的to屬性的search,hash, state向子元件傳參,子元件通過this.props.location.search|hash|state
獲取。
但是,這兩者是有區別的!使用的時候一定要小心!
以上面的新聞詳情頁為例,詳情頁的資料是從新聞頁直接傳過來的:
this.data = props.location.state.data;
現在,讓我們隨便點進一條新聞,然後重新整理它,發現沒毛病,然後手動輸入另一條存在的新聞id,卻報錯了:
報錯是肯定的,這個頁面的資料本身是通過props.location.state.data
獲取的,當我們在這個頁面手動輸入id時,根本沒有資料,而且此時列印state,它的值是undefined.
但是!!通過props.match.params
卻可以獲取到id,所以,這種方式顯然更保險,不過你應該也看出來了,由於這種方式涉及到url位址列,所以不可以傳遞過多的引數,所以開發過程中,要處理好這兩種傳參方式。
對於上面的新聞詳情頁例子,一般不需要把整條資料傳遞過去,而是傳遞一個id或者別的引數,然後在詳情頁再向伺服器發起請求拿到該條資料的詳情,可以修改程式碼:
pages/NewsDetail.js
constructor(props) {
super(props)
this.id = props.match.params.id;
this.state = {
data: ''
}
}
componentWillMount() {
this.getNewsDetail();
}
getNewsDetail() {
fetch(`xxx?id=${this.id}`).then(res => res.json())
.then(resData => {
this.setState({data: resData});
})
}
render() {
let title = this.state.data && this.state.data.title;
let content = this.state.data && this.state.data.content;
return(
<div>
<h1>{title}</h1>
<p>{content}</p>
</div>
)
}
不過,還是會有必須傳遞一整條資料過去或者其它更復雜的情況,這種時候就要處理好子元件接收資料的邏輯,以免出現數據為空時報錯的情況,修改程式碼:
pages/NewsDetail.js
class NewsDetail extends Component {
constructor(props) {
super(props)
this.data = props.location.state ? props.location.state.data : {} ;
}
render() {
let title = this.data.title || '';
let content = this.data.content || '';
return(
<div className="news">
<Header />
<h1>{title}</h1>
<p>{content}</p>
</div>
)
}
}
以上兩種處理方式都不會再出現使用者輸入一個不存在的id報錯的情況,不過,我們還可以做的更好。
根據資料判斷是否顯示404頁面
前面我們實現了一個簡單的404頁面,即路由不匹配時跳轉到404頁面,實際開發中還有一種情況,是根據引數去請求資料,請求回來的資料為空,則顯示一個404頁面,以上面的新聞詳情頁為例,假如我們現在是在這個頁面發起的資料請求,那麼我們可以用一個標誌位來實現載入404頁面:
pages/NewsDetail.js
constructor(props) {
super(props)
this.id = props.match.params.id;
this.state = {
data: '',
hasData: true,// 一開始的初始值一定要為true
}
}
componentWillMount() {
this.getNewsDetail();
}
getNewsDetail() {
fetch(`xxx?id=${this.id}`).then(res => res.json())
.then(resData => {
if (resData != null) {
this.setState({data: resData});
} else {
this.setState({hasData: false})
}
})
}
//找不到資料重定向到404頁面
renderNoDataView() {
return <Route path="*" render={() => <Redirect to="/error"/>}/>
}
render() {
return this.state.hasData ? this.renderView() : this.renderNoDataView()
}
按需載入
這真的是個非常非常重要的功能,單頁面應用有一個非常大的弊端就是首屏會載入其它頁面的內容,當專案非常複雜的時候首屏載入就會很慢,當然,解決方法有很多,webpack有這方面的技術,路由也有,把它們結合起來,真的就很完美了。
官網的code-splitting就介紹了路由如何配置按需載入,只是不夠詳細,因為它缺少有關wepback配置的程式碼。
安裝bundle-loader: yarn add bundle-loader
webpack.config.js
module.exports = {
output: {
path: path.resolve(__dirname, 'build'), //打包檔案的輸出路徑
filename: 'bundle.js', //打包檔名
chunkFilename: '[name].[id].js', //增加
publicPath: publicPath,
},
module: {
loaders: [
{
test: /\.bundle\.js$/,
use: {
loader: 'bundle-loader',
options: {
lazy: true,
name: '[name]'
}
}
},
]
},
}
專案中需要新建一個bundle.js
檔案,我們把它放在components
下:
components/Bundle.js
import React, { Component } from 'react'
class Bundle extends Component {
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() {
return this.state.mod ? this.props.children(this.state.mod) : null
}
}
export default Bundle
修改index.js
首先將引入元件的寫法改為:
import loaderHome from 'bundle-loader?lazy&name=home!./pages/Home'
import loaderNews from 'bundle-loader?lazy&name=news!./pages/News'
相當於先經過bundle-loader
處理,這裡的name
會作為webpack.config.js
配置的chunkFilename: '[name].[id].js'
的name
。注意這時候loaderHome
和loaderNews
不是我們之前引入的元件了,而元件應該這樣生成:
const Home = (props) => (
<Bundle load={loaderHome}>
{(Home) => <Home {...props}/>}
</Bundle>
)
const News = (props) => (
<Bundle load={loaderNews}>
{(News) => <News {...props}/>}
</Bundle>
)
剩下的就和之前的寫法一樣了,如果還有疑問我會把程式碼放在github上,地址貼在文末。現在來看看效果:
可以看到在首頁會有一個home.1.js
檔案載入進來,在新聞頁有一個news.2.js
檔案,這就實現了到對應頁面才載入該頁面的js,不過有一點你應該注意到就是bundle.js
檔案依然非常的大,這是因為react本身就需要依賴諸如react
,react-dom
以及各種loader,這些檔案都會被打包到bundle.js
中,而我們雖然用路由實現了各頁面的‘按需載入’,但這隻分離了一小部分程式碼出去,剩下的怎麼辦?還是得用webpack。
寫在最後
目前為止我使用到的路由例子就是以上這些了,小夥伴如果還有別的疑問可以評論,我們可以一起探討,程式碼我放在github上了。