React Router
React Router是为React而设计的一套完整的路由系统。通俗的理解路由系统:现在很多应用都是多页面的,当前界面的URL称为active的路由(个人理解:当前页面就是激活状态下的路由),由当前界面URL切换到其他界面URL就是路由的切换。这些切换都可以通过React Router进行。这门课程我们就要学习React Router的用法。
课前准备
本门课程讲解的基础是上面的react-router-demo文件夹,这是一个很简单的React项目。我们的课程会一步一步加入React Router的操作。对react-router-demo文件夹中的文件进行相应的修改。
开始前需要配置相关的实验环境,包括Node.js以及相关模块。与本课程相关的模块主要包括:react、react-dom、 express、react-router、webpack、webpack-dev-server等。同学们如果想在自己的设备上安装与本课程相关的模块,可以使用npm install命令进行安装。
如何运行react-router-demo?
$ cd react-router-demo~/react-router-demo
$ webpack ~/react-router-demo
$ node app.js
上面的操作分别是:进入react-router-demo文件夹、打包、运行应用。该项目运行后,会在8080端口监听连接请求。所以运行后,一个网页出现经典的"Hello, React Router!"。看官们,请记住此时的网址以“me.hubwiz.com”结尾,后面没有别的了。这个下一节的时候我们要做对比用。自己的设备上练习时,可以在浏览器中访问http://localhost:8080 查看。
渲染Route
其实React Router的核心就是React的一个组件。如果在index.js中用下面的渲染语句,那么网页上啥也没有,直到我们配置了一条路由,才会有所显示。
render(
, document.getElementById('app'))
有同学会想到,如果我们再配置渲染App组件的路由,那么网页上应该也能看到“Hello, React Router!”才对。到底是不是这样,实践才是硬道理。请打开index.js,并且进行如下修改:
// ...
import { Router, Route, hashHistory } from 'react-router'
render((
), document.getElementById('app'))
上面的代码中“// ...”表示省略了原来代码中不变动的部分。我们新导入了Router、Route、和hashHistory组件,并且不再直接渲染App组件,而改成了渲染一个Router。这个Router中只有一条路由,用于渲染App组件(见代码中component属性)。
对该项目重新打包运行
~/react-router-demo$ webpack
~/react-router-demo$ node app.js
运行后。你会发现网页没有变化,但是网页的URL会有所不同。还记得上一节的网址以“me.hubwiz.com”结尾,而现在的网页URL(网址)中增加了类似于“/#/?_k=koiqa2”的部分。这是因为我们使用了hashHistory组件,用于管理路由的历史。所以在URL中会有一个哈希值,用于记录浏览器的行为。
增加页面
创建两个组件,分别是Boys组件和Girls组件,分别写在modules/Boys.js和modules/Girls.js中,代码如下:
// modules/Boys.js中
import React from 'react'
export default React.createClass({
render() {
return
我是男神!}
})
// modules/Girls.js中
import React from 'react'
export default React.createClass({
render() {
return
我是女神!}
})
然后我们在index.js中就可以使用Boys组件和Girls组件了,就像使用App组件一样。将index.js的代码改为:
import React from 'react'
import { render } from 'react-dom'
import { Router, Route, hashHistory } from 'react-router'
import App from './modules/App'
import Boys from './modules/Boys'
import Girls from './modules/Girls'
render((
), document.getElementById('app'))
注意,上面代码中我们导入了Boys组件和Girls组件,并且在Router中加入了两条新的路由:渲染Boys组件的路由和渲染Girls组件的路由(这里简称Boys路由和Girls路由)。现在总共有三条路由,注意path属性的区别。同学们可以猜到刚进入网页会显示App组件,因为它的路径为“/”。
现在,虽然有Boys页面和Girls页面了,但是还不能通过点击网页,直接进行页面的切换。这要通过下一节学习到的Link组件来实现。但是我们通过改变网址就可以访问到这两个网页了,例如把“/#/?_k=koiqa2”改为"/boys",就可以看到“我是男神!”页面了。
Link
也许应用中最常用到的组件就是Link,可见其重要性。其实Link组件几乎与标签完全一样,除了一点:Link组件可以感知到被激活的路由。我们称当前在界面上显示的路由为被激活的路由
紧接上一小节,刚打开网页我们看到的是渲染后的App组件。如果想从App页面导航到Boys页面和Girls页面,就需要在App组件中创建一些导航。代码如下:
// modules/App.js中
import React from 'react'
import { Link } from 'react-router'
export default React.createClass({
render() {
return (
明星特区
)
}
})
现在再去打包运行(方法见上一节,以后不再重复说了哈),点击访问测试,你会惊奇的发现,点链接、回退、前进,都正常工作了。这就是Link组件的用法。
嵌套路由
有时会需要在每个页面上都显示App组件。如果没有React Router,就需要在每个页面上渲染App。这样的话,随着项目代码逐渐增多,代码冗杂现象就出现了。React Router跟Ember学了一个方法来共享UI,也就是嵌套路由。
很可能你需要的界面是:
也就是点击男神链接时,App组件还在被渲染,还能在界面上看到App组件。这可以通过把Boys和Girls路由嵌套在App路由中实现。因为这样的话,Boys路由就是App路由的子路由。记住一句话“子路由被激活时,其父路由也被激活”,所以Boys路由被激活时,App路由也是激活的状态,所以还在界面中显示。
代码改造
1.在index.js中,渲染Boys和Girls的路由要成为App的子路由。
// index.js
// ...
render((
), document.getElementById('app'))
2.在App中要渲染他的子组件,用{this.props.children}实现,换句通俗的话讲,就是在App中留个位置,用于显示子组件。如下所示:
// modules/App.js
// ...
render() {
return (
Ghettohub Issues
{this.props.children}
)
}
// ...
代码改造好了之后,打包运行。此时,虽然界面没有上图的整洁,但是已经到达了目的。现在来看,React Router组织界面的方式,特别像是大箱子套小箱子,对于男神和女神界面的组织方式如下:
就像搭积木一样,想要搭一个房子,可以先搭门、窗、墙等的小物体,最后把这些小物体组合起来就是一个房子。这就是React Router的思想:每条路由都可以是一个独立的小的应用,通过路由的配置将这小的应用组合起来,就是你所期待的大应用。就好比是大应用嵌套了小应用,特别像是大箱子套小箱子。
Link属性
activeStyle
Link组件区别于a标签的一个地方是,Link组件知道自己的路径是否是激活的状态,所以进行Style的配置。在App.js中修改代码,如下:
// modules/App.js
男神 女神
访问测试,你会发现,男神激活的时候,是红色的字体显示。
activeClassName
上面是用inline的方式进行修改的,当然也可以在CSS中进行格式的修改。
1.修改App.js中的属性
// modules/App.js
男神 女神
2.在index.htm中加入
3.新建index.css,并加入:
.active {
color: green;
}
打包运行,你会发现激活的某个路由,链接的文字会变成绿色。
Link包装器
其实一般情况下你的网站中大多数的链接都不需要知道自己是否被激活了,通常只是主导航的链接才需要。所以对主导航的Link进行包装是很有用的,这样你就不必记得哪些地方有activeClassName或activeStyle。所谓Link的包装器就是自定义的Link组件,通过自定义实现特别的Style。Link包装器的使用方式如下:
1.创建一个新的文件modules/NavLink.js,来自定义一个Link包装器,称为NavLink,当然可以是其他名字。内容为:
// modules/NavLink.js
import React from 'react'
import { Link } from 'react-router'
export default React.createClass({
render() {
return
}
})
2.在App中导入NavLink组件,并用它代替Link组件
// App.js
import NavLink from './NavLink'
// ...
男神 女神
以这样的方式,可以使用NavLink组件,也就是以后用到NavLink组件的时候,效果都是路由激活后,链接的文字变成绿色。当然你可以自定义你想要的Link的样式。
URL参数
大家都知道,URL是可以带参数的。通过URL中的参数,当前的页面可以将参数传递给下一个页面。
1.新建文件modules/Boy.js,内容为:
// modules/Boy.js
import React from 'react'
export default React.createClass({
render() {
return (
大家好,我是{this.props.params.boyName},我爱你们~~
)
}
})
注意,{this.props.params.boyName}是指要从上一个路由中得到boyName参数。
2.打开index.js,改为如下内容:
import React from 'react'
import { render } from 'react-dom'
import { Router, Route, hashHistory } from 'react-router'
import App from './modules/App'
import Boys from './modules/Boys'
import Girls from './modules/Girls'
import Boy from './modules/Boy'
render((
), document.getElementById('app'))
注意,
3.在Boys.js中,改为如下内容:
import React from 'react'
import { Link } from 'react-router'
export default React.createClass({
render() {
return (
我的男神们:
- 宋仲基
- 吴亦凡
)
}
})
注意,这更改了Boys组件,使得Boys路由被激活时显示:我的男神们:宋仲基 吴亦凡。这样的字眼。点击宋仲基,由于to="/boys/宋仲基",所以参数boyName就是“宋仲基”,切换到Boy路由被激活,并显示“大家好,我是宋仲基,我爱你们~~”
上面的例子,大家好好理解,在以后的编码中会经常用到。
二级嵌套
同学们也发现了上一节的分级,不是很合理啊。上一节中Boys和Boy是同一级的,都是App的子路由。这样做的后果是,当Boy激活时,Boys就不是激活的了,所以显示不了男神列表了。这一小节,我们讲解二级嵌套,将Boy路由变成Boys路由的子路由,这样Boy路由激活时,Boys路由也是激活的状态。三级甚至更多级的嵌套也同理。
1.在index.js更改代码,将Boy路由变成Boys路由的子路由,需要更改的部分如下:
2.我们需要在Boys中增加一个位置,用于显示Boy的信息。就像App中增加的{this.props.children}一样:
我的男神们:
- 宋仲基
- 吴亦凡
{this.props.children}
3.其实,我们对Boys同样可以使用Link包装器,使得Boys中的链接被激活时是绿色的。那么在Boys.js中,代码修改为:
import NavLink from './NavLink'
// ...
宋仲基 吴亦凡 // ...
打包运行,可以去点击访问测试,看下效果了。再说一次,子路由被激活时父路由也是被激活的状态。
IndexRoute
在我们的应用中,如果访问 / ,只会显示一个空白页。而我们的理想情况是先一个Home页,所以我们先建立一个Home组件,再去讲接下来怎么做。新建文件modules/Home.js并添加代码:
// modules/Home.js
import React from 'react'
export default React.createClass({
render() {
return
~~男神女神~~}
})
一种方式是,先看App里面有子路由激活吗?如果没有,你们显示Home。这种方式,App.js的代码为:
// App.js
import Home from './Home'
// ...
{/* ... */}
{this.props.children ||
} //...
这种方式也可以正常工作,但是我们更希望Home像Boys和Girls那样,绑定到一个路由上。这才更符合React Router的思想,一个组件绑定到一个路由上,通过路由的嵌套、激活等显示不同的界面。这种方式的实现要用到IndexRoute组件。只需改变index.js中的代码:
//...
import { Router, Route, hashHistory, IndexRoute } from 'react-router'
import Home from './modules/Home'
// ...
render((
), document.getElementById('app'))
上面代码的第2行,新引入了IndexRoute组件,第3行导入Home组件。在App路由中,加入了
注意,IndexRoute组件没有path属性。IndexRoute只有当其父路由的所有其他子路由(IndexRoute的所有兄弟路由)都没有激活时,才是父路由的this.props.children,并显示出来。如果Boys路由激活了,那么Boys组件才是App的this.props.children,并显示出来。
IndexLink
你可以也发现了,我们的应用没有能够回到渲染Home组件的导航啊。就是说你点击App组件中的男神或者女神按钮后就不会再有链接回到Home组件了。我们可以在App组件中加入一个链接,链回到 / ,如下:
// in App.js
// ...
Home // ...
但是,这样做会有一个不可思议的情况发生。Home路由总是被激活的状态:你点男神链接的时候,Home链接和男神链接都是绿色的。正如我们之前所说的,当子路由被激活时,父路由也是被激活的。而不幸的是, /是所有路由的父路由。所以会出现上述情况。
但我们的目的是,只有当Home路由被激活时, /路由才被激活。实现这个目的,可以有两种方式。
利用IndexLink
在App.js中改用IndexLink组件,代码如下:
import { IndexLink, Link } from 'react-router'
// ...
Home //...
去测试一下,会发现问题就这么解决了。所以可以看到IndexLink的作用:只有当该路由而不是其子路由被激活时,改路由才是被激活的。换句话说,IndexLink突破了“子路由被激活时,父路由也是被激活的”的限制。
利用onlyActiveOnIndex属性
我们可以不用IndexLink组件,而是在Link组件中加入onlyActiveOnIndex属性的设置,来达到我们的目的。其实,IndexLink简单来说就是一个加入了onlyActiveOnIndex属性的Link包装器。所以在App.js中可以用:
Home
activeClassName已经包装在NavLink中了,所以我们还可以用NavLink组件来实现:
Home
browserHistory
目前为止,我们应用的URL是建立在哈希值的基础上的。现在的浏览器允许JavaScript操控URL而不产生Http请求,所以,我们不必依赖于URL中的哈希部分来进行路由的切换。React Router中还提供了browserHistory。
在index.js中导入browserHistory,而不是导入hashHistory
// ...
import { Router, Route, browserHistory, IndexRoute } from 'react-router'
render((
//...
), document.getElementById('app'))
哈哈,这样的方式,在URL中就没有乱七八糟的哈希值了。
别高兴太早!
你点击个男神链接,在刷新下浏览器看看:Cannot GET /boys
出错了吧,但不怕。这个错误的原因是当前的Server不知道怎么处理刷新的请求,所以出错。因此我们可以在服务器(app.js)中设置一下,收到所有的请求后Server都反馈index.html,问题就可以解决:
1.修改app.js(看清楚,不是App.js哦):
// app.js
var express = require('express')
//导入path
var path = require('path')
var app = express()
// serve our static stuff like index.css
app.use(express.static(dirname))
//就是这句代码,解决的问题。
app.get('*', function (req, res) {
res.sendFile(path.join(dirname, 'index.html'))
})
var PORT = process.env.PORT || 8080
app.listen(PORT, function() {
console.log('Production Express server running at localhost:' + PORT)
})
2.把index.html中的相对路径改为绝对路径:index.css -> /index.css 以及bundle.js -> /bundle.js
打包并运行,可以发现,问题解决了!
通过程序进行导航
大多数的导航都是用Link实现的,但是伴随着表单的提交、按钮的点击也可以通过程序进行导航。我们在Boys.js中制作一个简单的表单:
import React from 'react'
import NavLink from './NavLink'
export default React.createClass({
// add this method
handleSubmit(event) {
event.preventDefault()
const boyName = event.target.elements[0].value
const path = `/repos/${boyName}`
console.log(path)
},
render() {
return (
我的男神们:
宋仲基
吴亦凡
{this.props.children}
)
}
})
点击提交按钮后会触发handleSubmit方法,在handleSubmit方法中,会得到路径path,上面的的代码只是把path记录到日志中
想要把你输入的男神姓名打印在网页上,可以利用Router提供的context实现:首先从组件的上下文context中请求router,再将路径push到router中。更改上面的代码:
export default React.createClass({
// ask for
router
from contextcontextTypes: {
router: React.PropTypes.object
},
// ...
handleSubmit(event) {
// ...
this.context.router.push(path)
},
// ..
})
打包运行再试试吧:在输入框中输入你喜欢的男神名字:李易峰、杨洋、鹿晗、胡歌、霍建华、吴磊等等,只要你输入,就可以看到他们了。
服务器端渲染
之前讲解的都是客户端的index.js的处理。现在来看服务器app.js的处理。服务器渲染的核心是React中很简单的一个概念。:
1.我们想要把路由封装成一个模块,以便服务器和客户端都可以请求它。创建文件modules/routes.js,并把路由和组件都写在这里。
import React from 'react'
import { Route, IndexRoute } from 'react-router'
import App from './App'
import Girls from './Girls'
import Boys from './Boys'
import Boy from './Boy'
import Home from './Home'
module.exports = (
)
2.更新index.js的内容:
import React from 'react'
import { render } from 'react-dom'
import { Router, browserHistory } from 'react-router'
import routes from './modules/routes'
render(
document.getElementById('app')
)
3.在服务器上渲染,我们会得到一个空白屏,这是因为服务器渲染是异步的,而路由匹配是同步的。虽然本应用不是加载数据的,但仍可以看到会发生什么。打开app.js,从router中引入 match 和 RouterContext,然后将路由匹配到URL,最后渲染:
// ...
import React from 'react'
import { renderToString } from 'react-dom/server'
import { match, RouterContext } from 'react-router'
import routes from './modules/routes'
// ...
// send all requests to index.html so browserHistory works
app.get('*', (req, res) => {
match({ routes: routes, location: req.url }, (err, redirect, props) => {
const appHtml = renderToString(
res.send(renderPage(appHtml))
})
})
function renderPage(appHtml) {
return `
`
}
var PORT = process.env.PORT || 8080
app.listen(PORT, function() {
console.log('Production Express server running at localhost:' + PORT)
})
4.编译服务器代码
bundle --presets react,es2015 ./app.js -o server.bundle.js
此时运行server.bundle.js,你会看到服务器正在将应用发送到浏览器。当你点击时,你会发现客户端应用接管了处理,而不是向服务器请求界面。很酷吧。