React 之 redux-saga

作者 糖一瓶 日期 2018-04-02
React 之 redux-saga

一、什么是saga?

saga react是官方推出的解决异步问题的库,作用类似于之前写过的thunk。可以参考看一下作用。
手册

二、saga初步了解


首先是了解一下目录结构:
test
|-node_modules
|-www
|-app
|-main.js
|-App.js
|-reducers
|-index.js
|-counterReducer.js
|-actions
|-counter.js
|-index.html
|-webpack.config.js
|-package.json

安装redux-saga

npm install –save dva

创建sagas.js的文件:


目录结构:
test
|-node_modules
|-www
|-app
|-main.js
|-App.js
|-reducers
|-index.js
|-counterReducer.js
|-actions
|-counter.js
|-sagas.js
|-index.html
|-webpack.config.js
|-package.json

这个文件现在向外暴露一个加星函数:

export function* helloSaga() {
    console.log('你好,我是saga');
}
关于加星函数:
ES6中有一个新的特性,叫做产生器,就是加星函数,英语叫做Generator。
function* say(){
    yield "你好";
    yield "哈哈";
    yield "嘻嘻";
    yield "么么哒";
}
var h = say(); //得到产生器的实例
console.log(h.next());       //得到第1个产生的内容。 你好
console.log(h.next());       //得到第2个产生的内容。 哈哈
console.log(h.next());       //得到第3个产生的内容。 嘻嘻
console.log(h.next());       //得到第4个产生的内容。 么么哒

也就是说,一个函数如果加上了*,此时就是一个产生器,里面就要有一条一条的yield语句。
此时我们可以
var h = say();
得到产生器的实例。这个实例就是h.next()逐条调用yield的返回值。
h.next();
h.next();
h.next();
能够有多条return的函数,就是产生器。
yield后面假如是一个返回Promise对象的函数,自动有async、await的能力,自动停留等待。
箭头函数不能加*,必须是function。

改变main.js:

import React from "react";
import ReactDOM from "react-dom";
import {createStore , applyMiddleware} from "redux";
import {Provider} from "react-redux";
// 引入redux-saga
import createSagaMiddleware from "redux-saga";

import App from "./App.js";
import reducer from "./reducers";

//引入默认saga
import { saga } from './sagas.js';

// 创建saga的中间件
const sagaMiddleware = createSagaMiddleware();

// 仓库
const store = createStore(reducer, applyMiddleware(sagaMiddleware));

//运行默认saga
sagaMiddleware.run(saga);

ReactDOM.render(
    <Provider store={store}>
        <App/>
    </Provider>
    ,
    document.getElementById("app")
);

然后补充一个babel的plugin(因为加星函数需要):

npm install –save-dev babel-plugin-transform-runtime

改变webpack.config.js文件:

const path = require('path');

module.exports = {
    entry: "./www/app/main.js", 
    output: {
        path: path.resolve(__dirname, "www/dist"),  
        filename: "bundle.js",
        publicPath: "/xuni/"
    },
    mode : "development",
    module: {
        rules: [
            {
                test: /\.js$/,
                include: [
                    path.resolve(__dirname, "www/app")
                ],
                exclude: [
                    path.resolve(__dirname, "node_modules")
                ],
                loader: "babel-loader",
                options: {
                    presets: ["es2015","react"],
                    plugins: ["transform-object-rest-spread","transform-runtime"] // 这里引入transform-runtime
                }
            }
        ]
    },
    devServer: {
        proxy: {
            //下面的参数从vue-cli上扒拉出来的
            "/api": {
                target: 'http://127.0.0.1:3000/',
                changeOrigin: true,
                pathRewrite: {
                    '^/api': '/'
                }
            }
        }
    }
}

此时输出项目可见:
1

三、异步与拦截

我们现在给按钮“按我加服务器那么多”绑定事件,让这个按钮能够发出一个普通的action。

counter.js:
export const add = () => ({"type" : "ADD"});
export const minus = () => ({"type" : "MINUS"});

注意这个,我将在sagas.js中对这个指令进行拦截
export const addServer = () => ({"type" : "ADDSERVER"});

改变sagas.js文件:

import {all , takeEvery} from "redux-saga/effects"

按照第一二三顺序查看

********************************************************
//第三步:
//worker saga,工人saga。
//这个saga的功能是被拦截之后做的事情。
function* addServer(){
    //做异步
    const {a} = yield fetch("/api/api").then(data=>data.json());
    //put用于发出新的action
    yield put({type : "ADD" , a});

    // if语句
    const { a } = yield select(state => state.counter); //这是获取到当前的state值
    //判断是不是奇数
    if(a % 2 == 1){
        //发出新的action
        yield put({ type: "ADD" });
    }
}
********************************************************
// 第二步:
//watcher saga,监控saga。
//这个saga的名字叫做watchAddServer,暗示了我们它在拦截ADDSERVER这个action,我要在这里进行拦截了
function* watchAddServer(){
    //takeEvery是英语的“拦截”的意思,这里表示拦截所有的ADDSERVER的action,一旦拦截到,做addServer函数。
    // 拦截成功执行addServer就是第三步里的函数
    yield takeEvery('ADDSERVER', addServer);
}
********************************************************
//第一步:
//默认暴露的函数,这个函数被main.js中的run调用了。
export default function* () {
    //all表示“并行执行”的意思,此时现在执行了watchAddServer这个函数。就是第二步里头的函数。
    yield all([watchAddServer()]);
}
  • 什么是saga,一系列有监控、拦截、工作函数组成的“拦截体系”我们称为saga。
  • 小结一下:
    就是在action中我们都以同步的形式发送dispatch 然而这个请求要进行异步操作的话
    我们将在sagas.js中对她进行拦截 拦截成功后发出一个函数,在这个函数里进行异步操作发送新的action
    发送的新的action才是reducer中接收到的
  • 与thunk对比
    在thunk中我们在actions中将异步用()=>()=>{}的形式,而在saga中actions中都是同步形式
    saga中所有的异步都转移到了sagas.js文件中,结构要清晰了很多
  • sagas.js文件,大约有三部分组成:
    默认暴露的:all()
    watcher saga: takeEvery()
    worker saga : fecth() 、put()
  • saga的核心思路是:类似你的项目中的一个单独的线程,单独负责副作用。

三、辅助函数

redux-saga提供了一些“辅助函数”来帮我们监听一些被指定的发往store的action。

  • takeEvery:监听action,执行函数

    yield takeEvery("ADDSERVER" , addServer);
    
  • all:同步运行一些监听

    yield all([watchAddServer() , watchJibianoububian()])
    
  • put:发action的

    yield put({"type" : "ADD" , a})
    
  • call : 调用一个函数

    import { all, takeEvery , put , select , call} from "redux-saga/effects"
    async function loadApi(){
        //做异步
        const { a } = await fetch("/api/api").then(data => data.json());
        return a;
    }
    
    //worker saga,工人saga。
    //这个saga的功能是被拦截之后做的事情。
    function* addServer() {
        //发出新的action
        const a = yield call(loadApi);
        yield put({type : "ADD" , a});
    }
    ……
    ……
    
  • fork:也是调用函数,但是要在调用的函数中加上put,而没有return值。

    import { all, takeEvery , put , select , call , fork} from "redux-saga/effects"
    
    function* loadApi(){
        //做异步
        const { a } =  yield fetch("/api/api").then(data => data.json());
        yield put({"type" : "ADD" , a})
    }
    
    //worker saga,工人saga。
    //这个saga的功能是被拦截之后做的事情。
    function* addServer() {
        //发出新的action
        yield fork(loadApi)
    }
    

四、saga有三个好处

1)saga写异步的地方就是saga文件,而不涉及action文件,action文件中画风都是一个(),都是非常简单的action creator。靠拦截的思路去写saga。
2)副作用清晰,比如做分页项目,改变排序的时候,一定会分页要到第1页,此时saga中用两个yield put()轻松解决。
3)数据的集结感非常强,做条件查询的业务非常适合。saga会集结当前reducer中的查询条件,包括按什么排序、排序方向、页码信息等,集结完毕,出发进行查询,返回的results经过put,当做载荷,改变reducer影响视图。

五、 拆分一下

类似reducers 和 actions 的拆分:
创建sagas文件夹,并创建counterSagas.js


目录结构:
test
|-node_modules
|-www
|-app
|-main.js
|-App.js
|-reducers
|-index.js
|-counterReducer.js
|-actions
|-counter.js
|-sagas.js
|-sagas
|-counterSagas.js
|-index.html
|-webpack.config.js
|-package.json

将all 的部分放在sagas.js中:

import {all} from "redux-saga/effects";
引入conterSagas.js中的watcher saga函数
import {watchAddServer, watchAddServer2, watchJibianoububian} from "./sagas/counterSagas.js";

//默认暴露的函数,这个函数被main.js中的run调用了。
export default function* () {
    //all表示“并行执行”的意思,此时现在执行了watchAddServer这个函数。
    yield all([
        watchAddServer(),
        watchAddServer2(),
        watchJibianoububian()
    ]);
}

sagas/conterSagas.js如下:

import { all, takeEvery, put, select, call, fork } from "redux-saga/effects"


//worker saga,工人saga。
//这个saga的功能是被拦截之后做的事情。
function* addServer() {
    //做异步
    const { a } = yield fetch("/api/api").then(data => data.json());
    yield put({ "type": "ADD", a })
}

function* addServer2() {
    //做异步
    const { a } = yield fetch("/api/api2").then(data => data.json());
    //发出新的action
    yield put({ type: "ADD", a });
}


function* jibianoububian() {
    const { v } = yield select(state => state.counterReducer);
    //判断是不是奇数
    if (v % 2 == 1) {
        //发出新的action
        yield put({ type: "ADD" });
    }
}

//*************************************************************** */
//watcher saga,监控saga。
//这个saga的名字叫做watchAddServer,暗示了我们它在拦截ADDSERVER这个action
export const watchAddServer = function* () {
    //takeEvery是英语的“拦截”的意思,这里表示拦截所有的ADDSERVER的action,一旦拦截到,做addServer函数。
    yield takeEvery('ADDSERVER', addServer);
}

export const watchAddServer2 = function* () {
    //takeEvery是英语的“拦截”的意思,这里表示拦截所有的ADDSERVER2的action,一旦拦截到,做addServer2函数。
    yield takeEvery('ADDSERVER2', addServer2);
}

export const watchJibianoububian = function* () {
    //takeEvery是英语的“拦截”的意思,这里表示拦截所有的JIBIANOUBUBIAN的action,一旦拦截到,做jibianoububian函数。
    yield takeEvery('JIBIANOUBUBIAN', jibianoububian);
}