(N00B警报)我正在尝试使用redux-saga(其中fetch
为isomorphic-fetch)实现以下redux-thunk操作:
export function addPostRequest(post) {
return (dispatch) => {
fetch(`${baseURL}/api/addPost`, {
method: 'post',
body: JSON.stringify({
post: {
name: post.name,
title: post.title,
content: post.content,
},
}),
headers: new Headers({
'Content-Type': 'application/json',
}),
}).then(res => res.json()).then(res => dispatch(addPost(res.post))).catch(err => console.log(err));
};
}
export function fetchPosts() {
return (dispatch) => {
return fetch(`${baseURL}/api/getPosts`)
.then(response => response.json())
.then(response => dispatch(addPosts(response.posts)));
};
}
我已经尝试过(可能非常不正确)以下的变化无济于事(我意识到fork调用不应该包含anon生成器函数,因为我为了清晰起见而设置它们但我是原型设计所以我没有还不担心清晰度:
function callApi(url, requestObj = {}) {
return fetch(url, requestObj)
.then(res => res.json())
.then(response => response)
.catch(err => console.log(err));
}
function *fetchPosts() {
while (true) {
yield take(ActionTypes.FETCH_POSTS);
const posts = yield fork(function *() { //eslint-disable-line
const result = yield call(callApi, `${baseURL}/api/getPosts`);
return result.posts;
});
yield put(Actions.addPosts(posts));
}
}
function *addPostRequest() {
while (true) {
const post = yield take(ActionTypes.ADD_POST_REQUEST);
yield fork(function *(post) { //eslint-disable-line
yield call(callApi, `${baseURL}/api/addPost`, {
method: 'post',
body: JSON.stringify({
post: {
name: post.name,
title: post.title,
content: post.content,
},
}),
headers: new Headers({
'Content-Type': 'application/json',
}),
});
}, post);
yield put(Actions.addPost(post));
}
}
export default function *() {
yield [
fork(fetchPosts),
fork(addPostRequest),
];
}
并在初始化存储后在服务器端调用默认函数(其中sagaMiddleware
由
function errorHandler(error, getState) {
console.error(error);
console.debug('current state', getState());
}
export const sagaMiddleware = createSagaMiddleware();
export function configureStore(initialState) {
const store = createStore(reducer, initialState, compose(
applyMiddleware(sagaMiddleware, reduxCatch(errorHandler)),
typeof window === 'object' && typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : f => f,
));
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers/post.reducer', () => {
const nextReducer = require('../reducers/post.reducer'); //eslint-disable-line
store.replaceReducer(nextReducer);
});
}
return store;
}
):
const store = configureStore(initialState);
sagaMiddleware.run(rootSaga);
fetchComponentData(store.dispatch, renderProps.components, renderProps.params)
.then(() => {
const createElement = (Component, props) => (
<Component
{...props}
radiumConfig={{ userAgent: req.headers['user-agent'] }}
/>
);
const initialView = renderToString(
<Provider store={store}>
<RouterContext {...renderProps} createElement={createElement} />
</Provider>
);
const finalState = store.getState().toJS();
res.status(200).end(renderFullPage(initialView, finalState, getFilename()));
})
.catch((err) => {
res.end(renderFullPage(`Error: ${err}`, {}, getFilename()));
});
并在客户端:
const store = configureStore(fromJS(window.__INITIAL_STATE__));
sagaMiddleware.run(rootSaga);
const history = syncHistoryWithStore(browserHistory, store, {
selectLocationState(state) {
return state.get('route').toJS();
},
});
更新:以下是在页面呈现服务器端时调用FETCH_POSTS
操作的日志(这是一个通用/同构应用程序),数组中的数据只是随机键盘粉碎: P:
Warning: Failed propType: Invalid prop `posts` of type `Immutable.Map` supplied to `PostContainer`, expected an Immutable.js List. Check the render method of `Connect(PostContainer)`.
Warning: Failed propType: Invalid prop `posts` of type `Immutable.Map` supplied to `PostListView`, expected an Immutable.js List. Check the render method of `Connect(PostListView)`.
Warning: Using Maps as children is not yet fully supported. It is an experimental feature that might be removed. Convert it to a sequence / iterable of keyed ReactElements instead.
Warning: Failed propType: Invalid prop `post` of type `object` supplied to `PostListItem`, expected an Immutable.js Iterable. Check the render method of `PostListView`.
Warning: Failed propType: Invalid prop `post` of type `function` supplied to `PostListItem`, expected an Immutable.js Iterable. Check the render method of `PostListView`.
Warning: Failed propType: Invalid prop `post` of type `string` supplied to `PostListItem`, expected an Immutable.js Iterable. Check the render method of `PostListView`.
Warning: Failed propType: Invalid prop `post` of type `boolean` supplied to `PostListItem`, expected an Immutable.js Iterable. Check the render method of `PostListView`.
Warning: Failed propType: Invalid prop `post` of type `number` supplied to `PostListItem`, expected an Immutable.js Iterable. Check the render method of `PostListView`.
Warning: Failed propType: Required prop `post.name` was not specified in `PostListItem`. Check the render method of `PostListView`.
webpack built 4e1f2a99c3c358612ecd in 6391ms
webpack building...
webpack built 4e1f2a99c3c358612ecd in 443ms
[ { id: 'cino38mfc0000mrrfdyn8v5k0',
name: 'sdf',
title: 'dfdfdh',
content: 'dfhdfh',
slug: 'dfdfdh',
dateadded: '2016-05-01T04:39:36.840Z',
updateddate: null },
{ id: 'cinmhus6x0000mtrfubcw3eur',
name: 'sdf',
title: 'dfhsdfh',
content: '`sdgdsgd',
slug: 'dfhsdfh',
dateadded: '2016-04-30T01:53:13.017Z',
updateddate: null },
{ id: 'cinmgl1wv000140rf5fdizqsl',
name: 'sdgsdg',
title: 'fssddsg',
content: 'fdfhdfh',
slug: 'fssddsg',
dateadded: '2016-04-30T01:17:39.439Z',
updateddate: null } ]
[ { id: 'cino38mfc0000mrrfdyn8v5k0',
name: 'sdf',
title: 'dfdfdh',
content: 'dfhdfh',
slug: 'dfdfdh',
dateadded: '2016-05-01T04:39:36.840Z',
updateddate: null },
{ id: 'cinmhus6x0000mtrfubcw3eur',
name: 'sdf',
title: 'dfhsdfh',
content: '`sdgdsgd',
slug: 'dfhsdfh',
dateadded: '2016-04-30T01:53:13.017Z',
updateddate: null },
{ id: 'cinmgl1wv000140rf5fdizqsl',
name: 'sdgsdg',
title: 'fssddsg',
content: 'fdfhdfh',
slug: 'fssddsg',
dateadded: '2016-04-30T01:17:39.439Z',
updateddate: null } ]
[ { id: 'cino38mfc0000mrrfdyn8v5k0',
name: 'sdf',
title: 'dfdfdh',
content: 'dfhdfh',
slug: 'dfdfdh',
dateadded: '2016-05-01T04:39:36.840Z',
updateddate: null },
{ id: 'cinmhus6x0000mtrfubcw3eur',
name: 'sdf',
title: 'dfhsdfh',
content: '`sdgdsgd',
slug: 'dfhsdfh',
dateadded: '2016-04-30T01:53:13.017Z',
updateddate: null },
{ id: 'cinmgl1wv000140rf5fdizqsl',
name: 'sdgsdg',
title: 'fssddsg',
content: 'fdfhdfh',
slug: 'fssddsg',
dateadded: '2016-04-30T01:17:39.439Z',
updateddate: null } ]
[ { id: 'cino38mfc0000mrrfdyn8v5k0',
name: 'sdf',
title: 'dfdfdh',
content: 'dfhdfh',
slug: 'dfdfdh',
dateadded: '2016-05-01T04:39:36.840Z',
updateddate: null },
{ id: 'cinmhus6x0000mtrfubcw3eur',
name: 'sdf',
title: 'dfhsdfh',
content: '`sdgdsgd',
slug: 'dfhsdfh',
dateadded: '2016-04-30T01:53:13.017Z',
updateddate: null },
{ id: 'cinmgl1wv000140rf5fdizqsl',
name: 'sdgsdg',
title: 'fssddsg',
content: 'fdfhdfh',
slug: 'fssddsg',
dateadded: '2016-04-30T01:17:39.439Z',
updateddate: null } ]
更新2: 所以我已经将reducer,actions和sagas实现为ducks module:
import fetch from 'isomorphic-fetch';
import { fromJS } from 'immutable';
import { take, call, put, fork } from 'redux-saga/effects';
// constants
const ADD_POST = 'ceres/post/ADD_POST';
const ADD_POST_REQUEST = 'ceres/post/sagas/ADD_POST_REQUEST';
const ADD_POSTS = 'ceres/post/ADD_POSTS';
const ADD_SELECTED_POST = 'ceres/post/ADD_SELECTED_POST';
const CHANGE_SELECTED_POST = 'ceres/post/CHANGE_SELECTED_POST';
const DELETE_POST_REQUEST = 'ceres/post/sagas/DELETE_POST_REQUEST';
const DELETE_POST = 'ceres/post/DELETE_POST';
const FETCH_POSTS = 'ceres/post/sagas/FETCH_POSTS';
const GET_POST = 'ceres/post/sagas/GET_POST';
// reducer
const initialState = fromJS({ posts: [], post: {} });
export default function reducer(state = initialState, action) {
switch (action.type) {
case ADD_POST :
return state.update('posts', posts => posts.unshift(action.payload));
case ADD_POSTS :
return state.set('posts', action.payload);
case CHANGE_SELECTED_POST:
return state.set('post', action.payload);
case DELETE_POST :
return state.update('posts', posts => posts.filter(post => post.get('id') !== action.payload.get('id')));
default:
return state;
}
}
// actions
function addPost(post) {
return {
type: ADD_POST,
payload: fromJS({
name: post.name,
title: post.title,
content: post.content,
slug: post.slug,
id: post.id,
dateadded: post.dateadded,
updateddate: post.updateddate,
}),
};
}
export function addPostRequest(post) {
return {
type: ADD_POST_REQUEST,
post,
};
}
export function addPosts(posts) {
return {
type: ADD_POSTS,
payload: fromJS(posts),
};
}
export function fetchPosts() {
return {
type: FETCH_POSTS,
};
}
export function changeSelectedPost(post) {
return {
type: CHANGE_SELECTED_POST,
payload: fromJS(post),
};
}
export function getPostRequest(slug) {
return {
type: GET_POST,
slug,
};
}
export function deletePost(post) {
return {
type: DELETE_POST,
payload: post,
};
}
export function deletePostRequest(post) {
return {
type: DELETE_POST_REQUEST,
post,
};
}
// sagas
const baseURL = typeof window === 'undefined' ? process.env.BASE_URL || (`http://localhost:${(process.env.PORT || 5000)}`) : '';
function callApi(url, requestObj = {}) {
return fetch(url, requestObj)
.then(res => res.json())
.then(response => response)
.catch(err => console.log(err));
}
function *watchFetchPosts() {
while (true) {
yield take(FETCH_POSTS);
yield fork(fetchPostsSaga);
}
}
function *fetchPostsSaga() {
const { posts } = yield call(callApi, `${baseURL}/api/getPosts`);
yield put(addPosts(posts));
}
function *watchAddPostRequest() {
while (true) {
const { post } = yield take(ADD_POST_REQUEST);
yield fork(addPostRequestSaga, post);
}
}
function *addPostRequestSaga(newPost) {
const { post } = yield call(callApi, `${baseURL}/api/addPost`, {
method: 'post',
body: JSON.stringify({
post: {
name: newPost.name,
title: newPost.title,
content: newPost.content,
},
}),
headers: new Headers({
'Content-Type': 'application/json',
}),
});
yield put(addPost(post));
}
function *watchGetPostRequest() {
while (true) {
const { slug } = yield take(GET_POST);
yield fork(getPostRequestSaga, slug);
}
}
function *getPostRequestSaga(slug) {
const { post } = yield call(callApi, `${baseURL}/api/getPost?slug=${slug}`, {
method: 'get',
headers: new Headers({
'Content-Type': 'application/json',
}),
});
yield put(changeSelectedPost(post));
}
function *watchDeletePostRequest() {
while (true) {
const { post } = yield take(DELETE_POST_REQUEST);
yield fork(deletePostRequestSaga, post);
}
}
function *deletePostRequestSaga(post) {
yield [
call(callApi, `${baseURL}/api/deletePost`, {
method: 'post',
body: JSON.stringify({
postId: post.get('id'),
}),
headers: new Headers({
'Content-Type': 'application/json',
}),
}),
put(deletePost(post)),
];
}
export function *rootPostSaga() {
yield [
fork(watchFetchPosts),
fork(watchAddPostRequest),
fork(watchGetPostRequest),
fork(watchDeletePostRequest),
];
}
它们都在客户端而不是服务器端按预期工作:/。我的configureStore.js
文件目前看起来像是:
import { createStore, applyMiddleware, compose } from 'redux';
import { combineReducers } from 'redux-immutable';
import { fromJS } from 'immutable';
import { LOCATION_CHANGE } from 'react-router-redux';
import createSagaMiddleware from 'redux-saga';
import reduxCatch from 'redux-catch';
import postReducer from './modules/post';
function errorHandler(error, getState) {
console.error(error);
console.debug('current state', getState());
}
function routeReducer(state = fromJS({ locationBeforeTransitions: null }), action) {
if (action.type === LOCATION_CHANGE) {
return state.merge({
locationBeforeTransitions: action.payload,
});
}
return state;
}
export default function configureStore(initialState) {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(combineReducers({ postReducer, route: routeReducer }), initialState, compose(
applyMiddleware(sagaMiddleware, reduxCatch(errorHandler)),
typeof window === 'object' && typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : f => f,
));
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('./modules/post', () => {
const nextReducer = require('./modules/post'); //eslint-disable-line
store.replaceReducer(nextReducer);
});
}
store.runSaga = sagaMiddleware.run; //eslint-disable-line
return store;
}
因此,我可以通过运行store.runSaga(rootPostSaga)
来运行rootPostSaga。这适用于我的客户端代码的根文件,但我不确定将它放在服务器端的哪个位置我已经尝试将它放在server/server.js
中:
// some code before and after not shown!
// Server Side Rendering based on routes matched by React-router.
app.use((req, res) => {
match({ routes, location: req.url }, (err, redirectLocation, renderProps) => {
if (err) {
return res.status(500).end('Internal server error');
}
if (!renderProps) {
return res.status(404).end('Not found!');
}
const initialState = fromJS({
postReducer: {
posts: [],
post: {},
},
route: {
locationBeforeTransitions: null,
},
});
const store = configureStore(initialState);
store.runSaga(rootPostSaga);
fetchComponentData(store, renderProps.components, renderProps.params)
.then(() => {
const createElement = (Component, props) => (
<Component
{...props}
radiumConfig={{ userAgent: req.headers['user-agent'] }}
/>
);
const initialView = renderToString(
<Provider store={store}>
<RouterContext {...renderProps} createElement={createElement} />
</Provider>
);
const finalState = store.getState().toJS();
res.status(200).end(renderFullPage(initialView, finalState, getFilename()));
})
.catch((err) => {
res.end(renderFullPage(`Error: ${err}`, {}, getFilename()));
});
});
});
和server/utils/fetchData
(包含`server / server.js的函数fetchComponentData
的文件):
import Promise from 'bluebird';
import { rootPostSaga } from '../../shared/redux/modules/post';
export function fetchComponentData(store, components, params) {
store.runSaga(rootPostSaga);
console.log('ran saga');
const needs = components.reduce((prev, current) => {
return (current.need || [])
.concat((current.WrappedComponent ? current.WrappedComponent.need : []) || [])
.concat(prev);
}, []);
const promises = needs.map(need => store.dispatch(need(params)));
return Promise.all(promises);
}
两者无济于事:/