如何下载获取响应作为文件

这是 actions.js中的代码

export function exportRecordToExcel(record) {
return ({fetch}) => ({
type: EXPORT_RECORD_TO_EXCEL,
payload: {
promise: fetch('/records/export', {
credentials: 'same-origin',
method: 'post',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
}).then(function(response) {
return response;
})
}
});
}

返回的响应是一个 .xlsx文件。我希望用户能够将它保存为一个文件,但是什么也不会发生。我假设服务器正在返回正确的响应类型,因为在控制台中它说

Content-Disposition:attachment; filename="report.xlsx"

我错过了什么? 我应该在减速器做什么?

165812 次浏览

浏览器技术目前不支持直接从 Ajax 请求下载文件。解决方法是添加一个隐藏的表单,并在后台提交它,让浏览器触发 Save 对话框。

我正在运行一个标准的 Flux 实现,所以我不确定具体的 Redux (Reducer)代码应该是什么,但是我刚刚为一个文件下载创建的工作流是这样的..。

  1. 我有一个叫做 FileDownload的反应组件。这个组件所做的就是呈现一个隐藏的表单,然后在 componentDidMount中立即提交表单并调用它的 onDownloadComplete道具。
  2. 我还有另一个 React 组件,我们称之为 Widget,带有一个下载按钮/图标(实际上很多... ... 表中的每个项目都有一个)。Widget有相应的操作和存储文件。进口 FileDownload
  3. Widget有两个与下载相关的方法: handleDownloadhandleDownloadComplete
  4. Widget存储有一个名为 downloadPath的属性。默认设置为 null。当它的值设置为 null时,没有正在下载的文件,而且 Widget组件不呈现 FileDownload组件。
  5. 单击 Widget中的按钮/图标将调用触发 downloadFile操作的 handleDownload方法。downloadFile操作不发出 Ajax 请求。它向存储发送一个 DOWNLOAD_FILE事件,同时发送 downloadPath供文件下载。该存储保存 downloadPath并发出一个更改事件。
  6. 由于现在有一个 downloadPathWidget将使 FileDownload通过必要的道具,包括 downloadPath以及 handleDownloadComplete方法作为 onDownloadComplete的值。
  7. FileDownload被渲染并且表单被提交到 method="GET"(POST 应该也可以)和 action={downloadPath}中时,服务器响应将触发浏览器的目标下载文件的保存对话框(在 IE9/10、最新的 Firefox 和 Chrome 中测试过)。
  8. 在表单提交之后,立即调用 onDownloadComplete/handleDownloadComplete。这将触发另一个分派 DOWNLOAD_FILE事件的操作。但是,这一次 downloadPath被设置为 null。该存储将 downloadPath保存为 null并发出一个更改事件。
  9. 由于不再有一个 downloadPathFileDownload组件不再呈现在 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}
/>
}
);
}

Js-仅部分代码

export function downloadFile(data) {
let downloadPath = null;


if (data) {
downloadPath = `${apiResource}/${data.fileName}`;
}


appDispatcher.dispatch({
actionType: actionTypes.DOWNLOAD_FILE,
downloadPath
});
}

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;

文件下载
- 可供复制和粘贴的完整、功能齐全的代码
- 使用 Babel 6.x 反应0.14.7[“ es2015”,“反应”,“阶段-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>
);
}
}

您可以使用这两个库来下载文件 http://danml.com/download.html https://github.com/eligrey/FileSaver.js/#filesaverjs

例子

//  for FileSaver
import FileSaver from 'file-saver';
export function exportRecordToExcel(record) {
return ({fetch}) => ({
type: EXPORT_RECORD_TO_EXCEL,
payload: {
promise: fetch('/records/export', {
credentials: 'same-origin',
method: 'post',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
}).then(function(response) {
return response.blob();
}).then(function(blob) {
FileSaver.saveAs(blob, 'nameFile.zip');
})
}
});


//  for download
let download = require('./download.min');
export function exportRecordToExcel(record) {
return ({fetch}) => ({
type: EXPORT_RECORD_TO_EXCEL,
payload: {
promise: fetch('/records/export', {
credentials: 'same-origin',
method: 'post',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
}).then(function(response) {
return response.blob();
}).then(function(blob) {
download (blob);
})
}
});

我也遇到过同样的问题。 我已经解决了这个问题,通过创建一个空链接,像这样:

linkRef = React.createRef();
render() {
return (
<a ref={this.linkRef}/>
);
}

在我的提取函数中,我做了这样的事情:

fetch(/*your params*/)
}).then(res => {
return res.blob();
}).then(blob => {
const href = window.URL.createObjectURL(blob);
const a = this.linkRef.current;
a.download = 'Lebenslauf.pdf';
a.href = href;
a.click();
a.href = '';
}).catch(err => console.error(err));

基本上,我已经将 blobs url (href)分配给链接,设置 download 属性并强制在链接上单击一次。 据我所知,这是由@Nate 提供的“基本”答案。 我不知道这样做是不是个好主意... 我做到了。

我只需要下载一个文件 onClick,但是我需要运行一些逻辑来获取或计算文件所在的实际 URL。我也不想使用任何反反应命令模式,比如设置一个 ref,当我有资源 URL 时手动点击它。我使用的声明模式是

onClick = () => {
// do something to compute or go fetch
// the url we need from the server
const url = goComputeOrFetchURL();


// window.location forces the browser to prompt the user if they want to download it
window.location = url
}


render() {
return (
<Button onClick={ this.onClick } />
);
}

我设法下载由其余 API URL 生成的文件,这种代码在我的本地工作得很好:

    import React, {Component} from "react";
import {saveAs} from "file-saver";


class MyForm extends Component {


constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}


handleSubmit(event) {
event.preventDefault();
const form = event.target;
let queryParam = buildQueryParams(form.elements);


let url = 'http://localhost:8080/...whatever?' + queryParam;


fetch(url, {
method: 'GET',
headers: {
// whatever
},
})
.then(function (response) {
return response.blob();
}
)
.then(function(blob) {
saveAs(blob, "yourFilename.xlsx");
})
.catch(error => {
//whatever
})
}


render() {
return (
<form onSubmit={this.handleSubmit} id="whateverFormId">
<table>
<tbody>
<tr>
<td>
<input type="text" key="myText" name="myText" id="myText"/>
</td>
<td><input key="startDate" name="from" id="startDate" type="date"/></td>
<td><input key="endDate" name="to" id="endDate" type="date"/></td>
</tr>
<tr>
<td colSpan="3" align="right">
<button>Export</button>
</td>
</tr>


</tbody>
</table>
</form>
);
}
}


function buildQueryParams(formElements) {
let queryParam = "";


//do code here
    

return queryParam;
}


export default MyForm;

这招对我很管用。

const requestOptions = {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
};


fetch(`${url}`, requestOptions)
.then((res) => {
return res.blob();
})
.then((blob) => {
const href = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', 'config.json'); //or any other extension
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
.catch((err) => {
return Promise.reject({ Error: 'Something Went Wrong', err });
})

我认为这个解决方案可能比其他方案更“被动”一些:

import React, { forwardRef, useImperativeHandle, useLayoutEffect, useState } from 'react';


export interface DownloadHandle {
download: (params: { title: string; data?: Blob }) => void;
}


export const Download = forwardRef<DownloadHandle, {}>((props, ref) => {
const linkRef = React.useRef<HTMLAnchorElement>(null);
const [download, setDownload] = useState<{ title: string; data: Blob }>();
useImperativeHandle(ref, () => ({
download: (params) => {
if (params.data) {
setDownload(params as typeof download);
}
},
}));


//trigger download and clear data
useLayoutEffect(() => {
if (download) {
linkRef?.current?.click();
}
setDownload(undefined);
}, [download]);


if (!download) {
return null;
}
const { title, data } = download;
return <a href={window.URL.createObjectURL(data)} download={title} ref={linkRef} />;
});


export type DownloadElement = React.ElementRef<typeof Download>;

用法

const App = () => {


const downloadRef = useRef<DownloadElement>(null);


const handleDownload = () => {
fetch(url, requestOptions)
.then((res) => res.blob())
.then((data) => {
downloadRef.current?.download({ title: `myFile.txt`, data});
});
}


return (
<div>
<Download ref={downloadRef} />
<button onClick={}>Download</button>
</div>
)
}