【问题标题】:Post method does not trigger the "download excel workbook" function in exceljspost方法不会触发exceljs中的“下载excel工作簿”功能
【发布时间】:2021-03-08 08:38:31
【问题描述】:

我目前正在开发一个 React + Node 应用程序。

应用程序将数据条目存储在数据库中。用户应该能够将所有存储的数据从数据库下载到 Excel 工作簿。为此,我使用了 exceljs npm 模块。

问题: 我实现了一个名为 downloadFile 的 POST 路由,当单击下载按钮时,它应该由客户端触发。路由确实被触发了,但是没有下载 excel 文件。

具有讽刺意味的是,在进行故障排除时,我在节点应用程序的 index.js 中将 POST 路由更改为 GET 路由,然后手动键入路由 (http://localhost:3001/downloadFile) 并按回车键键盘和工作簿已下载。这让我相信 downloadFile 路由的核心功能是有效的。

如何让客户端通过点击按钮触发 downloadFile POST 路由?

以下是迄今为止该项目的代码。我没有在 index.js 文件中包含其他路线的代码,这些代码可以完美地工作以确保简洁和清晰。

服务器端代码(NODEJS 和 EXPRESS)

index.js

const express = require("express");
const bodyParser = require("body-parser");
const mongoose = require("mongoose");
const excel = require("exceljs");
const app = express();

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

mongoose.connect("mongodb://localhost:27017/NOC", {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error:"));
db.once("open", function () {
  console.log("We're connected to db!");
});

//Ticket schema
const ticketSchema = new mongoose.Schema({
  nodeA: {
    type: String,
    required: true,
  },
  nodeB: {
    type: String,
    required: true,
  },
  vendor: {
    type: String,
    required: true,
  },
  impact: {
    type: String,
    required: false,
  },
  route: {
    type: String,
    required: false,
  },
  timeDown: {
    type: String,
    required: true,
  },
  timeUp: {
    type: String,
    required: false,
  },
  TTR: {
    type: Number,
    required: false,
  },
  RDT: {
    type: Number,
    required: false,
  },
  siteIDWithPowerFailure: {
    type: String,
    required: false,
  },
  COF: {
    type: String,
    required: false,
  },
  action: {
    type: String,
    required: false,
  },
  byWhom: {
    type: String,
    required: true,
  },
  subSystem: {
    type: String,
    required: false,
  },
});

const ticket = mongoose.model("ticket", ticketSchema);

...

//downloadFile post route
app.post("/downloadFile", function (req, res) {

  //retrieve all tickets in database and store it as result array
  ticket.find({}, function (err, results) {
    let modifiedResult = results.reverse();

    if (err) {
      console.log("Could not retrieve data from database " + err);
      res.status(400);
    } else {
      //create a new excel workbook, a worksheet and set its properties.
      const workbook = new excel.Workbook();
      const sheet = workbook.addWorksheet("FIBRE_SWITCH", {
        properties: {
          tabColor: { argb: "FFc0000" },
        },
      });
      sheet.columns = [
        { header: "S/N", key: "S/N", width: 4 },
        { header: "TERMINAL A", key: "nodeA", width: 12 },
        { header: "TERMINAL B", key: "nodeB", width: 12 },
        { header: "VENDOR", key: "vendor", width: 9 },
        { header: "IMPACT", key: "impact", width: 12 },
        { header: "ROUTE", key: "route", width: 10 },
        {
          header: "FROM (DATE & TIME)",
          key: "timeDown",
          width: 12,
        },
        { header: "TO (DATE & TIME)", key: "timeUp", width: 12 },
        { header: "TTR (HRS)", key: "TTR (HRS)", width: 8 },
        { header: "RDT (HRS)", key: "RDT (HRS)", width: 8 },
        {
          header: "SITE ID OF OFC ISSUE WITH POWER",
          key: "siteIDWithPowerFailure",
          width: 9,
        },
        {
          header: "PROBLEM/SPECIFIC CAUSE",
          key: "COF",
          width: 14,
        },
        { header: "ACTION TAKEN", key: "action", width: 14 },
        { header: "BY WHOM", key: "byWhom", width: 12 },
        { header: "SUBSYSTEM", key: "subSystem", width: 12 },
      ];
      
      //write the data from the database to the excel worksheet cells.
      sheet.addRows(modifiedResult);
      res.setHeader(
        "Content-Type",
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
      );
      res.setHeader(
        "Content-Disposition",
        "attachment; filename=" + "OFC REPORT.xlsx"
      );

      //Download workbook.
      return workbook.xlsx.write(res).then(function () {
        console.log("Download success");
        res.status(200);
      });
    }
  });
});

app.listen(3001, function () {
  console.log("Server started at port 3001");
});

客户端代码 (REACT.JS)

App.js


import React from "react";
import "bootstrap/dist/css/bootstrap.min.css";
import Failure from "./Failure";

function App() {
  return (
    <div className="App">
      <Failure />
    </div>
  );
}

export default App;

Failure.js

import React, { useEffect, useState } from "react";

export default function Failure() {
  const [ticketList, setTicketList] = useState([]);
  const [formData, setFormData] = useState({
    nodeA: "",
    nodeB: "",
    vendor: "",
    timeDown: "",
    OFCsiteWithPowerIssue: "",
    byWhom: "",
  });

  useEffect(() => {
    if (ticketList.length === 0) {
      getAllTicket();
    }
  });

  const onChangeHandler = (e) => {
    const name = e.target.name;
    const value = e.target.value;
    setFormData((prevState) => {
      return {
        ...prevState,
        [name]: typeof value === "string" ? value.toUpperCase() : value,
      };
    });
  };

  const updateUi = (res) => {
    res &&
      res.json().then((data) => {
        const reversedData = data.reverse();
        setTicketList(reversedData);
      });
  };

  const registerFailure = (event, data) => {
    event.preventDefault();
    fetch("/registerFailure", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    })
      .then((response) => {
        if (response.status === 200) {
          updateUi(response);
        }
      })
      .catch((err) => {
        alert("Failed to register ticket, " + err);
      });
  };

  const deleteTicket = (_id) => {
    const id = { _id: _id };
    fetch("/deleteTicket", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(id),
    })
      .then((response) => {
        if (response.status === 200) {
          updateUi(response);
        }
      })
      .catch((err) => {
        alert("Failed to delete ticket, " + err);
      });
  };

  const getAllTicket = () => {
    fetch("/getTable", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
    })
      .then((response) => {
        if (response.status === 200) {
          updateUi(response);
        }
      })
      .catch((err) => {
        alert("Failed to fetch all ticket, " + err);
      });
  };

  //Download excel workbook handler function
  const downloadExcelFile = (e) => {
    e.preventDefault();
    fetch("/downloadFile", {
      method: "POST",
    })
      .then((response) => {
        if (response.status === 200) {
          console.log("Download started");
        }
      })
      .catch((err) => {
        alert("Failed to fetch all ticket, " + err);
      });
  };

  return (
    <>
      <h1>Register Failure</h1>
      <div className="row contact-form">
        <div className="col-sm-12">
          <form
            className="form-horizontal"
            onSubmit={(e) => registerFailure(e, formData)}
            method="POST"
          >
            <div className="form-group">
              <div className="col-sm-2">
                <input
                  type="text"
                  className="form-control"
                  id="fullname"
                  placeholder="Terminal A"
                  name="nodeA"
                  onChange={(e) => onChangeHandler(e)}
                  value={formData.nodeA}
                  required
                />
              </div>
              <div className="col-sm-2">
                <input
                  type="text"
                  className="form-control"
                  id="fullname"
                  placeholder="Terminal B"
                  name="nodeB"
                  onChange={(e) => onChangeHandler(e)}
                  value={formData.nodeB}
                  required
                />
              </div>
              <div className="col-sm-2">
                <input
                  type="text"
                  className="form-control"
                  id="fullname"
                  placeholder="Vendor"
                  name="vendor"
                  onChange={(e) => onChangeHandler(e)}
                  value={formData.vendor}
                  required
                />
              </div>
              <div className="col-sm-2">
                <input
                  type="text"
                  className="form-control"
                  id="fullname"
                  placeholder="From"
                  name="timeDown"
                  onChange={(e) => onChangeHandler(e)}
                  value={formData.timeDown}
                  required
                />
              </div>
              <div className="col-sm-2">
                <input
                  type="text"
                  className="form-control"
                  id="fullname"
                  placeholder="Site with power issue"
                  name="OFCsiteWithPowerIssue"
                  onChange={(e) => onChangeHandler(e)}
                  value={formData.OFCsiteWithPowerIssue}
                />
              </div>
              <div className="col-sm-2">
                <input
                  type="text"
                  className="form-control"
                  id="fullname"
                  placeholder="By Whom"
                  name="byWhom"
                  onChange={(e) => onChangeHandler(e)}
                  value={formData.byWhom}
                  required
                />
              </div>
            </div>
            <div className="form-group">
              <div className="col-sm-offset-2 col-sm-12">
                <button
                  type="submit"
                  className="btn btn-outline-success form-button"
                >
                  Submit
                </button>
              </div>
            </div>
          </form>
          <form
            className="form-horizontal"
            onSubmit={(e) => downloadExcelFile(e)}
            method="POST"
          >
            <div className="form-group">
              <div className="col-sm-offset-2 col-sm-12">
                <button
                  type="submit"
                  className="btn btn-outline-success form-button"
                >
                  Download Excel File
                </button>
              </div>
            </div>
          </form>
          {ticketList.length === 0 ? (
            <h1 style={{ textAlign: "center" }}>
              There are no entries to display
            </h1>
          ) : (
            <table className="table">
              <thead>
                <tr>
                  <th>Terminal A</th>
                  <th>Terminal B</th>
                  <th>Vendor</th>
                  <th>Impact</th>
                  <th>Route</th>
                  <th>From</th>
                  <th>To</th>
                  <th>TTR</th>
                  <th>RDT</th>
                  <th>SITE ID WITH POWER FAILURE</th>
                  <th>PROBABLE/SPECIFIC CAUSE</th>
                  <th>ACTION TAKEN</th>
                  <th>BY WHOM</th>
                  <th>SUB SYSTEM</th>
                  <th></th>
                </tr>
              </thead>
              <tbody>
                {ticketList.map((ticket) => {
                  return (
                    <tr key={ticket._id}>
                      <td>{ticket.nodeA}</td>
                      <td>{ticket.nodeB}</td>
                      <td>{ticket.vendor}</td>
                      <td>{ticket.impact}</td>
                      <td>{ticket.route}</td>
                      <td>{ticket.timeDown}</td>
                      <td>{ticket.timeUp}</td>
                      <td>{ticket.TTR}</td>
                      <td>{ticket.RDT}</td>
                      <td>{ticket.siteIDWithPowerFailure}</td>
                      <td>{ticket.COF}</td>
                      <td>{ticket.action}</td>
                      <td>{ticket.byWhom}</td>
                      <td>{ticket.subSystem}</td>
                      <td>
                        <button
                          className="btn btn-outline-danger"
                          onClick={() => deleteTicket(ticket._id)}
                        >
                          Delete Ticket
                        </button>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </div>
      </div>
    </>
  );
}

【问题讨论】:

    标签: javascript node.js reactjs express exceljs


    【解决方案1】:

    我认为正在发生的事情是 Fetch 正在吞噬响应的内容 - AFAIK 默认情况下它不会触发下载。

    您可以使用浏览器工具来验证这一点,以查看单击按钮时网络上返回的内容 - 但您的服务器端代码对我来说看起来很可靠。

    解决方案

    1. 将端点切换到get
    2. 将按钮切换为&lt;a href="/downloadFile" download&gt;Download File&lt;/a&gt;
    3. 点击链接 - 您应该会看到一个新标签页很快打开,然后在浏览器中开始下载
    4. 适当地设置a标签的样式

    注意事项

    这里的一个潜在问题是您将无法将错误处理逻辑放入您的 React 代码中(不支持在从 a 标记下载失败时执行任意代码)。如果你需要这种行为,你可以这样做:

    1. 使用 fetch,当单击按钮时检索文件内容(与您当前的示例完全相同)
      • 如果请求失败 - 显示错误状态
      • 如果请求成功 - 将有效负载推送到动态创建的a 标记中并以编程方式单击它see examples

    【讨论】:

    • 所以我将端点更改为“get”并将按钮切换为“a”标签,就像你建议的那样,但它导致下载失败。它打算下载的文件是“downloadFile.html”。我将 href 属性从“/downloadFile”更改为“localhost:3001/downloadFile”并单击“a”标签完美地触发了下载。但是,“localhost:3001”仅用于开发目的,并且在部署应用程序时肯定会发生变化。有没有办法修改“a”标签的 href 属性以正确定位“/downloadFile”端点?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-19
    • 2017-08-17
    • 2020-02-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多