by Andrés Mijares

由AndrésMijares

使用Redux-Saga进行异步操作 (Async operations using redux-saga)

UPDATE August/2017:

2017年8月更新:

  • I published a second part of this article, Redux-saga common patterns

    我发布了本文的第二部分, Redux-saga通用模式

UPDATE April/2017:

2017年4月更新:

  • Thanks Eduardo for translating this article to Portuguese, find it here.

    感谢Eduardo将本文翻译成葡萄牙语,请 在此处找到 。

  • Also this unknown guy for translating this article to Chinese, find it here.

    也是这个将本文翻译成中文的不知名的人 在这里找到它 。

A few days ago my colleague gave a talk about managing async operations. He was using several tools to extend the capabilities of redux. Listening to him really drove home the realities of JavaScript Fatigue.

几天前,我的同事发表了关于管理异步操作的演讲。 他正在使用多种工具来扩展redux的功能。 听他的话确实使JavaScript疲劳的现实荡然无存 。

Let’s face it: if you’re used to doing your job and using technologies based on your needs — and not for the sake of technology itself — setting up a React ecosystem can prove frustrating and time consuming.

让我们面对现实:如果您习惯于根据自己的需要完成工作并使用技术,而不是仅仅出于技术本身,那么建立一个React生态系统可能会令人沮丧且耗时。

I’ve spent the last two years working on Angular projects and enjoying the Model-View-Controller state of the art. And I must say that — even if the learning curve was an issue coming from a Backbone.js background — learning Angular has really paid off. I got a better job, and I also had the chance to collaborate on interesting projects. I learned a lot from Angular’s supportive community.

最近两年来,我一直在Angular项目中工作,并享受着最先进的Model-View-Controller。 我必须说-即使学习曲线是Backbone.js背景带来的问题,学习Angular确实能带来回报。 我做得更好,也有机会在有趣的项目上进行合作。 我从Angular的支持社区中学到了很多东西。

Those were really cool days, but, well, The Fatigue Must Go On (trademark pending), and I’m moving on with the fashion: React, Redux, and Sagas.

那真是很酷的日子,但是,好吧, Fatigue Must Go On (商标待定),我正在继续流行:React,Redux和Sagas。

A few years ago, I came across with an article titled Flattening Promise Chains by Thomas Burleson. I learned a lot from reading it. Even two years later, I still recall a lot of these insights.

几年前,我遇到了Thomas Burleson撰写的名为Flattening Promise Chains的文章。 我从阅读中学到了很多东西。 甚至两年后,我仍然记得很多这些见解。

These days I’ve been migrating to React and I’ve found lot of power in Redux and using sagas to manage async operations. So I am writing this to borrow from Thomas’ post and create a similar approach using redux-saga. Here’s hoping this returns the favor to the universe and helps some folks understand how these important technologies work.

这些天来,我一直在迁移到React,在Redux中发现了很多功能,并使用sagas来管理异步操作。 因此,我写这篇文章是为了借鉴Thomas的文章,并使用redux-saga创建类似的方法。 希望这能使对宇宙的青睐返回并帮助一些人了解这些重要技术的工作原理。

Disclaimer: I will work with the same scenario and extend it, I hope (if i’m lucky) to create a discussion about both approaches. I will assume the reader has some basic understanding of Promises, React, Redux and (d’oh!)... JavaScript.

免责声明:我将在相同的情况下进行工作并将其扩展,我希望(如果幸运的话)就两种方法进行讨论。 我将假设读者对Promises,React,Redux和(d'oh!)... JavaScript有一些基本的了解。

首先是第一件事。 (First things first.)

According to Yassine Elouafi, creator of the redux-saga:

根据redux-saga的创建者Yassine Elouafi的说法 :

redux-saga is a library that aims to make side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) in React/Redux applications easier and better.

redux-saga是一个库,旨在使React / Redux应用程序中的副作用(例如,诸如数据获取之类的异步事物和诸如访问浏览器缓存之类的不纯之物)更加容易和更好。

Basically a helper library which allows us to organize all the asynchronous and distributed operations based on Sagas and ES6 Function Generators. If you want to know more about the Saga pattern itself, Caitie McCaffrey made a great job in this video and more about Functions Generators. Check this free Egghead video (at least it was free when I posted this article).

基本上是一个帮助程序库,它使我们能够基于Sagas和ES6函数发生器来组织所有异步和分布式操作。 如果您想进一步了解Saga模式本身, 那么Caitie McCaffrey在本视频中以及在有关函数发生器方面都做得很好。 观看此免费的Egghead 视频 (至少在我发布本文时是免费的)。

飞行仪表板案例 (The Flight Dashboard Case)

Thomas set a case that we are going to recreate. The final code is here, and the demo is here.

托马斯提出了一个我们要重新创建的案例。 最终代码在这里 ,演示在这里 。

The scenario goes like this:

场景如下:

As we can see, a sequence of three APIs call: getDeparture -> getFlight ->getForecast, so our API service class looks like this:

如我们所见,三个API调用序列:getDeparture-> getFlight-> getForecast,因此我们的API服务类如下所示:

class TravelServiceApi {
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);   });  }
}

This is a straight-forward API with some mocked information that will allow us to set the scenario. First we need to have a user. Then with that information, we’ll get the departure, the flight, and the forecast so we can create several ugly dashboards panels, which look like this:

这是一个简单明了的API,其中包含一些模拟信息,可让我们设置方案。 首先,我们需要一个用户。 然后,利用这些信息,我们将获得出发,飞行和天气预报,因此我们可以创建几个丑陋的仪表板面板,如下所示:

The React components can be found here. They’re three different components with a representation on the redux store given by three reducers, which look like this:

React组件可以在这里找到。 它们是三个不同的组件,由三个reducer在redux存储上表示,如下所示:

const dashboard = (state = {}, action) => { switch(action.type) {  case ‘FETCH_DASHBOARD_SUCCESS’:  return Object.assign({}, state, action.payload);  default :  return state; }};

We use a different reducer for each panel, with three different scenarios, which give the component access to the piece of the user using the StateToProps redux function:

我们为每个面板使用不同的reducer,并具有三种不同的方案,它们使用StateToProps redux函数使组件可以访问用户:

const mapStateToProps =(state) => ({ user : state.user, dashboard : state.dashboard});

After everything is setup (yeah I know I did not explain a lot of stuff, but I want to focus only on the sagas…) we are ready to play!

一切就绪后(是的,我知道我没有解释很多东西,但是我只想专注于sagas ...),我们已经准备好玩了!

告诉我萨加斯 (Show me the Sagas)

William Deming said once:

威廉·戴明曾经说过:

If you can’t describe what you are doing as a process, then you don’t know what you are doing.

如果您无法描述自己在做什么,那么您就不知道自己在做什么。

Ok, let’s create a step by step process of how to work with Redux Saga.

好的,让我们逐步创建如何与Redux Saga一起使用的过程。

1.注册Sagas (1. Register the Sagas)

I will use my own word to describe which method are exposed by the API. if you need more technical detail, feel free to refer to the documentation here.

我将用自己的话描述API公开的方法。 如果您需要更多技术细节,请随时参考此处的文档。

First we need to create our saga generator and register them:

首先,我们需要创建我们的传奇生成器并注册它们:

function* rootSaga() {  yield[    fork(loadUser),    takeLatest('LOAD_DASHBOARD', loadDashboardSequenced)  ];}

Redux saga expose several methods called Effects, we are going to define several of them:

Redux saga公开了几种称为Effects的方法我们将定义其中的几种方法:

  • Fork performs a non-blocking operation on the function passed.

    Fork对传递的功能执行非阻塞操作。

  • Take pauses until action received.

    暂停一下,直到收到动作为止。

  • Race runs effects simultaneously, then cancels them all once one finishes.

    比赛同时运行效果,然后在完成后将其全部取消。

  • Call runs a function. If it returns a promise, pauses the saga until the promise is resolved.

    调用运行函数。 如果它返回了一个承诺,则暂停传奇直到该承诺得到解决。

  • Put dispatches an action.

    放置调度动作。

  • Select Runs a selector function to get data from the state

    选择运行选择器功能以从状态获取数据

  • takeLatest means we are going to execute the operations, then return only the results of the last one call. If we trigger several cases, it’s going to ignore all of them except the last one.

    takeLatest表示我们将执行操作,然后仅返回最后一个调用的结果。 如果我们触发几种情况,它将忽略除最后一种情况以外的所有情况。

  • takeEvery will return results for all the calls triggered.

    takeEvery将返回所有已触发呼叫的结果。

We just registered two different sagas. We are going to define them later. So far, we take one for the user using fork and another takeLatest, which is going to wait for an action called “LOAD_DASHBOARD” to be executed. More info in step 3.

我们刚刚注册了两个不同的Sagas。 我们将在以后定义它们。 到目前为止,我们使用fork为用户提供了一个,而另一个takeLatest为用户提供了一个它将等待名为“ LOAD_DASHBOARD ”的操作被执行。 步骤3中的更多信息。

2.将Saga中间件注入Redux存储。 (2. Inject the Saga Middleware into the Redux store.)

When we define the Redux store and initialize it, most of time it will look like this:

当我们定义Redux存储并对其进行初始化时,大多数情况下,它看起来像这样:

const sagaMiddleware = createSagaMiddleware();const store = createStore(rootReducer, [], compose(      applyMiddleware(sagaMiddleware)  );sagaMiddleware.run(rootSaga); /* inject our sagas into the middleware*/

3.创建Sagas。 (3. Create the Sagas.)

First, we are going to define the sequence of the loadUser Saga:

首先,我们将定义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});  }}

We can read it like this:

我们可以这样阅读:

  • First, call a function called getUser, and assign the result to the const user.

    首先,调用一个名为getUser的函数,并将结果分配给const user

  • Later, dispatch an action called FETCH_USER_SUCCESS and pass the value of user to be consumed by the store.

    稍后,调度一个名为FETCH_USER_SUCCESS的操作,并传递要由商店使用的用户值。

  • If something goes bad, dispatch an action called FETCH_FAILED.

    如果出现问题,请调度一个名为FETCH_FAILED的操作

As you can see, it’s really cool that we can add the result of a yield operation to a variable.

如您所见,我们可以将yield操作的结果添加到变量中,这真的很酷。

Now we’re going to create the sequenced 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});  }
}

We can read the saga as follows:

我们可以阅读以下的传奇故事:

  • Wait for the FETCH_USER_SUCCESS action to be dispatched. This basically will be on hold until an event triggers it. We use the take effect for this.

    等待发送FETCH_USER_SUCCESS操作。 这基本上将一直保持到事件触发它为止。 我们使用这个生效

  • We take a value from the store. The select effect receives a function which has access to the store. We assign the user information to the constant user.

    我们从商店中获取价值。 选择效果接收可以访问商店的功能。 我们将用户信息分配给固定用户。

  • We exec an async operation to load the departure information, and pass the user as parameter using the call Effect.

    我们执行一个异步操作以加载出发信息,并使用调用效果将用户作为参数传递给用户。

  • After the loadDeparture is finished, we execute the loadFlight with the departure object fetched in the previous operation.

    loadDeparture完成之后,我们使用在先前操作中获取的离开对象执行loadFlight

  • The same will apply with the forecast, we need to wait until the flight is loaded to execute the next call effect.

    预测也是如此,我们需要等到加载航班后才能执行下一个通话效果。

  • Finally, once all the operations are finished, we use the put Effect to dispatch and action to the store and send all the arguments using the information loaded during the whole saga.

    最后,一旦所有操作完成,我们将使用put Effect向商店分发和操作,并使用整个故事中加载的信息发送所有参数。

As you can see, a saga is a collection of steps that wait from previous actions to modify their behaviors. Once finished, all the information is ready to be consumed in the store.

正如您所看到的,传奇是一系列步骤的集合,这些步骤等待先前的操作来修改其行为。 完成后,所有信息都准备好在商店中使用。

Pretty neat, eh?

很整洁吧?

Now let’s check a different case. Consider getFlight and getForecast can be triggered at the same time. They don’t need for one to finish in order to start the other, so we can create a different panel for that case.

现在让我们检查另一种情况。 考虑到可以同时触发getFlightgetForecast 。 他们不需要一个人完成就可以启动另一个,因此我们可以为这种情况创建一个不同的面板。

无阻碍的传奇 (Non-blocking Saga)

In order to execute two non-blocking operations, we need to make a little modification to our previous 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});  }}

We have to register the yield as an array:

我们必须将yield注册为数组:

const [flight, forecast] = yield [call(loadFlight, departure.flightID), call(loadForecast, departure.date)];

So both operations are called in parallel, but at the end of the day we can will wait for both to end to update the UI if needed.

因此,这两个操作是并行调用的,但是在一天结束时,如果需要,我们可以等待两者都结束以更新UI。

Then we need to register the saga into the rootSaga:

然后我们需要将传奇注册到rootSaga中:

function* rootSaga() {  yield[    fork(loadUser),    takeLatest('LOAD_DASHBOARD', loadDashboardSequenced),    takeLatest('LOAD_DASHBOARD2' loadDashboardNonSequenced)
];}

What if we need to update the UI as soon as operation is finished?

如果我们需要在操作完成后立即更新UI,该怎么办?

Don’t worry — I’ve got your back.

别担心-我支持你。

无序无阻塞Sagas (Non Sequenced and Non Blocking Sagas)

We can also isolate our sagas and combine them, meaning they can work independently. That’s exactly what we need. Let’s take a look.

我们还可以隔离Sagas并将其合并,这意味着它们可以独立运行。 这正是我们所需要的。 让我们来看看。

Step #1: We isolate the Forecast and the Flight Saga. They both depend on departure.

步骤#1 :我们将“预测”和“飞行传奇”隔离开来。 他们俩都依赖离开。

/* **************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});    }}

Notice something very important here? This is how we architect our sagas:

注意这里很重要的事情吗? 这就是我们构建Sagas的方式:

  • They both are waiting for the same Action Event (FETCH_DEPARTURE3_SUCCESS) to start.

    他们俩都在等待相同的动作事件 (FETCH_DEPARTURE3_SUCCESS)启动。

  • They will receive a value when this event is triggered. More detail on this in the next step.触发此事件时,他们将收到一个值。 在下一步中对此有更多详细信息。
  • They will execute their async operation using the call Effectand both will trigger the same event after completion. But they both send different data to the store. Thanks to the power of Redux, we can do this without any modification to our reducer.

    他们将使用效果调用执行异步操作 并且两者都将在完成后触发同一事件。 但是它们都将不同的数据发送到存储。 借助Redux的强大功能,我们可以在不对reducer进行任何修改的情况下执行此操作。

Step #2: Let’s make the changes to the departure sequence and make sure it sends a departure value with two other sagas:

步骤#2 :让我们对出发顺序进行更改,并确保它与其他两个sagas发送出发值:

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});  }}

Nothing different here until we get to the put Effect. we can pass an object to the actions and it will be yielded into the departure const in the departure and flight saga. I love this.

在达到put效果之前,这里没有什么不同。 我们可以将一个对象传递给动作,然后将其放到出发和飞行传奇中的出发常量中。 我喜欢这个。

Feel free to see the demo, and notice how the third panel loads the forecast before the flight because the timeout is higher, to simulate a slower request.

随时观看演示 ,并注意第三个面板在飞行前如何加载预测,因为超时时间更长,以模拟较慢的请求。

In a production app, I’d probably do things a little different. I just wanted to point out that you can pass values when using the put effect.

在生产应用程序中,我可能会做一些不同的事情。 我只想指出,使用put效果时可以传递值。

那测试呢? (What about testing?)

You do test your code… right?

您确实测试了代码...对吗?

Sagas are easy to test, but they are coupled with your steps, are set into the sequenced due to the nature of generators. Let’s see an example. (And feel free to check the all the test in the repo into the sagas folder):

Sagas易于测试,但是由于发电机的特性,它们与您的脚步相结合,被设置为顺序。 让我们来看一个例子。 (并随时检查回购到sagas文件夹中的所有测试):

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);    });});
  1. Make sure you import all the effect and functions helpers that you are going to test.确保导入要测试的所有效果和功能助手。
  2. When you store a value on the yield, you need to pass the mock data to the next function. Notice the third, forth and fifth test.当存储收益率值时,需要将模拟数据传递给下一个函数。 注意第三,第四和第五项测试。
  3. Behind the scene, each generator moves to the next line after a yield when the next method is called. This is why we use the saga.next().value here.

    在后台,调用next方法时,每个生成器将在yield之后移至下一行。 这就是为什么我们在这里使用saga.next()。value的原因

  4. This sequence is set in stone. If you change the steps on the saga, the test won’t pass.这个顺序是固定的。 如果您更改了传奇中的步骤,则测试将无法通过。

结论。 (Conclusion.)

I really like to test new technologies and in the front end development, we find new stuff almost on daily basis. It’s like a fashion: once something is accepted by the community, it’s like everyone wants to use it. Sometimes I find a lot of value in these things, but it’s still important to sit down and check to see whether we really need something.

我真的很想测试新技术,并且在前端开发中,我们几乎每天都会发现新东西。 这就像一种时尚:一旦某种东西被社区接受,就好像每个人都想使用它。 有时候,我在这些东西上发现了很多价值,但是坐下来检查我们是否真的需要一些东西仍然很重要。

I’ve found thunks easier to implement and maintain. But for more complex operation, Redux-Saga does a really great job.

我发现thunk更易于实现和维护。 但是对于更复杂的操作,Redux-Saga做得非常好。

Once again, I thank Thomas for the inspiration for this post. I hope someone finds as much inspiration in this post as I did in his :).

再次感谢Thomas的灵感。 我希望有人能像我在他的文章中那样从中得到启发。

If you have any questions, feel free to tweet at me. I’m happy to help.

如果您有任何疑问,请随时向我发推文 。 我很乐意提供帮助。

If you happen to be more interested about this topic, feel free to check the part 2 of this serie Redux-saga common patterns.

如果您对这个主题更感兴趣,请随时查看该系列Redux-saga通用模式的第2部分。

the mediocre engineerFor more content like this, please consider subscribe to my channelwww.youtube.com

平庸的工程师 想要获得更多此类内容,请考虑订阅我的频道 www.youtube.com

Finally feel free to check my open source projects at the moment:

最后,随时检查我的开源项目:

  • React Calendar Multiday

    React日历多天

翻译自: https://www.freecodecamp.org/news/async-operations-using-redux-saga-2ba02ae077b3/

使用Redux-Saga进行异步操作相关推荐

  1. React native 项目进阶(redux, redux saga, redux logger)

    之前利用知乎日报的api写了react-native的一个入门项目,传送文章地址React Native 项目入门和源码地址RN入门项目源码,目前github上的代码已经在原文的基础上增加了新的功能, ...

  2. 如何在React Native中使用Redux Saga监视网络更改

    by Pritish Vaidya 通过Pritish Vaidya 如何在React Native中使用Redux Saga监视网络更改 (How to monitor network change ...

  3. redux中间件saga和thunk的区别

    redux中的action仅支持原始对象(plain object),处理有副作用的action,需要使用中间件.中间件可以在发出action,到reducer函数接受action之间,执行具有副作用 ...

  4. redux中间件原理-讲义

    1.redux中间件简介 1.1.什么是redux中间件 redux 提供了类似后端 Express 的中间件概念,本质的目的是提供第三方插件的模式,自定义拦截 action -> reduce ...

  5. React+Redux技术栈核心要点解析(中篇)

    感谢作者郭永峰的授权发布. 作者:郭永峰,前端架构师,现用友网络 FED团队负责人,目前主要负责企业级应用前端技术平台建设工作,在前端工程化实现.Node 应用开发.React技术.移动开发等方向有丰 ...

  6. 一篇文章读懂 React and redux 前端开发 -DvaJS, a lightweight and elm-style framework.

    DvaJS: React and redux based, lightweight and elm-style framework. https://dvajs.com/ 实例项目源码:https:/ ...

  7. Redux中间件(redux-thunk、redux-promise、redux-saga)

    文章目录 1.redux中间件简介 1.1.什么是redux中间件 1.2.使用redux中间件 2.中间件的运行机制 2.1.createStore源码分析 2.2.applyMiddleware源 ...

  8. React + Redux

    相当长一段时间以来,我一直在React和Redux中实现应用程序.在过去的几年里,我写了两本关于它的电子书,并发布了学习React及其生态系统的课程平台.课程平台甚至内置在React和Redux中.我 ...

  9. 使用React,Redux,redux-sage构建图片库(翻译)

    看到这篇文章build an image gallery using redux saga,觉得写的不错,长短也适中. 文后有注释版的github代码库,请使用comment分枝. Flickr AP ...

最新文章

  1. mysql存储过程不常用_Python--day46--mysql存储过程(不常用)(包含防sql注入)
  2. 剑指 Offer II 022. 链表中环的入口节点(力扣剑指Offer专项突击版——链表2)
  3. 图的两种存储形式(邻接矩阵、邻接表)
  4. 如何利用 gulp 压缩混淆 “上古”时期的项目文件
  5. [转]cmd 设置环境cmd环境变量命令set 设置永久环境变量命令setx
  6. Linux下快速搭建DNS服务器
  7. 一张图告诉你,自学编程和科班程序员的差别在哪
  8. Linux+varnish安装配置
  9. 【Mac】一些软件的图片和视频位置 QQ 微信
  10. 《计算机系统:核心概念及软硬件实现(原书第4版)》——3.1 无符号二进制表示...
  11. intel 显卡 opencl安装
  12. ANX6585D VSP/VSN 正负压输出,适用于TFT-LCD小屏应用,兼容FP7721、NT50198。
  13. android曲面屏点击事件无响应,都说曲面屏中看不中用,主要原因有四点,第三点是关键!...
  14. apex老是显示匹配服务器失败,Apex英雄与服务器不同步怎么办-服务器连接超时怎么办 - Iefans...
  15. smtplib 改为通过SSL 465 发送邮件
  16. Android 学习之垂直切换的圆角 Banner 和垂直指示器
  17. Android设置文件共享
  18. 双减之后,体育培训升温,如何为孩子选择合适的体育项目?
  19. git branch -D 大写的D 删除分支
  20. linux 启动脚本

热门文章

  1. 吃货开发 阶段01 类的定义 方法的布局 0925
  2. flask-配置的设置-三种配置的实现方法
  3. dj鲜生-13-类视图-使用篇
  4. git-索引-1909
  5. -bash: composer: command not found解决办法
  6. 黄聪:VS2017调试时提示“运行时无法计算表达式的值”
  7. 一个Demo学会用Android兼容包新控件
  8. Android 5.0有哪些变化
  9. 人工智能 - paddlepaddle飞桨 - 深度学习基础教程 - 生成对抗网络
  10. MongoDB的RestAPI微服务组件--Mongo-Rest介绍