这是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"

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

推荐答案

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

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

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

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>
        );
    }
}

Reactjs相关问答推荐

未捕获的类型错误:类型Orbitum创建者未定义

为什么我的React App.js只在页面刷新时呈现3次?

导致useState中断的中断模式

无法找出状态正在被更改的位置

透明MOV文件未出现在我的React网页中

在React中使用映射创建flex组件

Github 操作失败:发布 NPM 包

React-router动态路径

Chakra UI:如何在选中状态下设置复选框 colored颜色

在添加了useplacesautocomplete后,在构建React/Next.js项目时,NPM抛出类型期望的错误

ReactJS on AWS Amplify与EC2上的Spring boot服务之间的混合内容错误

如何将 DocuSign 控制台界面嵌入到 React 应用中

使用新数据更新时,React 不更新具有过滤功能的深层嵌套对象数组中的变量状态

从 React 应用程序调用未经身份验证的 graphql 查询时出现错误:没有当前用户

页面不显示

Reactjs 表单未反映提交时的更改

从 API 中提取映射数组后出现重复元素

react-query、react-hook-form 和表单验证

在 redux 工具包中使用 dispatch 发送多个操作

未捕获的错误:操作必须是使用 redux/toolkit 的普通对象