前言
在開始一個React項目(三)路由基礎(v4)中我大概總結了一下web應用的路由撬槽,這一篇我會接著上一篇分享一些例子。
簡單的路由示例
一個最簡單的網(wǎng)站結構是首頁和幾個獨立的二級頁面诺核,假如我們有三個獨立的二級頁面分別為:新聞頁闯袒、課程頁瘦赫、加入我們留凭,路由配置如下:
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
里面壮不,但這會讓每一個頁面都渲染出導航欄由缆。
抽離導航的路由
假如現(xiàn)在新增了登錄頁注祖,要求登錄頁沒有導航欄猾蒂,其它頁面有導航欄。
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>
)
}
}
每個頁面根據(jù)需要選擇是否引入<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>
)
嵌套路由
假如課程頁下有三個按鈕分別為:前端開發(fā)蚊逢、大數(shù)據(jù)、算法箫章。
前面我提到過match
是實現(xiàn)嵌套路由的對象时捌,當我們在某個頁面跳轉到它的下一級子頁面時,我們不會顯示地寫出當前頁面的路由炉抒,而是用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`}>大數(shù)據(jù)</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值
帶參的嵌套路由
假如新聞頁是一個新聞列表,點擊某一條新聞時展示該條新聞詳情焰薄。與上一個示例不同的是拿诸,新聞列表頁需要將該條新聞的內(nèi)容傳遞給新聞詳情頁,傳遞參數(shù)可以有三種方式:
- 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'
//模擬數(shù)據(jù)
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>
傳遞的參數(shù)是通過location對象獲取的汹买。
優(yōu)化嵌套路由
前面兩種嵌套路由佩伤,子路由都渲染出了父組件,如果不想渲染出父組件晦毙,有兩種方法生巡。
方法一:將配置子路由的<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; //獲取父組件傳遞過來的數(shù)據(jù)
}
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組件,根據(jù)路由來渲染不同的組件,exact參數(shù)是加在這里的垃环,并且導出的是News而不是NewsPage邀层。
頁面間傳參的一些注意點
在嵌套路由和帶參的嵌套路由兩小節(jié)可以看到兩種傳參方式,如果僅僅是獲取url里面的參遂庄,比如<Route path={`${match.path}/:name`}/>
的name屬性寥院,子組件可以通過this.props.match.params.name
取得,如果還需要多余的參數(shù)涛目,比如選中的某一條數(shù)據(jù)秸谢,則父組件通過<NavLink>
的to屬性的search,hash, state向子組件傳參霹肝,子組件通過this.props.location.search|hash|state
獲取估蹄。
但是,這兩者是有區(qū)別的沫换!使用的時候一定要小心臭蚁!
以上面的新聞詳情頁為例,詳情頁的數(shù)據(jù)是從新聞頁直接傳過來的:
this.data = props.location.state.data;
現(xiàn)在讯赏,讓我們隨便點進一條新聞垮兑,然后刷新它,發(fā)現(xiàn)沒毛病漱挎,然后手動輸入另一條存在的新聞id系枪,卻報錯了:
報錯是肯定的,這個頁面的數(shù)據(jù)本身是通過
props.location.state.data
獲取的磕谅,當我們在這個頁面手動輸入id時私爷,根本沒有數(shù)據(jù),而且此時打印state膊夹,它的值是undefined.但是3幕搿!通過
props.match.params
卻可以獲取到id割疾,所以嚎卫,這種方式顯然更保險嘉栓,不過你應該也看出來了宏榕,由于這種方式涉及到url地址欄,所以不可以傳遞過多的參數(shù)侵佃,所以開發(fā)過程中麻昼,要處理好這兩種傳參方式。對于上面的新聞詳情頁例子馋辈,一般不需要把整條數(shù)據(jù)傳遞過去抚芦,而是傳遞一個id或者別的參數(shù),然后在詳情頁再向服務器發(fā)起請求拿到該條數(shù)據(jù)的詳情,可以修改代碼:
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>
)
}
不過叉抡,還是會有必須傳遞一整條數(shù)據(jù)過去或者其它更復雜的情況尔崔,這種時候就要處理好子組件接收數(shù)據(jù)的邏輯,以免出現(xiàn)數(shù)據(jù)為空時報錯的情況褥民,修改代碼:
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>
)
}
}
以上兩種處理方式都不會再出現(xiàn)用戶輸入一個不存在的id報錯的情況季春,不過,我們還可以做的更好消返。
根據(jù)數(shù)據(jù)判斷是否顯示404頁面
前面我們實現(xiàn)了一個簡單的404頁面载弄,即路由不匹配時跳轉到404頁面,實際開發(fā)中還有一種情況撵颊,是根據(jù)參數(shù)去請求數(shù)據(jù)宇攻,請求回來的數(shù)據(jù)為空,則顯示一個404頁面倡勇,以上面的新聞詳情頁為例逞刷,假如我們現(xiàn)在是在這個頁面發(fā)起的數(shù)據(jù)請求,那么我們可以用一個標志位來實現(xiàn)加載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})
}
})
}
//找不到數(shù)據(jù)重定向到404頁面
renderNoDataView() {
return <Route path="*" render={() => <Redirect to="/error"/>}/>
}
render() {
return this.state.hasData ? this.renderView() : this.renderNoDataView()
}
按需加載
這真的是個非常非常重要的功能妻熊,單頁面應用有一個非常大的弊端就是首屏會加載其它頁面的內(nèi)容亲桥,當項目非常復雜的時候首屏加載就會很慢,當然固耘,解決方法有很多题篷,webpack有這方面的技術,路由也有厅目,把它們結合起來番枚,真的就很完美了。
官網(wǎng)的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'
相當于先經(jīng)過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上,地址貼在文末』拥龋現(xiàn)在來看看效果:
可以看到在首頁會有一個
home.1.js
文件加載進來友绝,在新聞頁有一個news.2.js
文件,這就實現(xiàn)了到對應頁面才加載該頁面的js肝劲,不過有一點你應該注意到就是bundle.js
文件依然非常的大迁客,這是因為react本身就需要依賴諸如react
,react-dom
以及各種loader,這些文件都會被打包到bundle.js
中郭宝,而我們雖然用路由實現(xiàn)了各頁面的‘按需加載’,但這只分離了一小部分代碼出去掷漱,剩下的怎么辦粘室?還是得用webpack。
寫在最后
目前為止我使用到的路由例子就是以上這些了卜范,小伙伴如果還有別的疑問可以評論育特,我們可以一起探討,代碼我放在github上了先朦。