ES6 jeneratörleri ile redux-saga kullanmanın artıları / eksileri vs ES2017 zaman uyumsuz / beklemede redux-thunk


488

Şu anda redux kasabasındaki son çocuk, redux-saga / redux-saga hakkında çok fazla konuşma var . İşlemleri dinlemek / göndermek için jeneratör işlevlerini kullanır.

Başımı etrafına sarmadan önce , async / await ile redux-sagakullandığım aşağıdaki yaklaşım yerine kullanmanın artılarını / eksilerini bilmek istiyorum redux-thunk.

Bir bileşen böyle görünebilir, her zamanki gibi eylemler gönderir.

import { login } from 'redux/auth';

class LoginForm extends Component {

  onClick(e) {
    e.preventDefault();
    const { user, pass } = this.refs;
    this.props.dispatch(login(user.value, pass.value));
  }

  render() {
    return (<div>
        <input type="text" ref="user" />
        <input type="password" ref="pass" />
        <button onClick={::this.onClick}>Sign In</button>
    </div>);
  } 
}

export default connect((state) => ({}))(LoginForm);

Sonra eylemlerim şöyle görünür:

// auth.js

import request from 'axios';
import { loadUserData } from './user';

// define constants
// define initial state
// export default reducer

export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}

// more actions...

// user.js

import request from 'axios';

// define constants
// define initial state
// export default reducer

export const loadUserData = (uid) => async (dispatch) => {
    try {
        dispatch({ type: USERDATA_REQUEST });
        let { data } = await request.get(`/users/${uid}`);
        dispatch({ type: USERDATA_SUCCESS, data });
    } catch(error) {
        dispatch({ type: USERDATA_ERROR, error });
    }
}

// more actions...

6
Ayrıca bkz. Redux-thunk ile redux-saga karşılaştırması: stackoverflow.com/a/34623840/82609
Sebastien Lorber

22
Yapmadan ::önce ne this.onClickvar?
Downhillski

37
@ZhenyangHua işlevi nesneye ( this), yani aka bağlama için kısa eldir this.onClick = this.onClick.bind(this). Her el için kısa el yeniden bağlandığından, uzun formun genellikle yapıcıda yapılması önerilir.
hampusohlsson

7
Anlıyorum. Teşekkürler! Bu işleve bind()geçmek thisiçin çok şey kullanan insanlar görüyorum , ama () => method()şimdi kullanmaya başladım .
Downhillski

2
@Hosar Redux & redux-saga'yı bir süre üretimde kullandım, ancak aslında birkaç ay sonra MobX'e taşındım çünkü daha az ek yük
hampusohlsson

Yanıtlar:


461

Redux-saga'da yukarıdaki örneğin eşdeğeri

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST)
    try {
      let { data } = yield call(request.post, '/login', { user, pass });
      yield fork(loadUserData, data.uid);
      yield put({ type: LOGIN_SUCCESS, data });
    } catch(error) {
      yield put({ type: LOGIN_ERROR, error });
    }  
  }
}

export function* loadUserData(uid) {
  try {
    yield put({ type: USERDATA_REQUEST });
    let { data } = yield call(request.get, `/users/${uid}`);
    yield put({ type: USERDATA_SUCCESS, data });
  } catch(error) {
    yield put({ type: USERDATA_ERROR, error });
  }
}

Dikkat edilmesi gereken ilk şey, formu kullanarak api işlevlerini çağırmamızdır yield call(func, ...args). callefekti yürütmez, sadece düz bir nesne oluşturur {type: 'CALL', func, args}. Yürütme, işlevi yerine getirmeye ve jeneratörün sonucuyla devam etmesine özen gösteren redux-saga ara katman yazılımına devredilir.

Temel avantaj, jeneratörü Redux'un dışında basit eşitlik kontrolleri kullanarak test edebilmenizdir.

const iterator = loginSaga()

assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))

// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
  iterator.next(mockAction).value, 
  call(request.post, '/login', mockAction)
)

// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
  iterator.throw(mockError).value, 
  put({ type: LOGIN_ERROR, error: mockError })
)

Api çağrısı sonucunu, alay edilen verileri nextyineleyici yöntemine enjekte ederek alay ettiğimizi unutmayın . Verileri taklit etmek, taklit işlevlerinden çok daha kolaydır.

Dikkat edilmesi gereken ikinci şey, çağrıdır yield take(ACTION). Thunks, her yeni eylemde eylem oluşturucu tarafından çağrılır (örn. LOGIN_REQUEST). yani eylemler sürekli olarak gövdelere itilir ve bu eylemlerin ne zaman durdurulacağı konusunda thunks'ın kontrolü yoktur.

Redux-destan olarak, jeneratörler çekin sonraki eylem. yani bir eylemi ne zaman dinleyeceklerini ve ne zaman dinlemeyeceklerini kontrol ederler. Yukarıdaki örnekte, akış talimatları bir while(true)döngünün içine yerleştirilmiştir , bu nedenle, gelen itme davranışını taklit eden her gelen eylemi dinleyecektir.

Çekme yaklaşımı, karmaşık kontrol akışlarının uygulanmasına izin verir. Örneğin, aşağıdaki gereksinimleri eklemek istediğimizi varsayalım

  • LOGOUT kullanıcı işlemini yönetme

  • ilk başarılı oturum açma işleminde, sunucu bir expires_inalanda depolanan gecikme süresi dolmak üzere bir simge döndürür . Her expires_inmilisaniyede arka planda yetkilendirmeyi yenilememiz gerekecek

  • Api çağrılarının (ilk oturum açma veya yenileme) sonucunu beklerken kullanıcının arada oturum kapatabileceğini dikkate alın.

Bunu thunks ile nasıl uygularsınız; tüm akış için tam test kapsamı sağlarken? İşte Sagas ile nasıl görünebilir:

function* authorize(credentials) {
  const token = yield call(api.authorize, credentials)
  yield put( login.success(token) )
  return token
}

function* authAndRefreshTokenOnExpiry(name, password) {
  let token = yield call(authorize, {name, password})
  while(true) {
    yield call(delay, token.expires_in)
    token = yield call(authorize, {token})
  }
}

function* watchAuth() {
  while(true) {
    try {
      const {name, password} = yield take(LOGIN_REQUEST)

      yield race([
        take(LOGOUT),
        call(authAndRefreshTokenOnExpiry, name, password)
      ])

      // user logged out, next while iteration will wait for the
      // next LOGIN_REQUEST action

    } catch(error) {
      yield put( login.error(error) )
    }
  }
}

Yukarıdaki örnekte, eşzamanlılık şartımızı kullanarak ifade ediyoruz race. Eğer take(LOGOUT)kazanır yarış (yani kullanıcı Çıkış Düğme tıklandığında). Yarış authAndRefreshTokenOnExpiryarka plan görevini otomatik olarak iptal eder . Ve eğer authAndRefreshTokenOnExpirybir call(authorize, {token})çağrının ortasında engellenmişse, iptal edilir. İptal otomatik olarak aşağı doğru ilerler.

Yukarıdaki akışın çalıştırılabilir bir demosunu bulabilirsiniz


@yassine İşlev nereden delaygeliyor? Ah, buldum: github.com/yelouafi/redux-saga/blob/…
philk

122
redux-thunkKod oldukça iyi okunabilir ve kendinden açıkladı. Ama redux-sagas: Bir başlıca nedeni bu fiil benzeri fonksiyonların gerçekten okunamaz olduğu call, fork, take, put...
SyG

11
@syg, çağrı, çatal, al ve koy ifadesinin daha anlamsal olarak dost olabileceğini kabul ediyorum. Bununla birlikte, tüm yan etkileri test edilebilir yapan fiil benzeri işlevlerdir.
Downhillski

3
@syg hala bu garip fiiller fonksiyonları ile bir fonksiyon derin vaat zinciri ile bir fonksiyon daha okunabilir
Yasser Sinjab

3
bu "tuhaf" fiiller de destanın redux'dan gelen mesajlarla ilişkisini kavramsallaştırmanıza yardımcı olur. yapabilecekleriniz almak sonraki yinelemesini tetiklemek için sıklıkta ve yapabilecekleriniz - redux dışına mesajı türleri koymak için yan etkinin sonucunu yayın için geri yeni mesajlar.
worc

104

Kütüphane yazarının oldukça kapsamlı cevabına ek olarak, üretim sisteminde destan kullanma deneyimimi de ekleyeceğim.

Pro (destan kullanarak):

  • Testedilebilirlik. Sagas'ı test etmek (call () saf bir nesne döndürdüğü için çok kolaydır. Testler normalde testinize bir mockStore eklemenizi gerektirir.

  • redux-saga, görevler hakkında birçok yararlı yardımcı işlevle birlikte gelir. Bana öyle geliyor ki destan kavramı, uygulamanız için tepki redux mimarisinde (actionCreators ve redüktörlerin saf işlevler olması gerekir.) Eksik bir parça görevi gören bir tür arka plan çalışanı / iş parçacığı oluşturmaktır.

  • Sagas, tüm yan etkileri ele almak için bağımsız bir yer sunar. Deneyimimde değişiklik yapmak ve yönetmek genellikle thunk eylemlerinden daha kolaydır.

con:

  • Jeneratör sözdizimi.

  • Öğrenmek için birçok kavram.

  • API kararlılığı. Görünüşe göre redux-saga hala özellikler ekliyor (ör. Kanallar?) Ve topluluk o kadar büyük değil. Kütüphane bir gün geriye dönük uyumlu olmayan bir güncelleme yaparsa endişe vardır.


9
Sadece biraz yorum yapmak istiyorum, aksiyon yaratıcısının Dan'ın kendisi tarafından birçok kez iddia edildiği saf bir işlev olması gerekmiyor.
Marson Mao

14
Şu an itibariyle, kullanım ve toplum genişledikçe redux-sagas çok tavsiye edilmektedir. Ayrıca, API daha olgunlaştı. Con'u API stabilitymevcut durumu yansıtacak bir güncelleme olarak kaldırmayı düşünün .
Denialos

1
destan thunk daha başlar ve son taahhüt de thunk sonra
amorenew

2
Evet, FWIW redux-saga'nın 12k yıldızı var, redux-thunk'ın 8k'si var
Brian Burns

3
Sagaların başka bir meydan okumasını ekleyeceğim, sagaların varsayılan olarak eylemlerden ve eylem yaratıcılarından tamamen ayrıştırılmış olması. Thunks, aksiyon yaratıcılarını yan etkileriyle doğrudan bağlarken, sagas, aksiyon yaratıcılarını onları dinleyen sagalardan tamamen ayrı bırakır. Bunun teknik avantajları vardır, ancak kodu takip etmeyi çok daha zorlaştırabilir ve tek yönlü kavramların bazılarını bulanıklaştırabilir.
1919'da 0:25

33

Sadece kişisel deneyimimden bazı yorumlar eklemek istiyorum (hem sagas hem de thunk kullanarak):

Sagas test etmek için harika:

  • Efektlerle sarılmış fonksiyonları taklit etmenize gerek yok
  • Bu nedenle testler temiz, okunabilir ve yazması kolaydır
  • Sagas kullanırken, eylem yaratıcıları çoğunlukla düz nesne değişmezleri döndürür. Thunk'ın vaatlerinin aksine test etmek ve iddia etmek de daha kolaydır.

Sagalar daha güçlüdür. Bir thunk'ın aksiyon yaratıcısında yapabileceklerinizin hepsi bir destanda da yapabilirsiniz, ancak tam tersi olamaz (veya en azından kolayca değil). Örneğin:

  • bir eylemin / eylemlerin gönderilmesini bekleyin ( take)
  • rutin mevcut iptal ( cancel, takeLatest, race)
  • Birden rutinleri aynı eylem dinleyebilirsiniz ( take, takeEvery, ...)

Sagas ayrıca bazı yaygın uygulama modellerini genelleştiren başka yararlı işlevler de sunar:

  • channels harici olay kaynaklarını dinlemek için (örn. websockets)
  • çatal modeli ( fork, spawn)
  • boğaz
  • ...

Sagalar harika ve güçlü bir araçtır. Ancak güç ile sorumluluk gelir. Uygulamanız büyüdüğünde, eylemin kimin gönderilmesini beklediğini veya bir eylem gönderildiğinde her şeyin ne olduğunu anlayarak kolayca kaybolabilirsiniz. Öte yandan, thunk daha basit ve mantıklıdır. Birini veya diğerini seçmek, projenin türü ve boyutu, projenizin ne tür yan etkileri ele alması veya ekip tercihi gibi birçok özelliğe bağlıdır. Her durumda uygulamanızı basit ve öngörülebilir tutun.


8

Sadece bazı kişisel deneyimler:

  1. Kodlama stili ve okunabilirliği için, geçmişte redux-saga kullanmanın en önemli avantajlarından biri, redux-thunk'ta geri arama cehenneminden kaçınmaktır - artık birçok yuvalama kullanmaya / artık yakalamaya gerek yoktur. Ancak şimdi async / bekliyor thunk popülaritesi ile, redux-thunk kullanırken senkronize tarzda async kodu da yazılabilir, bu redux-think'da bir gelişme olarak kabul edilebilir.

  2. Redux-saga kullanırken, özellikle Daktilo metinlerinde çok daha fazla kaynak kodu yazmanız gerekebilir. Örneğin, bir getirme eşzamansız işlevi uygulamak isterse, veri ve hata işleme tek bir FETCH eylemi ile action.js içindeki bir thunk biriminde doğrudan gerçekleştirilebilir. Ancak redux-saga'da birinin FETCH_START, FETCH_SUCCESS ve FETCH_FAILURE eylemlerini ve bunların tüm ilgili tip kontrollerini tanımlaması gerekebilir, çünkü redux-saga'daki özelliklerden biri efekt oluşturmak ve talimat vermek için bu tür zengin “simge” mekanizmasını kullanmaktır kolay test için redux mağazası. Tabii ki, bu eylemleri kullanmadan bir destan yazabilirdi, ama bu onu bir thunk'a benzetirdi.

  3. Dosya yapısı açısından, redux-saga birçok durumda daha açık gibi görünmektedir. Her sagas.ts'de zaman uyumsuzlukla ilgili bir kod kolayca bulunabilir, ancak redux-thunk'ta, eylemlerde görmesi gerekir.

  4. Kolay test, redux-saga'daki bir diğer ağırlıklı özellik olabilir. Bu gerçekten uygun. Ancak açıklığa kavuşturulması gereken bir şey, redux-saga “call” testinin testte gerçek API çağrısı yapmamasıdır, bu nedenle API çağrısından sonra kullanabileceği adımlar için örnek sonucu belirtmek gerekecektir. Bu nedenle, redux-saga'da yazmadan önce, bir destan ve buna karşılık gelen sagas.spec.ts'i ayrıntılı olarak planlamak daha iyi olacaktır.

  5. Redux-saga, görevleri paralel olarak çalıştırmak, takeLatest / takeEvery, fork / spawn gibi eşzamanlılık yardımcıları, thunks'tan çok daha güçlü olan birçok gelişmiş özellik sunar.

Sonuç olarak, kişisel olarak şunu söylemek isterim: birçok normal durumda ve küçük ila orta boy uygulamalarda, async / await style redux-thunk ile gidin. Size birçok kazan plakası kodunu / eylemini / typedef'i kurtaracak ve birçok farklı sagas.ts arasında geçiş yapmanız ve belirli bir sagas ağacını korumanız gerekmeyecek. Ancak, çok karmaşık asenkron mantığı ve eşzamanlılık / paralel model gibi özelliklere ihtiyaç duyduğunuz veya test ve bakım (özellikle test odaklı geliştirmede) için yüksek bir talebe sahip büyük bir uygulama geliştiriyorsanız, redux-sagas muhtemelen hayatınızı kurtarabilir .

Her neyse, redux-saga, redux'un kendisinden daha zor ve karmaşık değildir ve iyi sınırlı çekirdek kavramlarına ve API'lara sahip olduğu için dik bir öğrenme eğrisi yoktur. Redux-saga öğrenmek için biraz zaman harcamak gelecekte bir gün kendinize fayda sağlayabilir.


5

Deneyimlerime göre birkaç farklı büyük ölçekli React / Redux projesini gözden geçirdikten sonra Sagas, geliştiricilere kod yazmanın çok daha kolay ve yanlış anlaşılması daha zor olan daha yapısal bir yol sunuyor.

Evet, başlamak biraz garip, ancak çoğu geliştirici bir günde bunu anlamaya yetiyor. İnsanlara her zaman ne yieldile başlayacakları konusunda endişelenmemelerini ve birkaç test yazdığınızda size geleceğini söylüyorum.

MVC patten kontrolörleri gibi thunks tedavi gibi bir çift proje gördüm ve bu hızla unmaintable karışıklık haline gelir.

Benim tavsiyem Sagas'ı ihtiyacınız olan yerde kullanmaktır A tek bir olayla ilgili B tipi şeyleri tetikler. Bir dizi eylemi kesebilecek herhangi bir şey için, müşteri ara katman yazılımı yazmanın ve tetiklemek için bir FSA eyleminin meta özelliğini kullanmanın daha kolay olduğunu düşünüyorum.


2

Thunks ve Sagas

Redux-Thunkve Redux-Sagabirkaç önemli açıdan farklılık gösterir, her ikisi de Redux için ara katman yazılımı kütüphaneleridir (Redux ara katman yazılımı, dispatch () yöntemi ile mağazaya gelen eylemleri engelleyen koddur).

Bir eylem tam anlamıyla herhangi bir şey olabilir, ancak en iyi uygulamaları izliyorsanız, eylem bir tür alanı ve isteğe bağlı yük, meta ve hata alanları olan düz bir javascript nesnesidir. Örneğin

const loginRequest = {
    type: 'LOGIN_REQUEST',
    payload: {
        name: 'admin',
        password: '123',
    }, };

Redux-Thunk

Standart eylemler göndermeye ek olarak, Redux-Thunkara katman yazılımı denilen özel işlevleri göndermenizi sağlar thunks.

Thunks (Redux'da) genellikle aşağıdaki yapıya sahiptir:

export const thunkName =
   parameters =>
        (dispatch, getState) => {
            // Your application logic goes here
        };

Yani a thunk, (isteğe bağlı olarak) bazı parametreleri alan ve başka bir işlev döndüren bir işlevdir. İç işlev, her ikisi de ara katman yazılımı tarafından sağlanacak olan bir dispatch functionve bir işlev alır .getStateRedux-Thunk

Redux-Saga

Redux-Sagaara katman yazılımı, karmaşık uygulama mantığını, sagas adı verilen saf işlevler olarak ifade etmenizi sağlar. Saf fonksiyonlar test açısından arzu edilir, çünkü tahmin edilebilir ve tekrarlanabilirler, bu da onları test etmeyi nispeten kolaylaştırır.

Sagalar, jeneratör fonksiyonları adı verilen özel fonksiyonlarla uygulanır. Bunlar yeni bir özellik ES6 JavaScript. Temel olarak, yürütme, bir getiri ifadesi gördüğünüz her yerde bir jeneratöre girip çıkar. Bir yieldifadeyi, jeneratörün verilen değeri duraklatmasına ve döndürmesine neden olarak düşünün . Daha sonra arayan, jeneratörü aşağıdaki ifadeden devam ettirebilir yield.

Bir jeneratör fonksiyonu bu şekilde tanımlanır. Function anahtar sözcüğünden sonra yıldız işaretine dikkat edin.

function* mySaga() {
    // ...
}

Giriş efsanesi kaydolduktan sonra Redux-Saga. Ancak daha sonra yieldilk satıra geçilmesi 'LOGIN_REQUEST', mağazaya tipte bir işlem gönderilinceye kadar destanı duraklatacaktır . Bu gerçekleştiğinde, yürütme devam edecektir.

Daha fazla ayrıntı için bu makaleye bakın .


1

Kısa bir not. Jeneratörler iptal edilebilir, zaman uyumsuz / beklemede - değil. Yani sorudan bir örnek için, ne seçeceğine dair bir anlam ifade etmiyor. Ancak daha karmaşık akışlar için bazen jeneratörleri kullanmaktan daha iyi bir çözüm yoktur.

Yani, başka bir fikir olabilir, redux-thunk ile jeneratörleri kullanmak, ama benim için, kare tekerlekli bir bisiklet icat etmeye çalışıyor gibi görünüyor.

Ve elbette, jeneratörleri test etmek daha kolaydır.


0

İşte her ikisinin de en iyi kısımlarını (artılarını) birleştiren bir proje redux-sagave redux-thunk: dispatchingilgili eylemle bir söz alırken, saga üzerindeki tüm yan etkileri ele alabilirsiniz : https://github.com/diegohaz/redux-saga-thunk

class MyComponent extends React.Component {
  componentWillMount() {
    // `doSomething` dispatches an action which is handled by some saga
    this.props.doSomething().then((detail) => {
      console.log('Yaay!', detail)
    }).catch((error) => {
      console.log('Oops!', error)
    })
  }
}

1
then()React bileşeninin içinde kullanılması paradigmaya aykırıdır. componentDidUpdateBir vaatin çözülmesini beklemek yerine , değiştirilen durumu ele almalısınız .

3
@ Maxincredible52 Sunucu Tarafı Oluşturma için geçerli değildir.
Diego Haz

Deneyimlerime göre, Max'in noktası sunucu tarafı oluşturma için hala doğrudur. Bu muhtemelen yönlendirme katmanında bir yerde ele alınmalıdır.
ThinkingInBits

3
@ Maxincredible52 neden paradigmaya karşı, bunu nereden okudun? Ben genellikle @Diego Haz benzer yapmak ama componentDidMount bunu (React dokümanlar göre, ağ aramaları tercih orada yapılmalıdır) bu yüzdencomponentDidlMount() { this.props.doSomething().then((detail) => { this.setState({isReady: true})} }
user3711421 14:17

0

Daha kolay bir yol redux-auto kullanmaktır .

belgeselden

redux-auto, bir söz veren bir "eylem" işlevi oluşturmanıza izin vererek bu eşzamansız sorunu düzeltti. "Varsayılan" fonksiyon eylem mantığınıza eşlik etmek için.

  1. Diğer Redux zaman uyumsuz ara yazılımlara gerek yoktur. örneğin thunk, promise-middleware, saga
  2. Kolayca redux içine bir söz geçmesine izin verir ve sizin için başarmış
  3. Harici servis çağrılarını dönüştürülecekleri yerde bulmanıza olanak tanır
  4. "İnit.js" dosyasını adlandırmak, uygulama başlangıcında dosyayı bir kez çağırır. Bu, başlangıçta sunucudan veri yüklemek için iyidir

Fikir, her eylemin belirli bir dosyada olmasını sağlamaktır . "beklemede", "yerine getirildi" ve "reddedildi" için redüktör işlevlerine sahip dosyadaki sunucu çağrısının birlikte bulunması. Bu işlem vaatleri çok kolaylaştırır.

Ayrıca durumunuzun prototipine otomatik olarak bir yardımcı nesne ("eşzamansız" denir) ekleyerek kullanıcı arayüzünüzde istenen geçişleri izlemenize olanak tanır.


2
Farklı çözümlerin de göz önünde bulundurulması gerektiğinden, alakasız bir cevap olsa bile +1 yaptım
amorenew

12
Bence oradalar çünkü projenin yazarı olduğunu açıklamadı
jreptak
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.