开始一个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上了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容