react、redux什么的都用起来 【3】穿越spa的路由
接着上回新闻搜索的例子。现在我们要通过路由进入一个新的页面来查看新闻详细内容。
react和路由并没有什么直接关系,用什么路由都可以。不过使用react-router可以让我们的代码风格统一, 并且有些工具使用起来很方便。
先来安装react-router库(我目前安装的版本是2.0.1,跟1.x版本区别比较大):
npm install react-router --save
从使用上来说,react-router不过是一些react组件,所以用起来特别方便。不用多说,看个例子就知道怎么用了。 先把咱们已经做好的Login和NewsList两个页面放到路由里。只需修改src/index.js文件:
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { Router, Route, browserHistory } from 'react-router'
import configureStore from './stores';
import Login from './containers/Login'
import NewsList from './containers/NewsList';
const store = configureStore();
render(
<Provider store={store}>
<Router history={browserHistory}>
<Route path="newslist" component={NewsList} />
<Route path="login" component={Login} />
</Router>
</Provider>,
document.getElementById('app')
);
这个文件相比以前只是把Provider标签里面的内容换了。以前咱们只放一个Login或者NewsList组件, 现在是放一个Router组件。Router组件只需要一个history属性,让我们可以选择使用哪种历史管理方式。 我们常用的就是browserHistory和hashHistory。browserHistory就是我们最熟悉的浏览器管理历史, 使用这种历史管理方式感觉上跟普通浏览网页的方式一样:url路径会随着跳转及前进、后退按钮而变化, 但是在react-router的browserHistory管理下,url的变化不会导致页面刷新。 hashHsitory只控制url中#号后面的部分,这是前一段时间单页应用比较通用的方式,但是随着HTML5的普及, 这个方式有逐渐被淘汰的趋势。这里我们使用browserHistory。
现在我们已经可以通过http://localhost:8000/newslist访问上一节做的新闻列表页面了。
接着把新闻详情页做出来吧。由于我们在新闻列表接口已经取到了全部的新闻内容,也为了简单,也为了反应快, 我们就直接用新闻列表接口提供的数据,而不再访问服务器了。
数据都在store里,任我们怎么玩。新闻详情页访问数据有两种方案:一种是记录新闻列表的index,然后直接根据index访问列表里相应的内容; 另一种是把要打开的新闻内容单拿出来一份另放到一个state里。我们用第二种方案。 还是先写action,直接在src/actions/news.js里面添加内容:
export const SET_CURRENT_NEWS = 'SET_CURRENT_NEWS'
const setCurrent = cac(SET_CURRENT_NEWS, 'news')
export const chooseNews = index => (dispatch, getState) => {
let current = getState().news.list[index]
dispatch(setCurrent(current))
}
setCurrentNews就是要把一个新闻对象放到相应的state中。chooseNews则是在组件里要调用的, 它根据一个index找出相应的新闻对象并放到当前新闻的state里。
然后往src/reducers/news.js添加新的reducer:
current: cr({}, {
[SET_CURRENT_NEWS](state, {news}){return news}
})
别忘了引入新定义的的action常量。
NewsList组件得派发设置当前新闻的动作,并跳转到新闻详情页面,只需要改renderList方法就行:
renderList(){
return this.props.list.map((item, i) =>{
item.key = item.title
item.onGotoDetail = () => {
this.props.dispatch(chooseNews(i))
this.props.history.push('/newsviewer')
}
return React.createElement(NewsOverview, item)
})
}
这里给每个NewsOverview组件都传了个onGotoDetail属性,NewsOverview在被点击时要调用这个属性的函数,只需要在最外层div加个click事件处理,像这样:
<div onClick={this.props.onGotoDetail}>
在item.onGotoDetail函数中有个this.props.history,它就是我们前面在构建路由时选择的那个browserHistory,当我们的组件作为Route组件的属性使用时,Route会给我们的组件注入这个history属性,这样用起来就比较方便了。这个history的方法和浏览器里的history所拥有的那几个方法功能差不多,常用的就是go(跳转)、goBack(回退一个历史)、goForword(前进一个历史)、push(跳转到一个url并添加一个历史状态)、replace(跳转到一个url并替换当前历史状态)。具体的可以参考专门对浏览器history论述的文章。如果我们想在组件之外控制历史状态(比如action里),从react-router里引入browserHistory或hashHsitory直接用就可以。
最后添加新闻详情页面的组件,这就很简单了吧。不过这个组件跟NewsOverview比较起来实在太像,就是新闻概述和详细内容的区别。 所以这里我偷个懒,让NewsOverview通过一个属性变身为可配置成新闻详情的组件。把NewsOverview里面最后一个P标签改成这样就行:
{this.props.showDetail ?
<p dangerouslySetInnerHTML={{__html:this.props.message}}/> :
<p>{this.props.description}</p>
}
然后新建个src/containers/NewsViewer.js,它就很简单了:
import React from 'react'
import {connect} from 'react-redux'
import NewsOverview from 'components/NewsOverview'
class NewsViewer extends React.Component{
render(){
return (
<div>
{React.createElement(NewsOverview, Object.assign({
showDetail: true
}))}
</div>
)
}
}
export default connect(state => {return {news: state.news.current}})(NewsViewer)
最后在index.js里面再添加一个路由:
<Route path="newsviewer" component={NewsViewer} />
功能是完美地实现了,但是想一下我们为什么要用路由?而且还要用浏览器管理历史的路由? 一个很重要的原因就是网站不同于app,它要保证输入任何一个有效的url后都要给用户呈现出一个可用的页面。 一个非常实用的场景就是刚才我在新闻详情页里阅读到一则很好的新闻,想给分享出去,那别人要通过这个url还能查看到这个新闻。 我们目前没做到这个。现在我们要实现依靠id访问到新闻。
id一定是通过url传来的,可以用query参数,但我们用一个更简洁的形式:“/newsviewer/30998729”,后面那串数字是新闻的id。 配置很简单,把新闻详情页的路由改成这样就行了:
<Route path="newsviewer/:id" component={NewsViewer} />
然后要修改src/containers/NewsList.js里面路由跳转的那句:
this.props.history.push('/newsviewer/' + item.id)
NewsViewer组件将要加载时让它去获取一下新闻详细内容。还记得目前数据来源是直接从新闻列表里拽过来的是吧, 没关系,还让它拽吧,这样既能有一般情况下访问的“唰”一下的用户体验,又能保证直接访问url能获取到内容。
给src/actions/news.js再加一个获取数据的action:
export const fetchNewsDetail = id => dispatch => window.$.ajax({
url: 'http://www.tngou.net/api/top/show',
data: {id},
dataType: 'jsonp',
success: data => data.status && dispatch(setCurrentNews(data))
})
给src/containers/NewsViewer.js加一个componentWillMount方法,让组件将要加载时就去获取数据:
componentWillMount(){
// 在react-router的帮助下,我们可以很轻松地拿到url路径上的参数id
this.props.dispatch(fetchNewsDetail(this.props.params.id))
}
现在就可以直接通过http://localhost:8000/newsviewer/3864来访问新闻详情页面了。哦,可能会有找不到assets/app.js的报错, 在index.html里面把引用他的路径改成绝对路径“/assets/app.js”就行了。
react-router的路由并不是扁平的,而是树状结构的,不仅路径可以组织成树状结构,组件也可以组织成相应的树状结构。
比如我们想要个通用的header,里面还有返回和登录按钮。先把header作为一个组件写出来再说。
src/components/Header.js:
import React from 'react';
import {Link} from 'react-router'
export default class Header extends React.Component {
render(){
let styl = {
textAlign:'center',
lineHeight:'32px',
width:'15%',
float:'left'
}
return (
<div style={{background: '#ddd', height:'32px'}}>
<div style={styl} onClick={this.props.onGoBack}>{'<'}</div>
<div style={Object.assign({},styl,{width:'70%'})}>{this.props.text}</div>
<Link style={Object.assign({},styl,{float: 'right'})} to="/login">登录</Link>
</div>
)
}
}
然后再把原来那个App.js找回来吧,它作为路由中的顶层组件,对应根路径“/”。把前面做的Header放进去:
src/containers/App.js:
import React from 'react';
import Header from 'components/Header'
class App extends React.Component {
render() {
return (
<div>
<Header onGoBack={this.goBack.bind(this)} text="欢迎访问"/>
<div style={{paddingTop:'10px'}}>
{this.props.children}
</div>
</div>
)
}
goBack(){
this.props.history.goBack()
}
}
export default connect()(App);
上面代码的render方法里,除了放进去了Header,还要注意那个this.props.children,react-router就是把这个属性所对应的组件作为App所对应路径的下一级路由的。
再来改一下src/index.js里面的路由。由于以后路由会越来越多,所以我打算把所有的route标签拿出去,放到一个单独的src/routes.js文件里,index.js里只要引入这个文件并放到原来route们的位置上就行了。
src/routes.js
import React from 'react'
import { Route } from 'react-router'
import App from './containers/App';
import Login from './containers/Login'
import NewsList from './containers/NewsList';
import NewsViewer from './containers/NewsViewer'
export default (
<Route path="/" component={App}>
<Route path="news" component={NewsList} />
<Route path="news/:id" component={NewsViewer} />
<Route path="login" component={Login} />
</Route>
)
做一个小小的手脚,为了url简洁,我把原来的newslist改成了news,而news后面加斜杠id的形式作为新闻详情。这两个url是平级的,看上去像是父子关系,其实结构上是完全平等的。别忘了NewsOverview.js里的连接也要改。
现在访问/news可以搜索新闻,点击新闻标题可以跳转到/news/xxx查看详细内容,点击登录可以跳转登陆页,可是,访问根路径却只有一个带标题的空白页。我们可以加一个默认页面,就是在访问某一级带有子路径路由时,可以给它一个对应到这个路径的页面,不一定是跟路径哦。做个索引作为默认页面吧,src/containers/Index.js:
import React from 'react';
import {Link} from 'react-router'
class Index extends React.Component {
render(){
return (
<ul>
<li><Link to="/news">新闻</Link></li>
</ul>
)
}
}
export default Index
虽然这个组件目前没有连接到redux,我还是忍不住把它放到了containers目录下面,毕竟它是一个页面级别的组件,没准哪天产品经理有个啥想法它就要和外界打交道了。
然后添加路由,这个路由比较特殊,不是用Route,而要用个专门的组件IndexRoute,整个src/routes.js代码如下:
import React from 'react'
import { Route, IndexRoute } from 'react-router'
import Index from './containers/Index';
import App from './containers/App';
import Login from './containers/Login'
import NewsList from './containers/NewsList';
import NewsViewer from './containers/NewsViewer'
export default (
<Route path="/" component={App}>
<IndexRoute component={Index} />
<Route path="news" component={NewsList} />
<Route path="news/:id" component={NewsViewer} />
<Route path="login" component={Login} />
</Route>
)
至此,我们可以用react和相关技术打造完整的单页web应用了。