用React+Redux写一个RubyChina山寨版(二)

代码地址

https://github.com/hql123/reactJS-ruby-china


Demo

https://hql123.github.io/reactJS-ruby-china/


在之前的用React+Redux写一个RubyChina山寨版(一)中我们已经把项目的基础框架搭建起来了,这一章主要讲述如何搭建 RubyChina 的首页(暂无自适应)。


Ruby导航栏

在开始写代码之前我们先引入一个灰常厉害的UI框架:antd,蚂蚁金服你值得信赖的好伙伴其实也不是非用不可的,用Boostrap也行,总之还是那句话我们就是不停折腾用下来对antd的感受大致就是:动态效果好棒棒响应式布局完全没有!当我意识到这个的时候我的表情是这样的:

Paste_Image.png

/(ㄒ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 的菜单导航联合使用。
还记得上一章的异步加载数据的例子中出现的 selectedTabswitchTab么,它们是挂在状态树中用来对页面元素进行切换状态管理的,现在我们使用 router 的时候可以把这两个去掉了,因为我们的router已经成功挂在状态树中,对于路由的切换结果可以直接从 state 当中取。


antd中提供了非常方便的一个 Layout 布局,实现基本的上中下-侧边栏格局。

Paste_Image.png
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 ,这个顶部的导航可以做好封装,在切换路由的时候就可以保持一直在页面中了。
我们待会要实现这样的菜单:

Paste_Image.png

我们先把基本组件 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元素,我们这里还差个分页,分页样式可以使用antdPagination就足够了。

<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函数修改一个状态,告诉状态树现在的数据可以被刷新并重新加载。
好了,我们的首页就完成了

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

推荐阅读更多精彩内容