バックグラウンド:
リクエストの管理にエピックを使用しています。
リクエストごとにトークンを送信します。トークンは期限切れになる可能性がありますが、猶予期間内に更新できます。
すべてのリクエストにトークンを使用していますが、リクエストを送信する前に、トークンの有効期限が切れているかどうかを確認したいと思います。有効期限が切れていて猶予期間がある場合は、最初にトークンを更新してから、対応するアクションに進みます。
すべてのリクエストには独自の叙事詩があります。
今私が試しているのは、トークンをチェックしてトークンを更新してからアクションを続行するためのすべてのアクションの事前フックです。
これが説明することを願っています。
// epics for querying data
// these epics are using a token, that is stored in redux state.
const getMenuEpic = action$ => ....
const getFoodListEpic = action$ => ....
const getFoodItemEpic = action$ => ....
...
// helper function to check
// if token has expired
const hasTokenExpired = (token) => .....
// refresh token
// this returns a new token in the promise
const refreshToken = fetch('http://.../refresh-toekn')
// i am trying to make an epic, that will fire
// before any actions in the application
// stop the action (let's call it action A)
// get token from redux state,
// verify if is valid or not
// if invalid call refresh token (async process),
// and when refresh token finished, proceed with the incoming action A
// if the token was valid then continue with action A.
const refreshEpic = (action$, store) =>
action$.map(() => store.getState().token)
.filter(Boolean)
.filter(token => hasTokenExpired(token))
.mergeMap(() => refreshToken()) ...
......
しかし、このアプローチはrefreshEpicでは機能しません
アクションがレデューサーに到達するのを本当に防ぐことはできません-エピックに与えられる前に実際にはすでにレデューサーを通過しています-代わりに、フェッチする意図を示すアクションをディスパッチできますが、実際にはそれをトリガーするものではありません。たとえば、UIはFETCH_SOMETHINGをディスパッチし、エピックはそれを確認し、有効な更新トークンがあることを確認し(または新しいトークンを取得し)、実際にフェッチをトリガーする別のアクションを発行します(例:FETCH_SOMETHING_WITH_TOKEN)。
この特定のケースでは、同じ要件を持つ多くの叙事詩があり、そのようにするのは面倒になる可能性があります。これを簡単にする方法はたくさんあります。ここにいくつかあります:
あなたはあなたのためにチェックをするヘルパーを書くことができます、そしてそれがリフレッシュを必要とするならば、それは先に進む前にそれを要求して待つでしょう。実際の更新は個別の専用エピックで個人的に処理するため、複数の同時更新要求などを防ぐことができます。
const requireValidToken = (action$, store, callback) => {
const needsRefresh = hasTokenExpired(store.getState().token);
if (needsRefresh) {
return action$.ofType(REFRESH_TOKEN_SUCCESS)
.take(1)
.takeUntil(action$.ofType(REFRESH_TOKEN_FAILED))
.mergeMap(() => callback(store.getState().token))
.startWith({ type: REFRESH_TOKEN });
} else {
return callback(store.getState().token);
}
};
const getMenuEpic = (action$, store) =>
action$.ofType(GET_MENU)
.switchMap(() =>
requireValidToken(action$, store, token =>
actuallyGetMenu(token)
.map(response => getMenuSuccess(response))
)
);
編集:これは私の最初の提案でしたが、上記のものよりもはるかに複雑です。いくつかの利点もありますが、上記のIMOは使いやすく、保守も簡単です。
代わりに、「スーパーエピック」を作成することもできます。これは、それ自体が構成され、他のエピックに委任されるエピックです。ルートエピックはスーパーエピックの一例です。(私は今その用語を作りました...笑)
おそらくやりたいことの1つは、ランダムアクションと認証トークンを必要とするアクションを区別することです。認証トークンをチェックして、ディスパッチされたすべてのアクションに対して更新する必要はありません。簡単な方法は、アクションにメタデータを含めることです。{ meta: { requiresAuth: true } }
これははるかに複雑ですが、他のソリューションよりも長所があります。これが私が話していることの大まかな考えですが、それはテストされておらず、おそらく100%考え抜かれたものではありません。コピーパスタではなく、インスピレーションと考えてください。
// action creator helper to add the requiresAuth metadata
const requiresAuth = action => ({
...action,
meta: {
...action.meta,
requiresAuth: true
}
});
// action creators
const getMenu = id => requiresAuth({
type: 'GET_MENU',
id
});
const getFoodList = () => requiresAuth({
type: 'GET_FOOD_LIST'
});
// epics
const getMenuEpic = action$ => stuff
const getFoodListEpic = action$ => stuff
const refreshTokenEpic = action$ =>
action$.ofType(REFRESH_TOKEN)
// If there's already a pending refreshToken() we'll ignore the new
// request to do it again since its redundant. If you instead want to
// cancel the pending one and start again, use switchMap()
.exhaustMap(() =>
Observable.from(refreshToken())
.map(response => ({
type: REFRESH_TOKEN_SUCCESS,
token: response.token
}))
// probably should force user to re-login or whatevs
.catch(error => Observable.of({
type: REFRESH_TOKEN_FAILED,
error
}))
);
// factory to create a "super-epic" which will only
// pass along requiresAuth actions when we have a
// valid token, refreshing it if needed before.
const createRequiresTokenEpic = (...epics) => (action$, ...rest) => {
// The epics we're delegating for
const delegatorEpic = combineEpics(...epics);
// We need some way to emit REFRESH_TOKEN actions
// so I just hacked it with a Subject. There is
// prolly a more elegant way to do this but #YOLO
const output$ = new Subject();
// This becomes action$ for all the epics we're delegating
// for. This will hold off on giving an action to those
// epics until we have a valid token. But remember,
// this doesn't delay your *reducers* from seeing it
// as its already been through them!
const filteredAction$ = action$
.mergeMap(action => {
if (action.meta && action.meta.requiresAuth) {
const needsRefresh = hasTokenExpired(store.getState().token);
if (needsRefresh) {
// Kick off the refreshing of the token
output$.next({ type: REFRESH_TOKEN });
// Wait for a successful refresh
return action$.ofType(REFRESH_TOKEN_SUCCESS)
.take(1)
.mapTo(action)
.takeUntil(action$.ofType(REFRESH_TOKEN_FAILED));
// Its wise to handle the case when refreshing fails.
// This example just gives up and never sends the
// original action through because presumably
// this is a fatal app state and should be handled
// in refreshTokenEpic (.e.g. force relogin)
}
}
// Actions which don't require auth are passed through as-is
return Observable.of(action);
});
return Observable.merge(
delegatorEpic(filteredAction$, ...rest),
output$
);
};
const requiresTokenEpic = createRequiresTokenEpic(getMenuEpic, getFoodList, ...etc);
前述のように、この問題に取り組む方法はたくさんあります。この「スーパーエピック」アプローチの代わりにトークンを必要とするエピック内で使用するある種のヘルパー関数を作成することを想像できます。あなたにとってそれほど複雑ではないと思われることをしてください。
この記事はインターネットから収集されたものであり、転載の際にはソースを示してください。
侵害の場合は、連絡してください[email protected]
コメントを追加