web-dev-qa-db-ja.com

ファイルとして反応するフェッチ応答をダウンロードする方法

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"

私は何が欠けていますか?レデューサーで何をすべきですか?

37
Rafael K.

現在、ブラウザテクノロジーは、Ajaxリクエストからのファイルの直接ダウンロードをサポートしていません。回避策は、非表示のフォームを追加し、バックグラウンドで送信して、ブラウザーが「保存」ダイアログをトリガーするようにすることです。

標準のFlux実装を実行しているため、正確なRedux(Reducer)コードがどうあるべきかわかりませんが、ファイルダウンロード用に作成したワークフローは次のようになります...

  1. FileDownloadというReactコンポーネントがあります。このコンポーネントは、非表示のフォームをレンダリングし、componentDidMountの内部ですぐにフォームを送信し、そのonDownloadComplete propを呼び出します。
  2. 別のReactコンポーネントがあります。これをWidgetと呼び、ダウンロードボタン/アイコン(実際には...テーブルの各アイテムに1つ)を使用します。 Widgetには、対応するアクションファイルとストアファイルがあります。 WidgetFileDownloadをインポートします。
  3. Widgetには、ダウンロードに関連するhandleDownloadhandleDownloadCompleteの2つのメソッドがあります。
  4. Widget storeには、downloadPathというプロパティがあります。デフォルトではnullに設定されています。値がnullに設定されている場合、ダウンロード中のファイルはなく、WidgetコンポーネントはFileDownloadコンポーネントをレンダリングしません。
  5. Widgetのボタン/アイコンをクリックすると、handleDownloadアクションをトリガーするdownloadFileメソッドが呼び出されます。 downloadFileアクションは、Ajax要求を行いません。 DOWNLOAD_FILEイベントをストアにディスパッチし、ダウンロードするファイルのdownloadPathを送信します。ストアはdownloadPathを保存し、変更イベントを発行します。
  6. 現在downloadPathがあるので、WidgetFileDownloadを含む必要なプロップとdownloadPathメソッドをhandleDownloadCompleteの値として渡すことでonDownloadCompleteをレンダリングします。
  7. FileDownloadがレンダリングされ、フォームがmethod="GET"(POSTも動作するはずです)およびaction={downloadPath}で送信されると、サーバーの応答により、ターゲットダウンロードファイルのブラウザーの[保存]ダイアログがトリガーされます(IE 9/10、最新のFirefoxおよびChrome)。
  8. フォーム送信の直後に、onDownloadComplete/handleDownloadCompleteが呼び出されます。これにより、DOWNLOAD_FILEイベントを送出する別のアクションがトリガーされます。ただし、今回はdownloadPathnullに設定されます。ストアはdownloadPathnullとして保存し、変更イベントを発行します。
  9. 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 Babel 6.xで0.14.7 ["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>
        );
    }
}
45
Nate

これら2つのライブラリを使用してファイルをダウンロードできます http://danml.com/download.htmlhttps://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);
          })
        }
      });
31
Anton Philin

私も同じ問題に一度直面しました。私はそれを参照して空のリンクを作成して解決しました:

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

基本的には、リンクにblob url(href)を割り当て、ダウンロード属性を設定し、リンクを1回クリックします。私が理解している限り、これは@Nateが提供する答えの「基本的な」アイデアです。この方法でこれを行うのが良いかどうかはわかりません...

2
Stanislav