You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在React JS前端下载Laravel后端生成的PDF文件

Laravel后端PDF在React前端的下载方案与最佳实践

问题背景

我有一个Laravel应用,已实现通过指定端点生成PDF的功能,后端逻辑正常,可直接访问对应路由获取PDF。现在需要在React前端实现PDF下载功能,想了解对接Laravel场景下的标准方法与最佳实践。

后端现有代码

Laravel控制器代码

public function mostReservationReport (Request $request) {
    $year = $request->input('year');

    $customer = DB::table('reservasi as r')
    ->join('customer as c', 'r.id_customer', '=', 'c.id')
    ->join('transaksi as t', 'r.id', '=', 't.id_reservasi')
    ->select('c.nama', DB::raw('COUNT(r.id) as jumlah'), DB::raw('SUM(t.total_pembayaran) as total'))
    ->whereYear('t.created_at', $year)
    ->groupBy('c.nama')
    ->orderByDesc('jumlah')
    ->limit(5)
    ->get();

    $now = now();

    $no = 1;
    $total = 0;
    foreach($customer as $data){
        $data->no = $no;
        $no++;
        $total += $data->total;
    }

    $pdf = PDF::loadView('top-five-customer', compact('year', 'customer', 'now', 'total'));
    return $pdf->stream('top-five-customer.pdf');
}

Blade模板代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Top 5 customer with the most reservasions</title>
    <link href="https://cdn.jsdelivr.net/npm/daisyui@3.9.4/dist/full.css" rel="stylesheet" type="text/css" media="all"/>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
      .table {
        font-family: arial, sans-serif;
        border-collapse: collapse;
        width: 100%;
      }

      .td, .th {
        border: 1px solid #dddddd;
        text-align: left;
        padding: 8px;
      }

      .table-borderless {
        border-collapse: collapse;
        border-spacing: 0;
        width: 100%;
        border: none !important;
      }

      .td-borderless {
        border: none !important;
      }

      .th-borderless {
        border: none !important;
      }

      .text-right {
        text-align: right;
      }

      .text-center {
        text-align: center;
      }

      .text-bold {
        font-weight: bold;
      }
      
      .w-10 {
        width: 10%;
      }

      .w-20 {
        width: 20%;
      }

      .w-30 {
        width: 30%;
      }

      .w-60 {
        width: 60%;
      }

      .w-80 {
        width: 400px;
      }

      .w-90 {
        width: 500px;
      }

      .w-full {
        width: 100%;
      }

      .center {
          text-align: center;
      }

      .center img {
          margin: auto;
          display: block;
      }
    </style>
</head>
<body>
  <div class="center">
    <img src="{{ public_path('storage/gah.jpg') }}" alt="logo">
    <h6>Jl. P. Mangkubumi No.18, Yogyakarta 55233</h6>
    <hr style="margin-top: 40px;margin-bottom: 10px;"/>
    <h4 style="text-align:center; font-weight: 900;">TOP 5 CUSTOMER WITH MOST RESERVATIONS REPORT</h5>
    <p style="text-align:left">Year : {{$year}}</p>
    
    <div className="overflow-x-auto" style="margin-top: 10px;">
      <table class="table w-full">
        <thead>
          <tr>
            <th class="th">No</th>
            <th class="th">Customer's Name</th>
            <th class="th">Reservation Count</th>
            <th class="th">Payment Total</th>
          </tr>
        </thead>
        <tbody>
          @foreach($customer as $item)
          <tr>
            <th class="th">{{$item->no}}</th>
            <td class="td">{{$item->nama}}</td>
            <td class="td">{{$item->jumlah}}</td>
            <td class="td">Rp {{ number_format($item->total, 2,'.', ',') }}</td>
          </tr>
          @endforeach
          <tr>
            <td></td>
            <td></td>
            <td class="text-bold">Total</td>
            <td class="text-bold">Rp {{ number_format($total, 2,'.', ',') }}</td>
          </tr>
        </tbody>
      </table>
    </div>
    <br/>
    <table class="table">
      <tbody>
        <tr>
          <td class="w-80"></td>
          <td class="w-10">Dicetak tanggal {{$now}}</th>
        </tr>
      </tbody>
    </table>
    <br/>
  </div>
</body>
</html>

前端实现方案

方案1:直接使用a标签跳转(最简GET请求场景)

如果PDF接口是GET请求,且不需要携带复杂参数或认证Token,直接用a标签跳转是最简便的方式。

function DownloadPDFButton() {
  const year = 2024; // 可根据业务逻辑动态获取
  return (
    <a
      href={`${process.env.REACT_APP_API_URL}/api/reports/most-reservation?year=${year}`}
      target="_blank"
      rel="noopener noreferrer"
      download="top-five-customer.pdf"
    >
      下载PDF报告
    </a>
  );
}

提示:若希望浏览器直接触发下载而非预览,可将后端代码中的return $pdf->stream('top-five-customer.pdf');改为return $pdf->download('top-five-customer.pdf');,该方法会自动添加Content-Disposition: attachment响应头。

方案2:使用Fetch请求Blob(适合带Token/POST请求场景)

如果需要携带认证Token,或用POST请求传递参数,需通过Fetch获取Blob对象后手动触发下载。

async function downloadPDF() {
  const year = 2024;
  try {
    const response = await fetch(`${process.env.REACT_APP_API_URL}/api/reports/most-reservation`, {
      method: 'POST', // 匹配后端路由定义的请求方法
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('token')}` // 携带认证Token
      },
      body: JSON.stringify({ year }) // POST请求参数
    });

    if (!response.ok) throw new Error('请求失败');

    const blob = await response.blob();
    const url = window.URL.createObjectURL(blob);
    
    // 创建临时a标签触发下载
    const a = document.createElement('a');
    a.href = url;
    a.download = 'top-five-customer.pdf';
    document.body.appendChild(a);
    a.click();
    
    // 清理资源避免内存泄漏
    window.URL.revokeObjectURL(url);
    document.body.removeChild(a);
  } catch (error) {
    console.error('下载失败:', error);
    alert('PDF下载失败,请稍后重试');
  }
}

// 按钮触发下载
function DownloadPDFButton() {
  return <button onClick={downloadPDF}>下载PDF报告</button>;
}

方案3:使用第三方库简化操作(推荐file-saver)

file-saver封装了Blob下载逻辑,代码更简洁易读。

先安装依赖:

npm install file-saver
# 或
yarn add file-saver

然后实现下载功能:

import { saveAs } from 'file-saver';

async function downloadPDF() {
  const year = 2024;
  try {
    const response = await fetch(`${process.env.REACT_APP_API_URL}/api/reports/most-reservation`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('token')}`
      },
      body: JSON.stringify({ year })
    });

    if (!response.ok) throw new Error('请求失败');

    const blob = await response.blob();
    saveAs(blob, 'top-five-customer.pdf');
  } catch (error) {
    console.error('下载失败:', error);
    alert('PDF下载失败,请稍后重试');
  }
}

function DownloadPDFButton() {
  return <button onClick={downloadPDF}>下载PDF报告</button>;
}

最佳实践

  1. 后端响应优化

    • 优先使用download()方法生成PDF,确保响应头包含Content-Disposition: attachment; filename="xxx.pdf",强制浏览器触发下载。
    • 明确设置Content-Type:return $pdf->download('top-five-customer.pdf')->header('Content-Type', 'application/pdf');
  2. 前端处理注意事项

    • 跨域配置:确保Laravel后端配置了CORS规则(可使用barryvdh/laravel-cors包),允许前端域名发起请求。
    • 错误处理:必须捕获请求失败的异常,给用户友好的提示信息。
    • 资源清理:使用URL.revokeObjectURL()释放Blob资源,避免内存泄漏。
    • 权限验证:若接口需要认证,务必在请求头中携带Token,确保权限校验通过。
  3. 参数传递规范

    • GET请求适合简单参数(如年份),直接拼在URL中即可。
    • POST请求适合复杂参数,通过请求体传递,同时后端需对应修改路由和请求方法。

内容的提问来源于stack exchange,提问作者Jumbo Mambo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 06:12:08