React中的redux-saga详解


1. 介绍

redux-saga 是 redux 一个中间件,它是基于ES6 的 Generator 功能实现,用于解决异步问题(让redux中可以直接进行异步操作)。

在这里插入图片描述

组件会发送一个 action 对象给 redux-saga,redux-saga(主saga) 就会分析监听 saga 中有没有当前 action 对应的 type 类型操作,如果在监听 saga 中找到了,说明当前操作是一个异步操作,然后就会走下面的异步操作流程,最后 action 会被交给 redux,也就是交给 reducer 完成修改。

如果主 saga 在监听 saga 没有找到对应 type 的实现,则说明当前操作是一个同步操作,就会直接交给 redux。

2. redux-saga安装和在项目中引入配置

安装:

yarn add redux-saga

在项目中引入:

store/index.js:

import {
    
     createStore, applyMiddleware } from 'redux'
import {
    
     composeWithDevTools } from '@redux-devtools/extension'
// 中间件
// import thunk from 'redux-thunk'

// 合并后的reducer
import reducer from './reducer'

// saga中间件
import createSagaMiddleware from 'redux-saga'
import mainSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()

const store = createStore(reducer, composeWithDevTools(applyMiddleware(sagaMiddleware)))

// 运行saga
sagaMiddleware.run(mainSaga)

export default store

sagas.js:

// saga中间件 主saga,用于区别是否需要saga来处理异步操作,如果没有异步,则放行
function* mainSaga() {
    
    

}

// 监听saga,监听type类型为异步操作名称的,此saga会通过主saga分配过来
function* watchSaga() {
    
    

}

// 工作saga,监听saga得到任务后,把任务分配给工作saga
function* workSaga(action) {
    
    

}

// 主saga要对外暴露出去
export default mainSaga

3. redux-saga的使用

用 resux-saga 写一个延时计数器。

App.jsx:

import React from 'react'
import {
    
     useDispatch, useSelector } from 'react-redux'
// useSelector : 读取redux的state的数据
// useDispatch : 修改redux的state的数据

const Login = () => {
    
    
  const dispatch = useDispatch()
  let num = useSelector(state => state.count.num)

  return (
    <div>
      <h3>{
    
    num}</h3>
      <button onClick={
    
    () => dispatch({
    
     type: 'asyncAdd', payload: 10 })}>进入系统</button>
    </div>
  )
}

export default Login

sagas.js:

import {
    
     takeEvery, put } from 'redux-saga/effects'
// put 它是saga提供给我们,用于发送指令给reducer来完成同步操作
// takeEvery 监听每一次dispatch发送的指令

// 延时器
function delay(n = 1) {
    
    
  return new Promise(_ => {
    
    
    setTimeout(() => _(''), 1000 * n)
  })
}

// saga中间件 主saga,用于区别是否需要saga来处理异步操作,如果没有异步,则放行
function* mainSaga() {
    
    
  // 这样的调用,它只能监听一个saga,不能进行模块化
  yield watchSaga()
}

// 监听saga,监听type类型为异步操作名称的,此saga会通过主saga分配过来
function* watchSaga() {
    
    
  yield takeEvery('asyncAdd', workSaga)
}

// 工作saga,监听saga得到任务后,把任务分配给工作saga
// function* workSaga(action) {
    
    
function* workSaga({
     
      payload }) {
    
    
  // 异步请求
  yield delay(2)
  // 发送指令让reducer完成同步操作
  yield put({
    
     type: 'add', payload })
}

// 主saga要对外暴露出去
export default mainSaga

count.js:

const initState = {
    
    
  num: 100
}

const reducer = (state = initState, {
     
      type, payload }) => {
    
    
  if ('add' === type) return {
    
     ...state, num: state.num + payload }
  return state
}

export default reducer

在这里插入图片描述

4. saga模块化拆分

在这一章节中,我们将要实现 saga 的模块化拆分。我们的需求是,点击登录系统之后,可以获取到 uid ,获取到 uid 之后可以跳转到后台首页。

首先我们需要 mock 一下后台的用户数据:

user.js:

module.exports = app => {
    
    
  // 用户登录
  app.post('/api/login', (req, res) => {
    
    
    let bufferData = []
    // 如果你要在mock时想要接受post数据
    req.on('data', chunk => bufferData.push(chunk))
    req.on('end', () => {
    
    
      // username=xxx&password=xx
      let postString = Buffer.concat(bufferData).toString('utf-8')

      res.send({
    
    
        code: 0,
        msg: 'ok',
        data: {
    
    
          uid: 2000,
          token: 'fwe;fjewlfjlwfjlefewlfelffewlfewjfe',
          nickname: '张英',
        }
      })
    })
  })
}

然后是 api 接口的书写:

userApi.js:

import {
    
     post } from '@/utils/http'

// 用户登录
export const loginApi = userData => post('/api/login', userData)

然后是 redux 中的 reducer 函数:

user.js:

const initState = {
    
    
  uid: 0,
  token: '',
  nickname: ''
}

const reducer = (state = initState, {
     
      type, payload }) => {
    
    
  // reducer函数处理 userlogin 类型的 action
  if ('userLogin' === type) return {
    
     ...state, ...payload }
  return state
}

export default reducer

接下来是主 saga 文件:

sagas.js:

import {
    
     all } from 'redux-saga/effects'
// all方法,可以监听多个监听saga,它的功能和Promise.all方法一样

import userWatchSaga from './watchsagas/userSaga'

// saga中间件 主saga,用于区别是否需要saga来处理异步操作,如果没有异步,则放行
function* mainSaga() {
    
    
  yield all([
    // 监听 saga 中有 userWatchSaga 操作,所以会拦截这个 action
    userWatchSaga()
  ])
}

export default mainSaga

监听 saga :
userSaga.js:

import {
    
     put, takeEvery, call } from 'redux-saga/effects'
// put 它是saga提供给我们,用于发送指令给reducer来完成同步操作
// takeEvery 监听每一次dispatch发送的指令

// call方法,调用Promise对象
//  引入网络请求方法
import {
    
     loginApi } from '@/api/userApi'

export default function* watchSaga() {
    
    
  yield takeEvery('asyncLogin', login)
}

// 在此处完成网络请求就可以了
// generator的返回值,不是普通函数这样的返回值,这样在登录成功后,无法让前端的组件完成路由的切换,
// 切换的原则是登录成功后,才能能跳转,登录的过程它是一个异步的,所以此时工作就有点难受,所以需要用到库
function* login({
     
      payload }) {
    
    
  // call内部实现了 co 方法,可以将自己的返回值返回给 ret
  let ret = yield call(loginApi, payload)
  // 得到的数据同步到redux中
  if (ret.code === 0) {
    
    
    // 通过reducer完成redux中的数据更新  登录成功
    yield put({
    
     type: 'userLogin', payload: ret.data })
  }
}

最后是我们的前台页面:

import React, {
    
     useEffect } from 'react'
// react-redux提供的hook工具函数
import {
    
     useDispatch, useSelector } from 'react-redux'
// useSelector : 读取redux的state的数据
// useDispatch : 修改redux的state的数据

const Login = ({
     
     history}) => {
    
    
  const dispatch = useDispatch()
  let num = useSelector(state => state.count.num)
  let uid = useSelector(state => state.user.uid)

  // hack处理方案,完成登录成功后,路由跳转
  // 只要 uid 发生改变(由零变为2000),这个函数就被触发
  useEffect(() => {
    
    
    // uid初始值为0,只要你登录成功,则一定会大于0,表示登录成功,跳转到后台
    if (uid > 0) history.push('/')
  }, [uid])

  const doLogin = () => {
    
    
    // 进行登录,它是一个异步的,交给saga,saga会完成异步操作,通知reducer完成同步修改redux中的state数据改变
    // reducer把state中的数据修改后,因为我在当前的组件中有通过useEffect来依赖此state中的值的变化,所以它只要变化了,我就可以来跳转,从而可以确认redux中的数据一定是存在后才跳转的
    dispatch({
    
     type: 'asyncLogin', payload: {
    
     username: 'admin', password: 'admin888' } })
  }

  return (
    <div>
      <h3>
        {
    
    num} -- {
    
    uid}
      </h3>
      <button onClick={
    
    doLogin}>进入系统</button>
    </div>
  )
}

export default Login

在这里插入图片描述

5. connected-react-router

描述:

此库可以让redux中完成路由跳转相关的功能。

安装:yarn add connected-react-router

使用步骤:

  1. 在 src 目录下创建 history.js 文件,并书写如下代码:

    // history模块它是react-router-dom安装成功后就存在的,无需手动再安装
    import {
          
           createBrowserHistory, createHashHistory } from 'history'
    const history = createBrowserHistory()
    // 告知当前路由的模式为 history模式
    export default history
    
  2. 在入口文件中把原来的react-router-dom中定义路由模式组件更换:

    import React from 'react'
    import ReactDOM from 'react-dom'
    
    // 路由
    // import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
    // 使用connected-react-router库,就需要把原来的路由模式组件进行更换
    import {
          
           ConnectedRouter as Router } from 'connected-react-router'
    import history from './history'
    
    // redux
    import {
          
           Provider } from 'react-redux'
    import store from './store'
    
    import App from './App'
    
    // 后台首页
    
    ReactDOM.render(
      <Provider store={
          
          store}>
        <Router history={
          
          history}>
          <App />
        </Router>
      </Provider>,
      document.getElementById('root')
    )
    
  3. 在 reducer 模块中,定义一个 router 的模块

    import {
          
           combineReducers } from 'redux'
    import {
          
           connectRouter } from 'connected-react-router'
    import history from '@/history'
    
    import user from './user'
    import count from './count'
    
    export default combineReducers({
          
          
      // 添加一个 router 的模块
      router: connectRouter(history),
      user,
      count
    })
    
  4. 在redux入口文件中,以中间件的方式把connected-react-router包含到redux中

    import {
          
           createStore, applyMiddleware } from 'redux'
    import {
          
           composeWithDevTools } from '@redux-devtools/extension'
    // 中间件
    // import thunk from 'redux-thunk'
    
    // 合并后的reducer
    import reducer from './reducer'
    
    // saga中间件
    import createSagaMiddleware from 'redux-saga'
    import mainSaga from './sagas'
    
    // redux中路由
    import {
          
           routerMiddleware } from 'connected-react-router'
    import history from '@/history'
    
    const sagaMiddleware = createSagaMiddleware()
    
    // 在redux入口文件中,以中间件的方式把connected-react-router包含到redux中
    const store = createStore(reducer, composeWithDevTools(applyMiddleware(routerMiddleware(history), sagaMiddleware)))
    
    // 运行saga
    sagaMiddleware.run(mainSaga)
    
    export default store
    
  5. 在 redux 中间件中就可以完成路由跳转:

    userSaga.js:

    import {
          
           put, takeEvery, call } from 'redux-saga/effects'
    // put 它是saga提供给我们,用于发送指令给reducer来完成同步操作
    // takeEvery 监听每一次dispatch发送的指令
    
    // 通过库,可以完成在redux中实现跳转跳转功能
    import {
          
           push, replace, goBack } from 'connected-react-router'
    // call方法,调用Promise对象
    //  引入网络请求方法
    import {
          
           loginApi } from '@/api/userApi'
    
    export default function* watchSaga() {
          
          
      yield takeEvery('asyncLogin', login)
    }
    
    // 在此处完成网络请求就可以了
    // generator的返回值,不是普通函数这样的返回值,这样在登录成功后,无法让前端的组件完成路由的切换,
    // 切换的原则是登录成功后,才能能跳转,登录的过程它是一个异步的,所以此时工作就有点难受,所以需要用到库
    function* login({
           
            payload }) {
          
          
      // call内部实现了 co 方法,可以将自己的返回值返回给 ret
      let ret = yield call(loginApi, payload)
      // 得到的数据同步到redux中
      if (ret.code === 0) {
          
          
        // 通过reducer完成redux中的数据更新  登录成功
        yield put({
          
           type: 'userLogin', payload: ret.data })
        // 跳转到后台首页 -- 在redux中间件中就可以完成路由的跳转
        yield put(push('/'))
      }
    }
    
  6. 在前台页面不用使用 hack 方式,而是使用当前库实现路由跳转:

    import React from 'react'
    // react-redux提供的hook工具函数
    import {
          
           useDispatch, useSelector } from 'react-redux'
    
    const Login = ({
           
            history }) => {
          
          
      const dispatch = useDispatch()
      let num = useSelector(state => state.count.num)
    
      const doLogin = () => {
          
          
        // 进行登录,它是一个异步的,交给saga,saga会完成异步操作,通知reducer完成同步修改redux中的state数据改变
        // reducer把state中的数据修改后,因为我在当前的组件中有通过useEffect来依赖此state中的值的变化,所以它只要变化了,我就可以来跳转,从而可以确认redux中的数据一定是存在后才跳转的
        dispatch({
          
           type: 'asyncLogin', payload: {
          
           username: 'admin', password: 'admin888' } })
      }
    
      return (
        <div>
          <h3>
            {
          
          num}
          </h3>
          <button onClick={
          
          doLogin}>进入系统</button>
        </div>
      )
    }
    
    export default Login
    

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_45605541/article/details/127328192