浏览器技术目前不支持直接从 Ajax 请求下载文件。解决方法是添加一个隐藏的表单并在后台提交它以使浏览器触发“保存”对话框。
我正在运行一个标准的 Flux 实现,所以我不确定确切的 Redux(Reducer)代码应该是什么,但我刚刚为文件下载创建的工作流程是这样的......
- 我有一个名为
FileDownload
. 这个组件所做的就是渲染一个隐藏的表单,然后在里面componentDidMount
,立即提交表单并调用它的onDownloadComplete
prop。
- 我有另一个 React 组件,我们称之为
Widget
,带有一个下载按钮/图标(实际上很多......一个表格中的每个项目)。Widget
有相应的动作和存储文件。Widget
进口FileDownload
。
Widget
有两种与下载相关的方法:handleDownload
和handleDownloadComplete
。
Widget
store 有一个名为downloadPath
. 它null
默认设置为。当它的值设置为 时null
,没有正在进行的文件下载并且Widget
组件不会呈现FileDownload
组件。
- 单击中的按钮/图标
Widget
调用handleDownload
触发downloadFile
操作的方法。该downloadFile
操作不会发出 Ajax 请求。它将DOWNLOAD_FILE
事件分派到商店,并随它一起发送downloadPath
要下载的文件。商店保存downloadPath
并发出更改事件。
- 由于现在有一个
downloadPath
,Widget
将渲染FileDownload
传递必要的props,包括downloadPath
以及handleDownloadComplete
方法作为 的值onDownloadComplete
。
- 当
FileDownload
呈现并提交表单时method="GET"
(POST 也应该工作) and action={downloadPath}
,服务器响应现在将触发浏览器的目标下载文件的保存对话框(在 IE 9/10、最新的 Firefox 和 Chrome 中测试)。
- 紧跟在表单提交之后,
onDownloadComplete
/handleDownloadComplete
被调用。这会触发另一个调度DOWNLOAD_FILE
事件的操作。但是,该时间downloadPath
设置为null
。商店将其另存downloadPath
为null
并发出更改事件。
- 由于不再存在不渲染
downloadPath
的FileDownload
组件Widget
,因此世界是一个快乐的地方。
Widget.js - 仅部分代码
import FileDownload from './FileDownload';
export default class Widget extends Component {
constructor(props) {
super(props);
this.state = widgetStore.getState().toJS();
}
handleDownload(data) {
widgetActions.downloadFile(data);
}
handleDownloadComplete() {
widgetActions.downloadFile();
}
render() {
const downloadPath = this.state.downloadPath;
return (
// button/icon with click bound to this.handleDownload goes here
{downloadPath &&
<FileDownload
actionPath={downloadPath}
onDownloadComplete={this.handleDownloadComplete}
/>
}
);
}
widgetActions.js - 仅部分代码
export function downloadFile(data) {
let downloadPath = null;
if (data) {
downloadPath = `${apiResource}/${data.fileName}`;
}
appDispatcher.dispatch({
actionType: actionTypes.DOWNLOAD_FILE,
downloadPath
});
}
widgetStore.js - 仅部分代码
let store = Map({
downloadPath: null,
isLoading: false,
// other store properties
});
class WidgetStore extends Store {
constructor() {
super();
this.dispatchToken = appDispatcher.register(action => {
switch (action.actionType) {
case actionTypes.DOWNLOAD_FILE:
store = store.merge({
downloadPath: action.downloadPath,
isLoading: !!action.downloadPath
});
this.emitChange();
break;
FileDownload.js
- 准备好复制和粘贴的完整、功能齐全的代码
- React 0.14.7 with Babel 6.x ["es2015", "react", "stage-0"]
- 表单需要display: none
是“隐藏的” "className
是为了
import React, {Component, PropTypes} from 'react';
import ReactDOM from 'react-dom';
function getFormInputs() {
const {queryParams} = this.props;
if (queryParams === undefined) {
return null;
}
return Object.keys(queryParams).map((name, index) => {
return (
<input
key={index}
name={name}
type="hidden"
value={queryParams[name]}
/>
);
});
}
export default class FileDownload extends Component {
static propTypes = {
actionPath: PropTypes.string.isRequired,
method: PropTypes.string,
onDownloadComplete: PropTypes.func.isRequired,
queryParams: PropTypes.object
};
static defaultProps = {
method: 'GET'
};
componentDidMount() {
ReactDOM.findDOMNode(this).submit();
this.props.onDownloadComplete();
}
render() {
const {actionPath, method} = this.props;
return (
<form
action={actionPath}
className="hidden"
method={method}
>
{getFormInputs.call(this)}
</form>
);
}
}