文件上传的 Redux thunk 设计,包括取消和进度
Redux thunk design for file upload, including cancel and progress
想在 react-redux 中上传一些文件,我有了以下想法:
- 设置一个 redux-thunk
uploadFile
动作,启动上传,以 File
描述符作为参数
- 在商店中定义我自己的 "file descriptor" 可序列化(使用
uuid
、pending
、sent
、errored
和 pending
属性
- 设置一些其他 FSA,例如
addFile
、removeFile
、setFileErrored
、setFileSent
、setFileSent
像这样
减速机
switch (action.type) {
case FILES__ADD_FILE:
return {
...state,
files: [
...state.files,
action.payload
]
}
case FILES__REMOVE_FILE:
return {
...state,
files: state.files.filter(
file => file.id !== action.payload
)
}
case FILES__SET_FILE_ERRORED:
return {
...state,
files: state.files.map(file => {
if(file.id !== action.payload.fileId) {
return file
}
return {
...file,
sending: false,
errored: true,
sent: false
}
})
}
case FILES__SET_FILE_SENDING:
return {
...state,
files: state.files.map(file => {
if(file.id !== action.payload) {
return file
}
return {
...file,
sending: true,
errored: false,
sent: false
}
})
}
case FILES__SET_FILE_SENT:
return {
...state,
files: state.files.map(file => {
if(file.id !== action.payload) {
return file
}
return {
...file,
sending: false,
errored: false,
sent: true
}
})
}
case FILES__SET_FILE_PROGRESS:
return {
...state,
files: state.files.map(file => {
if(file.id !== action.payload.fileId) {
return file
}
return {
...file,
progress: action.payload.progress
}
})
}
default:
return state
}
动作
// skipping static actions
export const uploadFile = (actualFile) => {
const file = {
id: uuidv4(),
sending: false,
sent: false,
errored: false
}
return (dispatch) => {
dispatch(addFile({
...file,
sending: true
}))
return uploadFile(actualFile, {
onUploadProgress: (evt) => {
if(evt.loaded && evt.total) {
const progress = (evt.loaded / evt.total) * 100
dispatch(setFileProgress(file.id, progress))
}
}
})
.then((fileUrl) => {
dispatch(setFileSent(file.id))
dispatch(setFileUrl(file.id, url))
})
.catch((err) => {
console.log(err)
dispatch(setFileErrored(file.id))
})
}
}
note : uploadFile
是我的帮手,包装了一个 axios promise。
第一个参数是一个 File
描述符,第二个参数是一个 axios 选项对象。
我认为应该可行..
但现在我正在为一些设计问题而苦苦挣扎:
- 这是正确的做法吗?我是说:
- 这充满了杂质,但是 ajax 查询本质上是不纯的..
- 我完全失去了
File
描述符引用,因此不允许以后有任何机会访问它(例如 preview
)。我会把它存放在哪里?我觉得把它存储在商店里太糟糕了,主要是因为我们不能纯粹用 ES6 的东西更新 File
描述符,因此我们需要改变它
- Axios 提供了一个简洁的
CancelToken
东西,我可以将其传递给我的选项。我之前一直在 React 中使用它,但是切换到 redux 时,我遇到了同样的问题:如果我在 uploadFile()
中定义一个 cancelToken
,我应该把它存储在哪里,这样我就可以在里面访问它进一步说,cancelFileUpload(fileId)
thunk 动作 ?
在 reducer 方面,我认为你的行为很好(spread/rest+1)。但是通过使用一些像 simple-update-in
这样的 "update-in" 库,代码结构看起来会更好一些。它可以帮助您消除一些 filter()
电话。
在动作设计上,我猜你正在建立一个上传队列,所以你需要有QUEUE
/SEND_PENDING
/SEND_PROGRESS
/SEND_REJECTED
/SEND_FULFILLED
(为了清楚起见,我用 redux-promise-middleware
命名方法重新表述了它们。)这里没有什么是你无法逃避的。
关于动作工厂,因为 Promise 没有进度事件,所以现在看起来有点笨拙。您可以尝试使用 redux-saga
来实现。代码看起来稍微干净了一点。但是处理多个事件变得轻而易举。但是那里有一个学习曲线。代码应该与下面类似。关注下面的 while-loop,并查看取消处理(通过 CANCEL
操作)。
import { call, put, race, takeEvery } from 'redux-saga/effects';
import EventAsPromise from 'event-as-promise';
yield takeEvery('QUEUE', function* (action) {
yield put({ type: 'SEND_PENDING' });
const { file } = action.payload;
const progress = new EventAsPromise();
const donePromise = uploadFile(file, progress.eventListener);
try {
while (true) {
// Wait until one of the Promise is resolved/rejected
const [progress] = yield race([
// New progress event came in
call(progress.upcoming),
// File upload completed promise resolved or rejected
call(() => donePromise),
// Someone dispatched 'CANCEL'
take(action => action.type === 'CANCEL' && action.payload.file === file)
]);
if (progress) {
// Progress event come first, so process it here
yield put({
type: 'SEND_PROGRESS',
payload: {
loaded: progress.loaded,
total: progress.total
}
});
} else {
// Either done or cancelled (use your cancel token here)
// Breaking a while-true loop isn't really looks good, but there aren't any better options here
break;
}
}
// Here, we assume CANCEL is a kind of fulfillment and do not require retry logic
yield put({ type: 'SEND_FULFILLED' });
} catch (err) {
yield put({ type: 'SEND_REJECTED', error: true, payload: err });
}
});
要上传文件,请发送 QUEUE
操作。要取消上传文件,只需发送一个 CANCEL
操作。
由于redux-saga
只接受Promise或action,为了将事件流转化为Promise,所以这里介绍event-as-promise
有了redux-saga
,流程控制(progress/done/error/cancel)变得非常清晰并且不容易出现错误。在您的情况下,我使用 redux-saga
的方式类似于文件上传的生命周期管理器。设置和拆卸总是成对进行的。如果拆解调用 (done/error/cancel) 未正确触发,您无需担心。
你需要弄清楚关于文件描述符的故事。尽量不要将它放在商店中,因为它会阻止商店被持久化,您可能希望在页面导航或应用重启时持久化商店。看你的场景,如果用于重试上传,可以暂时存放在saga里面的闭包中。
在此示例中,您可以轻松地将其扩展为上传文件 one-by-one 的上传队列。精通redux-saga
不难,改代码10行以内。如果没有 redux-saga
,要实现这一目标将是一项艰巨的任务。
想在 react-redux 中上传一些文件,我有了以下想法:
- 设置一个 redux-thunk
uploadFile
动作,启动上传,以File
描述符作为参数 - 在商店中定义我自己的 "file descriptor" 可序列化(使用
uuid
、pending
、sent
、errored
和pending
属性 - 设置一些其他 FSA,例如
addFile
、removeFile
、setFileErrored
、setFileSent
、setFileSent
像这样
减速机
switch (action.type) {
case FILES__ADD_FILE:
return {
...state,
files: [
...state.files,
action.payload
]
}
case FILES__REMOVE_FILE:
return {
...state,
files: state.files.filter(
file => file.id !== action.payload
)
}
case FILES__SET_FILE_ERRORED:
return {
...state,
files: state.files.map(file => {
if(file.id !== action.payload.fileId) {
return file
}
return {
...file,
sending: false,
errored: true,
sent: false
}
})
}
case FILES__SET_FILE_SENDING:
return {
...state,
files: state.files.map(file => {
if(file.id !== action.payload) {
return file
}
return {
...file,
sending: true,
errored: false,
sent: false
}
})
}
case FILES__SET_FILE_SENT:
return {
...state,
files: state.files.map(file => {
if(file.id !== action.payload) {
return file
}
return {
...file,
sending: false,
errored: false,
sent: true
}
})
}
case FILES__SET_FILE_PROGRESS:
return {
...state,
files: state.files.map(file => {
if(file.id !== action.payload.fileId) {
return file
}
return {
...file,
progress: action.payload.progress
}
})
}
default:
return state
}
动作
// skipping static actions
export const uploadFile = (actualFile) => {
const file = {
id: uuidv4(),
sending: false,
sent: false,
errored: false
}
return (dispatch) => {
dispatch(addFile({
...file,
sending: true
}))
return uploadFile(actualFile, {
onUploadProgress: (evt) => {
if(evt.loaded && evt.total) {
const progress = (evt.loaded / evt.total) * 100
dispatch(setFileProgress(file.id, progress))
}
}
})
.then((fileUrl) => {
dispatch(setFileSent(file.id))
dispatch(setFileUrl(file.id, url))
})
.catch((err) => {
console.log(err)
dispatch(setFileErrored(file.id))
})
}
}
note : uploadFile
是我的帮手,包装了一个 axios promise。
第一个参数是一个 File
描述符,第二个参数是一个 axios 选项对象。
我认为应该可行..
但现在我正在为一些设计问题而苦苦挣扎:
- 这是正确的做法吗?我是说:
- 这充满了杂质,但是 ajax 查询本质上是不纯的..
- 我完全失去了
File
描述符引用,因此不允许以后有任何机会访问它(例如preview
)。我会把它存放在哪里?我觉得把它存储在商店里太糟糕了,主要是因为我们不能纯粹用 ES6 的东西更新File
描述符,因此我们需要改变它
- Axios 提供了一个简洁的
CancelToken
东西,我可以将其传递给我的选项。我之前一直在 React 中使用它,但是切换到 redux 时,我遇到了同样的问题:如果我在uploadFile()
中定义一个cancelToken
,我应该把它存储在哪里,这样我就可以在里面访问它进一步说,cancelFileUpload(fileId)
thunk 动作 ?
在 reducer 方面,我认为你的行为很好(spread/rest+1)。但是通过使用一些像 simple-update-in
这样的 "update-in" 库,代码结构看起来会更好一些。它可以帮助您消除一些 filter()
电话。
在动作设计上,我猜你正在建立一个上传队列,所以你需要有QUEUE
/SEND_PENDING
/SEND_PROGRESS
/SEND_REJECTED
/SEND_FULFILLED
(为了清楚起见,我用 redux-promise-middleware
命名方法重新表述了它们。)这里没有什么是你无法逃避的。
关于动作工厂,因为 Promise 没有进度事件,所以现在看起来有点笨拙。您可以尝试使用 redux-saga
来实现。代码看起来稍微干净了一点。但是处理多个事件变得轻而易举。但是那里有一个学习曲线。代码应该与下面类似。关注下面的 while-loop,并查看取消处理(通过 CANCEL
操作)。
import { call, put, race, takeEvery } from 'redux-saga/effects';
import EventAsPromise from 'event-as-promise';
yield takeEvery('QUEUE', function* (action) {
yield put({ type: 'SEND_PENDING' });
const { file } = action.payload;
const progress = new EventAsPromise();
const donePromise = uploadFile(file, progress.eventListener);
try {
while (true) {
// Wait until one of the Promise is resolved/rejected
const [progress] = yield race([
// New progress event came in
call(progress.upcoming),
// File upload completed promise resolved or rejected
call(() => donePromise),
// Someone dispatched 'CANCEL'
take(action => action.type === 'CANCEL' && action.payload.file === file)
]);
if (progress) {
// Progress event come first, so process it here
yield put({
type: 'SEND_PROGRESS',
payload: {
loaded: progress.loaded,
total: progress.total
}
});
} else {
// Either done or cancelled (use your cancel token here)
// Breaking a while-true loop isn't really looks good, but there aren't any better options here
break;
}
}
// Here, we assume CANCEL is a kind of fulfillment and do not require retry logic
yield put({ type: 'SEND_FULFILLED' });
} catch (err) {
yield put({ type: 'SEND_REJECTED', error: true, payload: err });
}
});
要上传文件,请发送 QUEUE
操作。要取消上传文件,只需发送一个 CANCEL
操作。
由于redux-saga
只接受Promise或action,为了将事件流转化为Promise,所以这里介绍event-as-promise
有了redux-saga
,流程控制(progress/done/error/cancel)变得非常清晰并且不容易出现错误。在您的情况下,我使用 redux-saga
的方式类似于文件上传的生命周期管理器。设置和拆卸总是成对进行的。如果拆解调用 (done/error/cancel) 未正确触发,您无需担心。
你需要弄清楚关于文件描述符的故事。尽量不要将它放在商店中,因为它会阻止商店被持久化,您可能希望在页面导航或应用重启时持久化商店。看你的场景,如果用于重试上传,可以暂时存放在saga里面的闭包中。
在此示例中,您可以轻松地将其扩展为上传文件 one-by-one 的上传队列。精通redux-saga
不难,改代码10行以内。如果没有 redux-saga
,要实现这一目标将是一项艰巨的任务。