浏览器技术目前不支持直接从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请求.它向store 发送DOWNLOAD_FILE
事件,同时发送downloadPath
以便下载文件.store 保存downloadPath
并发出更改事件.
- 由于现在有一个
downloadPath
,Widget
将渲染FileDownload
通过必要的props ,包括downloadPath
以及handleDownloadComplete
方法作为onDownloadComplete
的值.
- 当呈现
FileDownload
并提交method="GET"
(POST也可以)和action={downloadPath}
时,服务器响应现在将触发浏览器的目标下载文件保存对话框(在IE 9/10、最新Firefox和Chrome中测试).
- 表格提交后立即调用
onDownloadComplete
/handleDownloadComplete
.这会触发另一个发送DOWNLOAD_FILE
事件的操作.然而,这次downloadPath
被设置为null
.store 将downloadPath
保存为null
,并发出更改事件.
- 由于不再有
downloadPath
,FileDownload
组件不会在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;
文件下载.js
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>
);
}
}