把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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...