把react什麼的都用起來 【2】非同步action和redux中間件

来源:http://www.cnblogs.com/tolg/archive/2016/03/17/5289697.html
-Advertisement-
Play Games

現代web頁面里到處都是ajax,所以處理好非同步的代碼非常重要。 這次我重新選了個適合展示非同步處理的應用場景——搜索新聞列表。由於有現成的介面,我們就不用自己搭服務了。 我在網上隨便搜到了一個新聞服務介面,支持jsonp,就用它吧。 一開始,咱們仍然按照action->reducer->compon


現代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/stres/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);

大功告成!

不過還沒完。現在我們只有一個新聞列表,如果想看新聞的具體內容呢?

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本節教程將繼續帶領大家完善教學demo 將要學習的demo效果圖如下所示 1. 如何導入完整項目 本節示例demo請參考下載地址,可以導入到設計器中學習。 2. 完善主框架在上一節教程搭建主框架中大家已經學會瞭如何主框架,本節教程使用上一節未完成的demo。 我們分析一下demo機構,通過點擊Bot
  • 相信每一個前端工程師都或多或少遇上過“亂碼”這位仁兄,無論你的基礎有多麼扎實,在生產的過程中都免不了偶爾和“亂碼”兄弟喝上幾杯茶吧。作為一個前端工程師,你是如何指定一個頁面的編碼的呢?你知道瀏覽器是怎麼識別編碼的嗎? 首先,一個很簡單的例子,用遇簡的HTML頁面來看看各瀏覽器下有什麼不同: 最簡HT
  • 大家先看一段簡單的jquery ajax 返回值的js 代碼 function getReturnAjax{ $.ajax({ type:"POST", http://www.cnblogs.com/wlmemail/admin/%22ajax/userexist.aspx", data:"user
  • JS and Jquery 都能獲取頁面元素的寬度,高度和相對位移等數值,那他們之間能相互轉換或替代嗎,寫法又有哪些差異呢?本文將詳細為你介紹。 1.Js獲取瀏覽器高度和寬度document.documentElement.clientWidth ==> 瀏覽器可見區域寬度 document.doc
  • 瀏覽器可以被認為是使用最廣泛的軟體,本文將介紹瀏覽器的工作原理,我們將看到,從你在地址欄輸入google.com到你看到google主頁過程中都發生了什麼。 將討論的瀏覽器 今天,有五種主流瀏覽器——IE、Firefox、Safari、Chrome及Opera。 本文將基於一些開源瀏覽器的例子——F
  • 試玩:http://hovertree.com/game/9/ 貪吃蛇是一種風靡全球的小游戲,就是一條小蛇,不停地在屏幕上游走,吃各個方向出現的蛋,越吃越長。只要蛇頭碰到屏幕四周,或者碰到自己的身子,小蛇就立即斃命。和別的游戲不同,貪食蛇是一個悲劇性的游戲。許多電子游戲都是打通關到底,游戲操作者以勝
  • 一個web前端的小白,聽前輩說寫好筆記很關鍵,so 特此用博客來開始記錄自己的旅程——Web之路 最近幾天看的HTML 1、糾正一個認知錯誤 “HTML是一種編程語言” ————(錯) HTML (全名:Hype Text Mark-up Language ) 超文本標記語言 ,它是一種描述性標記語
  • 模塊化:每個模塊只完成一個獨立的功能,然後提供該功能的介面。模塊間通過介面訪問。模塊中的(過程和數據)對於其它模塊來說是私有的(不能訪問修改) 原始人寫法: 這種寫法已經有點模塊的樣子了,一下就能看出這幾個函數和變數之間的聯繫。 缺點在於所有變數都必須聲明為公有,所以都要加this指示作用域以引用這
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...