開始一個React項目(四)路由實例(v4)

前言

開始一個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對象的pathurl屬性奢讨。
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對象獲取的汹买。

嵌套路由演示.gif

優(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系枪,卻報錯了:

路由問題.gif

報錯是肯定的,這個頁面的數(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。注意這時候loaderHomeloaderNews不是我們之前引入的組件了诱桂,而組件應該這樣生成:

const Home = (props) => (
  <Bundle load={loaderHome}>
    {(Home) => <Home {...props}/>}
  </Bundle>
)


const News = (props) => (
  <Bundle load={loaderNews}>
    {(News) => <News {...props}/>}
  </Bundle>
)

剩下的就和之前的寫法一樣了洋丐,如果還有疑問我會把代碼放在github上,地址貼在文末』拥龋現(xiàn)在來看看效果:

image.png

可以看到在首頁會有一個home.1.js文件加載進來友绝,在新聞頁有一個news.2.js文件,這就實現(xiàn)了到對應頁面才加載該頁面的js肝劲,不過有一點你應該注意到就是bundle.js文件依然非常的大迁客,這是因為react本身就需要依賴諸如react,react-dom以及各種loader,這些文件都會被打包到bundle.js 中郭宝,而我們雖然用路由實現(xiàn)了各頁面的‘按需加載’,但這只分離了一小部分代碼出去掷漱,剩下的怎么辦粘室?還是得用webpack。

寫在最后

目前為止我使用到的路由例子就是以上這些了卜范,小伙伴如果還有別的疑問可以評論育特,我們可以一起探討,代碼我放在github上了先朦。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缰冤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子喳魏,更是在濱河造成了極大的恐慌棉浸,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刺彩,死亡現(xiàn)場離奇詭異迷郑,居然都是意外死亡,警方通過查閱死者的電腦和手機创倔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門嗡害,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人畦攘,你說我怎么就攤上這事霸妹。” “怎么了知押?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵叹螟,是天一觀的道長。 經(jīng)常有香客問我台盯,道長罢绽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任静盅,我火速辦了婚禮良价,結果婚禮上,老公的妹妹穿的比我還像新娘蒿叠。我一直安慰自己明垢,他們只是感情好,可當我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布栈虚。 她就那樣靜靜地躺著袖外,像睡著了一般。 火紅的嫁衣襯著肌膚如雪魂务。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機與錄音粘姜,去河邊找鬼鬓照。 笑死,一個胖子當著我的面吹牛孤紧,可吹牛的內(nèi)容都是我干的豺裆。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼号显,長吁一口氣:“原來是場噩夢啊……” “哼臭猜!你這毒婦竟也來了?” 一聲冷哼從身側響起押蚤,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤蔑歌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后揽碘,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體次屠,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年雳刺,在試婚紗的時候發(fā)現(xiàn)自己被綠了劫灶。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡掖桦,死狀恐怖本昏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情枪汪,我是刑警寧澤凛俱,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站料饥,受9級特大地震影響蒲犬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜岸啡,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一原叮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧巡蘸,春花似錦奋隶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至搬味,卻和暖如春境氢,著一層夾襖步出監(jiān)牢的瞬間蟀拷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工萍聊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留问芬,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓寿桨,卻偏偏與公主長得像此衅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子亭螟,可洞房花燭夜當晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,307評論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理挡鞍,服務發(fā)現(xiàn),斷路器预烙,智...
    卡卡羅2017閱讀 134,711評論 18 139
  • 前言 react-router針對不同的使用場景衍生了不同的路由包墨微,RN項目用react-router-nativ...
    大柚子08閱讀 36,484評論 5 39