react+redux+webpack教程3

现代web页面里到处都是ajax,所以处理好异步的代码非常重要。

这次我重新选了个最适合展示异步处理的应用场景——搜索新闻列表。由于有现成的接口,我们就不用自己搭服务了。 我在网上随便搜到了一个新闻服务接口,支持jsonp,就用它吧。

一开始,咱们仍然按照action->reducer->components的顺序把基本的代码写出来。先想好要什么功能, 我设想的就是有一个输入框,旁边一个搜索按钮,输入关键字后一点按钮相关的新闻列表就展示出来了。

首先是action,现在能想到的动作就是把新闻列表放到仓库里,至于列表数据是哪儿来的一会儿再说。 来看src/actions/news.js:

import {cac} from 'utils'

export const PUSH_NEWS_LIST = 'PUSH_NEWS_LIST'

export const pushList = cac(PUSH_NEWS_LIST, 'list')

然后是reducer,没什么特别的,只要遇到上面定义的那个action,就把数据放到相应的状态里就行了。 我们先定一个叫做news的状态,里面再包含一个子状态list。后面还要扩充功能,还会给news状态添加更多的子状态。 以下是src/reducers/news.js的代码:

import {combineReducers} from 'redux';

import {cr} from '../utils'

import {PUSH_NEWS_LIST} from 'actions/news'

export default combineReducers({
  list: cr([], {
    [PUSH_NEWS_LIST](state, {list}){return list}
  })})

现在就可以开始写组件了。这回我们要做的是个列表,也就是要有重复的东西,我想最好把重复的东西单抽取成一个组件以便维护和复用。 那就把一条新闻抽取成一个组件吧,它应该具有标题、发布时间、图片以及概述这些内容。 这个组件绝对是纯纯的,不用跟外界打交道,所以把它放到components目录里。src/components/NewsOverview.js:

import React from 'react';

class NewsOverview extends React.Component {
  render(){
    let date = new Date(this.props.time)
    return (
      <div>
        <h2>{this.props.title}</h2>
        <div style={{padding:'16px 0',color: '#888'}}>
          {date.toLocaleDateString()} {date.toLocaleTimeString()}
        </div>
        <div style={{textAlign:'center'}}>
          <img src={this.props.img} style={{maxWidth:'100%'}}/>
        </div>
        <p>{this.props.description}</p>
      </div>
    )
  }}export default NewsOverview

然后写要跟外界打交道的组件,这个组件需要响应用户的点击按钮的事件,发起获取新闻列表的请求,然后把数据放到页面里。 src/containers/newsList.js:

import React from 'react';

import { connect } from 'react-redux'

import NewsOverview from 'components/NewsOverview'

import {pushList} from 'actions/news'

class NewsList extends React.Component {
  search(){
    let keyword = this.refs.keyInput.value    // TODO: 获取新闻列表
  }
  renderList(){
    return this.props.list.map(item =>{
      item.key = item.title      
return React.createElement(NewsOverview, item)
    })
  }
  render(){
    return (
      <div>
        <div>
          <input ref="keyInput"/>
          <button onClick={this.search.bind(this)}>搜索</button>
        </div>
        <div>
          {this.renderList()}
        </div>
      </div>
    )
  }}function mapStateToProps(state) {
  // 一般一组状态都是为一个页面服务的,所以把它们一股脑的映射过来比较方便
  // 但是把映射一一写出来也有好处,就是很容易看到组件里有什么属性
  return Object.assign({}, state.news)}

export default connect(mapStateToProps)(NewsList);

代码差不多了,但是它现在没法工作,因为我们还没给添加ajax请求的代码。最简单粗暴的方法就是在上面的search方法中直接来个ajax请求, 然后在回调中派发“PUSH_NEWS_LIST”的action。也行。先写出来吧。为了简化ajax代码,我在src/index.html里面引入了jQuery。 当然,用了react,我们也许用不上jQuery的其他功能,所以用fetch或者其它ajax库都行。

search(){
  let keyword = this.refs.keyInput.value
  window.$.ajax({
    url: 'http://www.tngou.net/api/search',
    data: { keyword, name: 'topword' },
    dataType: 'jsonp',
    success: (data)=>{
      if(data.status)
        this.props.dispatch(pushList(data.tngou))
    }
  })}

最后别忘了修改入口、添加reducer:把src/index.js里面Provider下面的组件换成NewsList; 在src/reducers/index.js里面引入新增的reducer,并加到reducers对象里。

好了,试一下,输入个关键字点击搜索,新闻列表如约而至。但是不能到这就满足啊。

我们希望组件尽可能接近纯函数,组件要跟外界打交道要通过connent函数连接到仓库,仓库所存的状态才是可以被外界改变的。 组件里的表单带来的外界影响实在是没办法,但是连网络请求都塞到组件里实在是不雅观。从维护上讲,我们的组件只是要展示出新闻列表, 它不想管是哪里来的新闻列表,更不愿意管你新闻列表是异步请求来的或是同步从本地文件读取来的, 它只是想:我发起一个action,你根据这个action给我咱们约定好格式的数据就行了。

OK,action,我们应该变换动作来伺候好组件。那么改action吧。目前来看我们的action是同步的,怎么能让它异步呢? 也就是我发起一个action,给个回调的机会,让它过一会儿能发起另一个action。

朴素的action是没有这个能力的。这时候中间件该上场了。

中间件是一个软件行业里比较混乱的词汇。运维人员管weblogic甚至tomcat叫中间件;SOA里面管流程中间的服务叫中间件。 再加上现在很多软件大厂都声称自己是中间件的供应商,让中间件这个词听起来都十分高大上。高大上的东西太恐怖, 我只理解node的web框架express里的中间件,就是在处理请求时插入到流程中间可以加工请求数据或者根据请求数据做点别的事情的函数。 这个概念应该跟SOA的中间件差不多,但十分简单明了。redux的中间件也是如此。既然它要“做点别的事情”, 说明它往往不会是个纯函数,总要搞点副作用出来,ajax请求就是要搞副作用。

我们派发一个action(实际是store派发的),这个action最终会被reducer处理,在这之前redux允许我们插入中间件搞点别的事情。 举个简单的例子,我们在中间件里可以打印日志。下面,先别着急修改我们的ajax请求,先通过打印一些日志来熟悉一下中间件。

action的派发和被reducer处理都是由store控制的,所以中间件的注册应该在store的代码里。 我们来修改src/stores/index.js:

const { createStore, applyMiddleware } = require('redux');

const reducers = require('../reducers');

const logger = store => next => action => {
  window.console.log('dispatching', action)
  next(action)
  window.console.log('next state', store.getState())}

module.exports = function(initialState) {
  let createStoreWithMiddleware = 
applyMiddleware(logger)(createStore)
  let store = createStoreWithMiddleware(reducers, initialState)
  // 原来生成的文件里这里有一段热加载的代码,若要保留热加载功能请自行留下这段代码
  return store}

来看下中间件logger函数,它先打印出了正在派发的action,然后通过调用next让action执行, 最后在action执行结束后打印出了最终的仓库状态。很简单吧,就是在派发action的过程中搞点打印日志的事情。

回到我们的目标上来,我们希望的是一个action派发后做一些异步的事情,然后给个机会执行回调。 如果是异步的,action就不会立刻送到reducer那里,那就需要两个action,一个action是通知异步开始执行, 另一个action是我们熟悉的reducer所需要的action。既然第一个action不需要给reducer传达指令而要做些别的事情, 那他是个函数就行了。中间件需要做的事情就是遇到类型为函数的action就直接执行,遇到普通的action就正常发送给reducer。 于是这个中间件就是这个样子:

const thunk = store => next => action =>
  typeof action === 'function' ?
    action(store.dispatch, store.getState) :
    next(action)

其实这个名为thunk的中间件在npm上有现成的,安装一下就行了:

npm install redux-thunk --save

然后在src/store/index.js里面注册它:

import { createStore, applyMiddleware } from 'redux'

import thunk from 'redux-thunk'

import reducers from '../reducers'

module.exports = function(initialState) {
  // 原来的日志中间件先给去掉了,
其实applyMiddleware的参数列表里面是可以放任意多个中间件的
  let createStoreWithMiddleware = applyMiddleware(thunk)(createStore)
  let store = createStoreWithMiddleware(reducers, initialState)
  return store}

现在就可以把ajax的代码移到src/actions/news.js里面了:

import {cac} from 'utils'

export const PUSH_NEWS_LIST = 'PUSH_NEWS_LIST'

const pushList = cac(PUSH_NEWS_LIST, 'list')

export function fetchList (keyword){
  return dispatch => {
    window.$.ajax({
      url: 'http://www.tngou.net/api/search',
      data: { keyword, name: 'topword' },
      dataType: 'jsonp',
      success: (data)=>{
        if(data.status)
          dispatch(pushList(data.tngou))
      }
    })
  }}

在组件src/containers/NewsList.js里面,不再需要pushList,而需要fetchList这个可用于中间件trunk的action:

import React from 'react';

import {connect} from 'react-redux'

import NewsOverview from 'components/NewsOverview'

import {fetchList} from 'actions/news'

class NewsList extends React.Component {
  search(){
    let keyword = this.refs.keyInput.value    
this.props.dispatch(fetchList(keyword))
  }// ...

好了,组件回到了纯洁的样子,ajax获取数据依然没有问题。

thunk中间件虽然非常简单,但它让redux具有了在action里面派发action的能力,这样我们的action就不仅仅是指导reducer如何处理状态, 而可以做一切不纯粹处理数据的事情。但是我们应该尽量避免action的膨胀,是处理数据的事儿就让reducer去做, 是界面的事儿就交给组件,这样才能让逻辑尽可能的清晰。

我们来把这个应用做得更完善一些吧。作为一个新闻列表,不能分页不太像话。来改造一下。

还是从action开始。需要什么新的动作吗?设置总数、页码?其实我们在一个ajax请求中已经把这些数据都获取到了, 设置这些都是处理数据的事儿,把它们放到action里有些不合适,还是让reducer去处理比较好。 在action里,我们只需要把所有有用的数据都传给reducer,嗯,名字也最好改个合适的。 除此之外,关键字也要保存到状态里,以供翻页时使用。这里把fetchList函数设计得多功能一些: 翻页时不传keyword,新查询时不传页码

src/actions/news.js:

import {cac} from 'utils'

export const RECEIVE_NEWS_LIST = 'RECEIVE_NEWS_LIST'

export const SET_KEYWORD = 'SET_KEYWORD'

export const PAGE_SIZE = 10

const receiveList = cac(RECEIVE_NEWS_LIST, 'data', 'page')

const setKeyword = cac(SET_KEYWORD, 'value')

export function fetchList (keyword, page=1){
  return (dispatch, getState) => {
    if(!keyword)
      keyword = getState().news.keyword    else
      dispatch(setKeyword(keyword))
    window.$.ajax({
      url: 'http://www.tngou.net/api/search',
      data: { keyword, name: 'topword', page, rows:PAGE_SIZE },
      dataType: 'jsonp',
      success: (data)=>{
        if(data.status)
          dispatch(receiveList(data, page))
      }
    })
  }}

reducer改动就比较大了,对于同一个“RECEIVE_NEWS_LIST”的动作,好几个状态都要进行修改。

src/reducers/news.js:

import {combineReducers} from 'redux';import {cr} from '../utils'import {RECEIVE_NEWS_LIST, SET_KEYWORD, PAGE_SIZE} from 'actions/news'export default combineReducers({
  list: cr([], {
    [RECEIVE_NEWS_LIST](state, {data}){return data.tngou}
  }),
  totalPage: cr(0, {
    [RECEIVE_NEWS_LIST](state, {data}){return Math.ceil(data.total/PAGE_SIZE)}
  }),
  page: cr(1, {
    [RECEIVE_NEWS_LIST](state, {page}){return page}
  }),
  keyword: cr('', {
    [SET_KEYWORD](state, {value}){return value}
  })})

页码的展示一定要单独写一个组件,因为它被复用的几率太大了。我这里就简单写一个,省略号、上下页之类的先不搞了。

src/components/pager.js

import React from 'react';class Pager extends React.Component{
  renderNumbers(){
    let {page, totalPage, onChangePage} = this.props    return Array.from({length:totalPage}, (x,i)=>{
      ++i;
      let style = {
        display: 'inline-block',
        border: 'solid 1px #ddd',
        padding: '5px',
        margin: '2px',
        color: page==i ? 'red' : '#999'
      }
      return <b style={style} onClick={()=>{onChangePage(i)}}>{i}</b>
    })
  }
  render(){
    return <div> {this.renderNumbers()} </div>
  }}Pager.propTypes = {
  page: React.PropTypes.number.isRequired,
  totalPage: React.PropTypes.number.isRequired,
  onChangePage: React.PropTypes.func.isRequired}export default Pager

在这里为了展示方便,所有组件的样式都使用内联样式。不过实际开发中还是推荐使用单独的样式表文件。 另外,在webpack的帮助下,每个组件最好对应一个样式文件,在组件文件中require进来,这样组件就能保持完整的模块化。

作为一个被复用可能性很大的公共组件,强烈建议定义组件的属性类型。另外这个组件要求的属性与接口所返回的数据并不完全一致, 服务返回的是条目总数,而Pager组件要的是总页数,这个转换放到reducer里比较合适。

最后把Pager放到srsc/containers/NewsList.js里面去

import React from 'react';

import { connect } from 'react-redux'

import NewsOverview from 'components/NewsOverview

import Pager from 'components/Pager'

import {fetchList} from 'actions/news'

class NewsList extends React.Component {
  search(){
    let keyword = this.refs.keyInput.value   
 this.props.dispatch(fetchList(keyword))
  }
  renderList(){
    return this.props.list.map(item =>{
      item.key = item.title   
    return React.createElement(NewsOverview, item)
    })
  }
  render(){
    let {page, totalPage, dispatch} = this.props    return (
      <div>
        <div>
          <input ref="keyInput"/>
          <button onClick={this.search.bind(this)}>搜索</button>``
        </div>
        <div>
          {this.renderList()}
        </div>
        <Pager page={page} totalPage={totalPage} 
onChangePage={i=>dispatch(fetchList(null,i))} />
      </div>
    )
  }}function mapStateToProps(state) {
  return Object.assign({}, state.news)}

export default connect(mapStateToProps)(NewsList);

大功告成!

不过还没完。现在我们只有一个新闻列表,如果想看新闻的具体内容呢??点进去看啊。。。

好吧,这就需要一个新的页面了。难道我们再写一个新页面另建一套这堆东西吗?no, no, no。 都什么时代了,我们要做单页应用(spa),给用户最佳的操作体验。要在单页中模拟出来多个页面, 就要用到路由了。下一节,我们就玩一玩react自己的路由系统:react-router。

原文发布于微信公众号 - 交互设计前端开发与后端程序设计(interaction_Designer)

原文发表时间:2017-11-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏ChaMd5安全团队

N1CTF2018 APFS&Lipstick题解

APFS题目描述 Apple released the brand new APFS on WWDC 2017 with a bunch of new feat...

35010
来自专栏小文博客

良心压缩软件Bandizip——无广告超精简

7.8K5
来自专栏NetCore

无尽的忙碌换来幸福的日子

人总是忙碌的,从小要读书,长大了工作,结婚了,有孩子了,一辈子也可能等到孩子成家了才能稍微休息一下下吧,不过有时候想想,忙碌点好,一辈子也就那么长,等闭了后还能...

21510
来自专栏bboysoul

linux 服务器带宽测试脚本ZBench

很郁闷,今天我的vultr服务器的ip被ban了,无奈只能换服务器,今天给大家推荐一个vps的带宽测速脚本ZBench可以一键测试你的服务器到国内和国外的速度

4522
来自专栏林德熙的博客

C# 设计模式 责任链 后退按钮使用责任链

责任链模式是一种对象的行为模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。...

1041
来自专栏源哥的专栏

SaaS行业命名规范

    很多企业在启动软件开发的时候,完成没有命名规范,导致代码的可读性极差。而业界对于命名,却没有一个统一的命名规范,比如说,获取客户列表,Java类的方法是...

1843
来自专栏JadePeng的技术博客

HTML5录音控件

最近的项目又需要用到录音,年前有过调研,再次翻出来使用,这里做一个记录。 HTML5提供了录音支持,因此可以方便使用HTML5来录音,来实现录音、语音识别等功能...

1.5K5
来自专栏SAP最佳业务实践

SAP最佳业务实践:MM–组件收费的委外加工(251)-6开销售发票

4.7 创建出具发票凭证 创建出具发票凭证给委外加工商。 完成了对委外加工商的发货。 SAP ECC菜单Processes -Create Invoices f...

4038
来自专栏杨建荣的学习笔记

dg的奇怪问题终结和分区问题答疑 (r7笔记第77天)

今天来说几个问题,一个是对昨天《让我焦灼的四个问题》的升华,不能起博眼球的题目,技术分析给大家兜底了,你们看看有没有类似的问题。 还有几个小问题说说今天的感受和...

3515
来自专栏施炯的IoT开发专栏

Windows 10 IoT Serials 5 - 如何为树莓派应用程序添加语音识别与交互功能

    都说语音是人机交互的重要手段,虽然个人觉得在大庭广众之下,对着手机发号施令会显得有些尴尬。但是在资源受限的物联网应用场景下(无法外接鼠标键盘显示器),如...

21110

扫码关注云+社区

领取腾讯云代金券