1. 程式人生 > >開始一個React專案(四)路由例項(v4)

開始一個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物件的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`}>大資料</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物件獲取的。

巢狀路由演示.gif

優化巢狀路由

前面兩種巢狀路由,子路由都渲染出了父元件,如果不想渲染出父元件,有兩種方法。

方法一:將配置子路由的<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,卻報錯了:
路由問題.gif
報錯是肯定的,這個頁面的資料本身是通過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。注意這時候loaderHomeloaderNews不是我們之前引入的元件了,而元件應該這樣生成:

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


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

剩下的就和之前的寫法一樣了,如果還有疑問我會把程式碼放在github上,地址貼在文末。現在來看看效果:
image.png
可以看到在首頁會有一個home.1.js檔案載入進來,在新聞頁有一個news.2.js檔案,這就實現了到對應頁面才載入該頁面的js,不過有一點你應該注意到就是bundle.js檔案依然非常的大,這是因為react本身就需要依賴諸如react,react-dom以及各種loader,這些檔案都會被打包到bundle.js 中,而我們雖然用路由實現了各頁面的‘按需載入’,但這隻分離了一小部分程式碼出去,剩下的怎麼辦?還是得用webpack。

寫在最後

目前為止我使用到的路由例子就是以上這些了,小夥伴如果還有別的疑問可以評論,我們可以一起探討,程式碼我放在github上了。