代码地址
https://github.com/hql123/reactJS-ruby-china
Demo
https://hql123.github.io/reactJS-ruby-china/
在之前的用React+Redux写一个RubyChina山寨版(一)中我们已经把项目的基础框架搭建起来了,这一章主要讲述如何搭建 RubyChina 的首页(暂无自适应)。
Ruby导航栏
在开始写代码之前我们先引入一个灰常厉害的UI框架:antd,蚂蚁金服你值得信赖的好伙伴其实也不是非用不可的,用Boostrap也行,总之还是那句话我们就是不停折腾用下来对antd的感受大致就是:动态效果好棒棒响应式布局完全没有!当我意识到这个的时候我的表情是这样的:
/(ㄒoㄒ)/~~没有自适应
npm install --save antd
首先,我们在webpack的配置文件中加上对 antd
的按需加载:
// webpack.config.dev.js
{
test: /\.(js|jsx)$/,
include: paths.appSrc,
loader: 'babel',
query: {
babelrc: false,
presets: [require.resolve('babel-preset-react-app')],
plugins: [
['import', [{ libraryName: "antd", style: 'css' }]]
],
cacheDirectory: true
}
}
然后我们就不需要在文件中加上类似 import '../antd.css'
之类的代码了 。
上一章我们已经把 react-router-redux
加入到我们的项目中,并且实现了大概配置,这一章我们要将这个插件和 antd
的菜单导航联合使用。
还记得上一章的异步加载数据的例子中出现的 selectedTab
和 switchTab
么,它们是挂在状态树中用来对页面元素进行切换状态管理的,现在我们使用 router
的时候可以把这两个去掉了,因为我们的router
已经成功挂在状态树中,对于路由的切换结果可以直接从 state
当中取。
antd
中提供了非常方便的一个 Layout
布局,实现基本的上中下-侧边栏格局。
import { Layout, Affix } from 'antd';
const { Header, Footer, Content } = Layout;
let App = ({ children }) => {
return (
<Layout className='layout'>
<Affix><Header><HeaderComponent /></Header></Affix>
<Content style={{ minHeight: 300 }}>
{children}
</Content>
<Footer style={{ textAlign: 'center' }}><FooterComponent /></Footer>
</Layout>
);
}
这样就可以实现导航-内容-底部+导航固定在顶部,是不是很方便感觉自己随时要上天了?!(≧≦)/啦啦啦
顺便说一句,这个{ children }
就是我们在 route.js
中定义的各个路由对应的组件:component={Home}
然后我们就开始写 header.js
,这个顶部的导航可以做好封装,在切换路由的时候就可以保持一直在页面中了。
我们待会要实现这样的菜单:
我们先把基本组件 import
进来:
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { Menu, Form, Icon, Input } from 'antd'
import { Link } from 'react-router'
import '../assets/styles/header.css'
const FormItem = Form.Item;
header.css
是我们自己定义的样式文件。
引入组件之后我们直接把导航菜单渲染出来:
<Menu selectedKeys={[this.state.current]}
mode="horizontal"
id="nav">
<Menu.Item key="/topics">
<Link to="/topics">社区</Link>
</Menu.Item>
<Menu.Item key="/wiki">
<Link to="/wiki">Wiki</Link>
</Menu.Item>
<Menu.Item key="/sites">
<Link to="/sites">酷站</Link>
</Menu.Item>
<Menu.Item key="/homeland">
<Link to="/homeland">Homeland</Link>
</Menu.Item>
<Menu.Item key="/jobs">
<Link to="/jobs">招聘</Link>
</Menu.Item>
<Menu.Item key="gems">
Gems
</Menu.Item>
</Menu>
我们给每个MenuItem都设置了跟路由相同的 key
,这个key是用来待会用来记录 menu
是否被 激活。我们并不需要对点击事件做任何处理,因为我们有 router
呀~
当我们触发 Link
的时候我们的状态树就会记录下来最新的 pathname
,通过 mapStateToProps
方法获取最新的 state
const mapStateToProps = (state, props) => {
return {
pathname: state.routing.locationBeforeTransitions.pathname,
}
}
然后我们在构造函数中初始化一个局部的 state :
constructor(props) {
super(props);
this.state = {
current: this.props.pathname,
};
}
使 this.state.current
等于当前的 pathname
,当 state 树绑定的数据有变化时, 我们可以动态设置这个 current
componentWillReceiveProps(nextProps) {
if (nextProps.pathname !== this.state.current) {
this.setState({
current: nextProps.pathname,
})
}
}
由于我们在 Menu
中设置了选中的 key
: selectedKeys={[this.state.current]
,这个时候菜单栏就会进行切换。
现在我们就实现了导航的菜单列表,再自定义样式加上左边的logo和右边的搜索框和注册登录按钮,我们的导航就实现了。是不是很简单的说~
社区页的标签页切换
这部分内容我们最好不要写在header里面,因为貌似只有社区主页才有这个标签切换,所以我们直接写在社区的组件里。
Paste_Image.png这个我们不用 antd 的 Tab,我们自己写就行了,其实也很简单的
样式这一块我就不做赘述了,我们还是用 Layout 布局实现的社区主页:
<Layout>
<Header id="node-header">
<ul className="node-filter">
<li><Button type="default" className="node-button">所有节点<Icon type="right" /></Button></li>
<li className={ this.state.current === '/topics' ? 'active' : '' } ><Link to='/topics'>默认</Link></li>
<li className={ this.state.current === '/topics/popular' ? 'active' : '' } ><Link to='/topics/popular' ><Icon type="smile-o" />优质帖子</Link></li>
<li className={ this.state.current === '/topics/no_reply' ? 'active' : '' }><Link to='/topics/no_reply' >无人问津</Link></li>
<li className={ this.state.current === '/topics/last' ? 'active' : '' } ><Link to='/topics/last' >最新发布</Link></li>
</ul>
</Header>
<Layout className="main">
<Content className="main-content"><Topics /></Content>
<Sider className="main-sider"><Siderbar /></Sider>
</Layout>
</Layout>
妈蛋把我的代码变得好乱,可以的话还是去看我的github上面的代码,这边不一定全面毕竟我也是一边写代码一边写文档的,也算是记录成长的痕迹吧
简单来说就是定义好切换的列表,给每一个<li>
设置一个动态的className
,当this.state.current
等于这个<li>
对应的路由或key的时候就显示出来,表示这个标签被触发,this.state.current
的状态变化还是跟之前一样,初始化的时候等于状态树中指定路由的pathname
,当路由改变的时候就设置一下this.state.current
的值等于最新的当前的pathname
。是不是好简单的说~
注意一下,由于我们的社区页其实对路由是有两个控制的,一个是顶部的导航栏,一个是内容的标签栏,我这边设置都是切换路由,所以如果不对这个进行设置的话就会发现,当你切换内容的标签栏的时候,我们顶部导航栏焦点就会跑掉了!
其实只需要一个简单的操作就可以解决,我们在header.js中写一个方法:
getCurrent(pathname){
switch(pathname) {
case '/topics':
case '/topics/popular':
case '/topics/no_reply':
case '/topics/last':
return '/topics';
default :
return pathname;
}
}
就是当切换标签所更新的路由都指向社区的key。我们初始化的时候就可以这么写:
this.state = {
current: this.getCurrent(this.props.pathname),
};
当接收到更新的时候:
componentWillReceiveProps(nextProps) {
if (this.getCurrent(nextProps.pathname) !== this.state.current) {
this.setState({
current: nextProps.pathname,
})
}
}
这样就没问题了~
社区页列表和分页实现
接下来我们就可以开始加载数据并且渲染到我们的组件中去。
首先我们在topics.js的组件中初始化构造函数:
constructor(props) {
super(props);
this.state = {
current: this.props.pathname,
isFetching: this.props.isFetching,
};
}
componentDidMount() {
const { fetchTopicsIfNeeded } = this.props
fetchTopicsIfNeeded(this.props.pathname);
}
当加载页面的时候我们就判断是否需要fetch数据
然后我们在render中对异步加载的渲染进行处理,肯定是先出现一个loading的动画,然后成功就出现列表,不成功就出现一个错误提示~这里我们可以使用 antd 的加载动画和alert提示。
render() {
const {topics, isFetching, error } = this.props
const isEmpty = topics.length === 0;
const errorMsg = error;
const container = (
isEmpty || errorMsg ? <Alert message="数据加载失败,真相只有一个!" description="请检查你的网络状态" type="info" />
: <div className="topics">
{topics.map((topic, i) =>
<TopicItem key={topic.id} topic={topic} />
)}
</div>
);
return (
<Spin spinning={isFetching} tip="Loading...">{container}</Spin>
);
}
获取数据的方法我们之前已经讲过了,这里也不再重复,直接上代码,在状态树更新的时候我们就再判断是否需要更新数据:
componentWillReceiveProps(nextProps) {
if (nextProps.pathname !== this.state.current || nextProps.isFetching !== this.state.isFetching) {
const { fetchTopicsIfNeeded, invalidateTab } = nextProps;
this.setState({
current: nextProps.pathname,
isFetching: nextProps.isFetching,
})
if (nextProps.pathname !== this.state.current || Number(nextProps.page) !== this.state.page) {
invalidateTab(nextProps.pathname);
fetchTopicsIfNeeded(nextProps.pathname);
}
}
}
定义好TopicItem
组件后,数据就可以成功渲染了~
我们看看首页差不多了,右边的侧边栏我这边就不讲了,都是些静态的html元素,我们这里还差个分页,分页样式可以使用antd
的Pagination
就足够了。
<Pagination onChange={this.handleChangePage} total={700} current={this.state.page} />
我们在构造的state中加入新的字段 page
constructor(props) {
super(props);
this.state = {
current: this.props.pathname,
isFetching: this.props.isFetching,
page: Number(this.props.page)
};
this.handleChangePage = this.handleChangePage.bind(this);
}
我们之前在做异步加载数据例子的时候并没有在状态树中加上page这个对象,现在我们在 mapToStateProps 方法中加上我们需要用到的对象:
const {pathname, search, query} = state.routing.locationBeforeTransitions;
const page = query.page || 1
...
return {
pathname,
page,
search,
...
}
这个时候我们就可以取到路由中的page并把它放到我们自己定义的state中了。
那么我们怎么让路由变成page=2这样的形式呢?
我们在分页组件中加入了一个onChange的方法,这个方法默认传入 page的参数,就像这样:
handleChangePage(page){
const { pathname } = this.props
browserHistory.push({
pathname: pathname,
query: { page: page }
});
}
我们可以直接跳转到page变化的对应路由,这样我们的pagination就会触发到对应的页码。
这个时候你会发现其实还有很多问题,比如api接口中并没有page的参数,只有offset的参数,但是我们的路由希望能够保持纯净的page参数和与api并不相同的路由方式,就不能单纯得将这些pathname直接发到api中,需要做转换。
我们在fetch列表的时候希望能传入page的参数,但是不能跟我们定义的标签页的触发条件和导航栏的触发条件有冲突,所以这里我们需要再加一个参数到fetchTopicsIfNeeded的action中,我们把search(例如:?page=2)加入到参数里,我们在fetchData的方法里面加入这样一行代码:
const api = url + urlTranslate(pathTranslate(tag), search);
然后对传入的不同路由做好规定,再请求api接口
const pathTranslate = (tag) => {
switch(tag) {
case '/jobs':
return '/topics?node_id=25'
case '/topics/popular':
return '/topics?type=popular'
case '/topics/no_reply':
return '/topics?type=no_reply'
case '/topics/last':
return '/topics?type=recent'
default :
return tag
}
}
const urlTranslate = (path, search) => {
let offset = 0;
if (search.indexOf('page') > -1) {
var end = new RegExp(/\d+$/);
const page = end.exec(search)[0];
if (Number(page) > 1) {
offset = (Number(page) - 1)*25;
}
}
if (path.indexOf('?') > 0) {
return path + '&offset=' + offset + '&limit=25';
}
return path + '?offset=' + offset + '&limit=25';
}
然后我们在componentWillReceiveProps函数中加入对page的判断条件,当page改变时,就需要重新fetch数据:
componentWillReceiveProps(nextProps) {
if (nextProps.pathname !== this.state.current || nextProps.isFetching !== this.state.isFetching || Number(nextProps.page) !== this.state.page) {
const { fetchTopicsIfNeeded, invalidateTab } = nextProps;
this.setState({
current: nextProps.pathname,
isFetching: nextProps.isFetching,
page: Number(nextProps.page)
})
if (nextProps.pathname !== this.state.current || Number(nextProps.page) !== this.state.page) {
invalidateTab(nextProps.pathname);
fetchTopicsIfNeeded(nextProps.pathname, nextProps.search);
}
}
}
忘了说,invalidateTab函数修改一个状态,告诉状态树现在的数据可以被刷新并重新加载。
好了,我们的首页就完成了
网友评论