原文请见.
saga其实也是一种设计模式, 他的作用是wrapper包装器的原理,现在我手头没有javascirpt设计模式的书,不知道有没有这个模式.简单说Saga就是把逻辑进行了打包,
这样在书写逻辑关系的时候就相对比较容易了
.所有有关的逻辑卸载一起,符合组件分离,集中控制的原理.(我们的电脑就是这么搞的, 外界设备很多,但是控制权在操作系统上,cpu其实就是我们的saga,用来处理操作流程的)
开始
几天前我的同事做了一个关于异步操作管理的讲座.他正在使用几个工具来扩展Redux的功能.听了他的报告,简直让我产生了javascirpt 疲劳.
让我们面对事实:如果你习惯于基于需求来使用技术完成工作-并不是从技术本身来考虑问题的话-构建React 生态系统将会是非常令人沮丧和花费时间的.(译注:高屋建瓴的话,细细体会:意思别光顾眼前的项目,要有长远的开发眼光).
我已经在Angular 项目上花费了两年时间,非常的喜欢MVC模型对于state控制的能力.但是我要说即使我已经有了Backbone.js(译注:另一个前端MVC框架)
的背景,但是Angular.js的学习曲线仍然很有挑战性.我因此获得了不错的工作,因此我也有机会在有关的项目上使用它。我从Angular的舍去学习到了很多的东西.
还挺酷的,但是Fatigue还是在继续(译注:Angular的水很深啊,吃不透的意思)
所以我迁移到了时髦的框架上:React,Redux,Sagas.
几年以前,我遇到一篇文章,是关于扁平promise对象链的文章.我从这篇文章上学到了很多东西。甚至是两年以后,我仍然能回想起文章里的真知灼见.
申明:我将乎继续使用一样的场景,并扩展他.我希望能创建相同方法的讨论.我会假设读者已经对于Promise对象,React,Redux有了基础的了解,当然还有JavaScript(基础的基础).
首先要做的事情
根据Redux-saga的创建者Yassine Flouafi的想法:
redux-saga是一个在React/Redux应用中,针对性解决异步操作的库.
基本上,他是一个助手库,基于Sagas和ES6的Generators函数来进行组织异步和分发操作.如果你想进了解Saga的模式,可以参见Caitie McCattrey的视频.
航班展示案例
场景是这样的
航班展示示意图
如上面的图所示,三个API的调用时一个承前启后的过程,getDeparture->getFlight->getForecast,所以我们的API Serivces类看起来是这样的:
class TravelServiceApi {//旅行服务API
static getUser() { //首先异步获取用户信息
return new Promise((resolve) => {
setTimeout(() => {
resolve({//模拟返回的数据
email : "somemockemail@email.com",
repository: "http://github.com/username"
});
}, 3000);
});
}
static getDeparture(user) {//获取用户航班信息
return new Promise((resolve) => {
setTimeout(() => {
resolve({//模拟返回的数据
userID : user.email,
flightID : “AR1973”,
date : “10/27/2016 16:00PM”
});
}, 2500);
});
}
static getForecast(date) {//获取天气情况
return new Promise((resolve) => {
setTimeout(() => {
resolve({ //模拟返回的数据
date: date,
forecast: "rain"
});
}, 2000);
});
}
}
这个API非常的直接,允许我们使用假数据来设定场景.首先我们的需要一个用户,然后根据用户的的信息,来获取用户的航班信息,我们得到离港信息,航班信息和天气信息,据此我们可以创建几个丑陋的仪表盘
仪表盘示意图使用的React组件可以看这里.这是三个不同过的组件,数据来源是redux store中的三个reducer.类似下面的样子:
const dashboard = (state = {}, action) => {
switch(action.type) {
case ‘FETCH_DASHBOARD_SUCCESS’:
return Object.assign({}, state, action.payload);
default :
return state;
}
};
因为有三个不同的场景,我们为每一个面板使用一个不同的reducer,这么做就可以让组件从Redux的StateProps函数获取到需要的部分:
const mapStateToProps =(state) => ({
user : state.user,
dashboard : state.dashboard
});
每个步骤都配置好以后(我很清楚,很多的细节问题都还没有解释,但是我想集中注意在sagas上),准备开始运行了.
展现我们的Sagas
William Deming 曾经说过:
如果你不能描述出你所做事情的步骤,那么你就不知道你到底在做什么
(译注:process或者flow对于Redux的学习很重要,初学者总是不能把分散的一些部分连城一个整体去考虑).
那么,我们就来一步一步的使用Redux Saga创建我们的工作流程.
1.注册Sagas
我将会使用我自己的表达方式来描述API 暴露出来的方法.如果你需要更多的技术细节,参考文档
首先我们需要创建saga 生成器函数(译注:generator,这算是坎吧,要先理解ES6的内容),并且注册一下.
function* rootSaga() {
yield[
fork(loadUser),
takeLatest('LOAD_DASHBOARD', loadDashboardSequenced)
];
}
Redux saga暴露出了几个方法,这几个方法被成为effects
,我们将会定义几个effects:
-
Fork 非阻塞执行传递进来的函数(译注:这算是有一个坎儿,javascript的函数是一类对象,可以作为函数的参数来传递,初学者很难理解这个问题)
-
Take 暂停执行直达收到匹配的action
-
Race 同时运行effects,然后其中之一完成了,就会立即退出,其他的effects也就会终止
-
Call 执行一个函数,如果他返回一个promise对象(译注:异步操作的又一个坎儿),saga就会在这里终止,知道promise返回 resolved
-
put dispatch一个动作
-
Select 运行selector函数从state获取数据
-
takeLatest 意思是我们将会执行一个操作,操作只会返回最新的一个滴啊用的结果.如果我们出发几个cases,将会忽略所有的操作,除了最后一个.
-
takeEvery将会返回每个调用的结果
我们将会注册两个不同的sagas.稍后会定义他们.目前我们为了user信息使用了fork和takeLeatest,他们会等待直到”LOAD_DASBOARD“的调用后,才会执行.
2.把Saga中间件注入到Redux的store中
当我们定义了Redux store并初始化以后,大多数情况下看起来像这样:
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, [], compose(
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga); /* inject our sagas into the middleware*/
3.创建Sagas
首先,我们将定义loadUser的流程 saga:
function* loadUser() {
try {
//1st step
const user = yield call(getUser);
//2nd step
yield put({type: 'FETCH_USER_SUCCESS', payload: user});
} catch(error) {
yield put({type: 'FETCH_FAILED', error});
}
}
我按照下面的方式解读一下:
- 首先调用getUser函数,返回的结果赋值给user常量
- 之后,dispatch一个叫做FETCH_USER_SUCCESS的action,user传递给store去处理.
- 如果操作中出问题了,dispatch一个FETCH_FAILED的 action
如你所见的,的确是非常的酷,我们把yield操作的结果赋值给了一个变量
接着来创建saga序列
function* loadDashboardSequenced() {
try {
yield take(‘FETCH_USER_SUCCESS’);
const user = yield select(state => state.user);
const departure = yield call(loadDeparture, user);
const flight = yield call(loadFlight, departure.flightID);
const forecast = yield call(loadForecast, departure.date);
yield put({type: ‘FETCH_DASHBOARD_SUCCESS’, payload: {forecast, flight, departure} });
} catch(error) {
yield put({type: ‘FETCH_FAILED’, error: error.message});
}
}
按照下面的步骤来解读:
-
等待
FETCH_USER_SUCCESS
action的被派发,这个操作的基础是基于一个事件被暂定直到触发为止.我们使用take effect来实施这个过程 -
我们从store中获取一个值.select effects接收一个函数可以接入到store.我们把用户信息赋值给user常量
-
执行一个异步操作来加载depature信息,使用call effect ,user常量作为参数
-
loadDeparture 完成以后,我们执行 loadFlight,参数是前一个操作异步获取的departure对象.
-
forecast的操作是一样的,我们需要等待航班信息加载完成以后才可以执行下一个call effect
-
最后,当所有的操作都完成以后,使用put effect来分发一个action,把所有获取的信息发送到Redux的store.
正如你看到的,一个saga就是一系列等待前一个action来修改他们行为的集合体(译注:集合中每个步骤都会等待前一个步骤的操作结果).一旦完成整个流程,所有的信息就可以提供给Redux的store来处理了.
是不是相当的整洁啊!
接下来,我们看看一个不同的示例.考虑一下 getFlight 和 getForecast 同时触发,他们不需要一个等另一个完成了再执行下一个.
所以我们可以创建一个不同的面板
非阻塞Saga
为了执行两个非阻塞操作,需要对之前的saga做一点改动:
function* loadDashboardNonSequenced() {
try {
//Wait for the user to be loaded
yield take('FETCH_USER_SUCCESS');
//Take the user info from the store
const user = yield select(getUserFromState);
//Get Departure information
const departure = yield call(loadDeparture, user);
//Here is when the magic happens
const [flight, forecast] = yield [call(loadFlight, departure.flightID), call(loadForecast, departure.date)];
//Tell the store we are ready to be displayed
yield put({type: 'FETCH_DASHBOARD2_SUCCESS', payload: {departure, flight, forecast}});
} catch(error) {
yield put({type: 'FETCH_FAILED', error: error.message});
}
}
我们把yield注册为一个数组:
const [flight, forecast] = yield [call(loadFlight, departure.flightID), call(loadForecast, departure.date)];
因此,两个操作会同时被调用(并行),但是在在最后们会等待两个操作都返回结果以后,如果有需求再更新UI
接着我们在rootSaga中注册saga
function* rootSaga() {
yield[
fork(loadUser),
takeLatest('LOAD_DASHBOARD', loadDashboardSequenced),
takeLatest('LOAD_DASHBOARD2' loadDashboardNonSequenced)
];
}
如果操作一旦完成就需要更新UI,应该怎么办?
别担心,我们往回看一下
非序列和非阻塞Sagas
我们可以隔离我们的saga,也可以合并他们,意思是saga可以独立的工作.这就是我们需要的操作.看看操作步骤
step #1 把Forecast和Flight Saga隔离开,这两个Saga都依赖departure的操作
/* **************Flight Saga************** */
function* isolatedFlight() {
try {
/* departure will take the value of the object passed by the put*/
const departure = yield take('FETCH_DEPARTURE3_SUCCESS');
const flight = yield call(loadFlight, departure.flightID);
yield put({type: 'FETCH_DASHBOARD3_SUCCESS', payload: {flight}});
} catch (error) {
yield put({type: 'FETCH_FAILED', error: error.message});
}
}
/* **************Forecast Saga************** */
function* isolatedForecast() {
try {
/* departure will take the value of the object passed by the put*/
const departure = yield take('FETCH_DEPARTURE3_SUCCESS');
const forecast = yield call(loadForecast, departure.date);
yield put({type: 'FETCH_DASHBOARD3_SUCCESS', payload: { forecast, }});
} catch(error) {
yield put({type: 'FETCH_FAILED', error: error.message});
}
}
有什么要注意的吗?这是我们的构建过程
- 两个saga都等待同一个 Action 事件(FETCH_DEPARTURE3_SUCCESS)的结果,来执行后续的工作.
- 当这个事件被触发的时候,他们会收到一个值,这个问题的细节,下一步会讲.
- 两个saga使用call effect来执行异步操作,异步操作完成以后,他们会触发同一个事件.但是发送到store的数据是不同的.感谢Redux的巨大威力,我们这样操作,但是不会对reducer做任何修改.
step #2 对departure序列做出改动,发送一个departure值到两个其他的saga:
function* loadDashboardNonSequencedNonBlocking() {
try {
//Wait for the action to start
yield take('FETCH_USER_SUCCESS');
//Take the user info from the store
const user = yield select(getUserFromState);
//Get Departure information
const departure = yield call(loadDeparture, user);
//Update the store so the UI get updated
yield put({type: 'FETCH_DASHBOARD3_SUCCESS', payload: { departure, }});
//trigger actions for Forecast and Flight to start...
//We can pass and object into the put statement
yield put({type: 'FETCH_DEPARTURE3_SUCCESS', departure});
} catch(error) {
yield put({type: 'FETCH_FAILED', error: error.message});
}
}
之前的代码没有变化,put effect这里有改变.我们可以给一个action传递一个对象,它将会把yield的结果赋值给一个departure常量,departure saga和flight saga都这样操作.
看看demo,注意一下第三个面板加载的foreacast要比flight快一点,因为flight的延时长了一点,模拟了慢速的请求过程.
在实际生产的app中,处理的过程可能有一点不一样.我只是想说明在使用put effect的时候怎么传递值.
关于测试?
你会测试你的代码,对不?
Saga很容易测试,因为saga耦合了操作步骤,根据生成器函数的逻辑,操作步骤被序列化了.让我们看看实例:
describe('Sequenced Saga', () => {
const saga = loadDashboardSequenced();
let output = null;
it('should take fetch users success', () => {
output = saga.next().value;
let expected = take('FETCH_USER_SUCCESS');
expect(output).toEqual(expected);
});
it('should select the state from store', () => {
output = saga.next().value;
let expected = select(getUserFromState);
expect(output).toEqual(expected);
});
it('should call LoadDeparture with the user obj', (done) => {
output = saga.next(user).value;
let expected = call(loadDeparture, user);
done();
expect(output).toEqual(expected);
});
it('should Load the flight with the flightId', (done) => {
let output = saga.next(departure).value;
let expected = call(loadFlight, departure.flightID);
done();
expect(output).toEqual(expected);
});
it('should load the forecast with the departure date', (done) => {
output = saga.next(flight).value;
let expected = call(loadForecast, departure.date);
done();
expect(output).toEqual(expected);
});
it('should put Fetch dashboard success', (done) => {
output = saga.next(forecast, departure, flight ).value;
let expected = put({type: 'FETCH_DASHBOARD_SUCCESS', payload: {forecast, flight, departure}});
const finished = saga.next().done;
done();
expect(finished).toEqual(true);
expect(output).toEqual(expected);
});
});
- 确保导入所有需要测试的effect和助手函数
- 当你在一个yield中存储一个值时,需要传递一个mock 数据 到下一个函数.注意看看第三个,第四个和第五个测试
- 在测试的幕后,当下一个方法被调用以后,yield完成以后,就会移动到下一步.这就是为什么我们使用saga.next().value的原因.
- 序列会被固话,如果你改变saga的步骤,测试就不会通过了.
结论
我非常喜欢测试新技术.每天都会返现新东西.这就像时装:一旦一些事情被公众接受,好像每个人都想使用它.有时候我会在这些事情中发现一些价值(译注:意思是别人说好,你接受了,也可以获得很多有意义的东西,但是不是全部),但是坐下来考虑一下我们正真需要什么是非常重要的.
我发现thunks更容易实现和维护,但是对于复杂的操作,Redux-saga能做的更好.
再次声明,感谢 Thomas 为这篇文章提供的灵感.我希望其他人也可以从我的文章中激发一些灵感:)
如果你阅读中有问题,可以tweet我.我乐意提供帮助.
翻译结束. 好文章.知乎好像有也有一篇翻译稿,才看到.如果有兴趣可以参考一下
网友评论