React实现PDF加载编辑后保存至NestJS后端的问题求助
解决React前端修改PDF表单后无法保存到NestJS后端的问题
核心问题分析
当前代码存在两个关键问题导致保存失败:
- 用
<embed>标签预览PDF时,用户在原生PDF查看器中的修改无法同步到pdf-lib的PDFDocument实例,最终保存的还是原始PDF内容 - 保存时将二进制PDF数据转成JSON数组传输,不仅效率低下,后端也难以正确解析还原
前端代码修复方案
我们将替换预览交互逻辑,在React中渲染与PDF表单绑定的可编辑组件,实时同步用户修改到PDFDocument实例,同时优化请求方式用FormData传输二进制数据:
import React, { useRef, useState } from "react" import { PDFDocument } from "pdf-lib" const PDFViewer = () => { const domain = process.env.REACT_APP_API_URL const pdfUrl = `${domain}/example.pdf` const [pdfView, setPdfView] = useState(null) const pdfDocRef = useRef(null) // 存储PDF表单域对应的值,绑定到React表单 const [formValues, setFormValues] = useState({}) // 加载PDF并初始化表单数据 const loadPDF = async () => { try { const response = await fetch(pdfUrl) if (!response.ok) throw new Error("Failed to fetch PDF") const pdfBytes = await response.arrayBuffer() const pdfDoc = await PDFDocument.load(new Uint8Array(pdfBytes)) pdfDocRef.current = pdfDoc // 提取PDF表单域初始值 const form = pdfDoc.getForm() const fields = form.getFields() const initialValues = {} fields.forEach(field => { const fieldName = field.getName() if (field.constructor.name === "PDFTextField") { initialValues[fieldName] = field.getText() } else if (field.constructor.name === "PDFDropdown") { initialValues[fieldName] = field.getSelected() } else if (field.constructor.name === "PDFRadioGroup") { initialValues[fieldName] = field.getSelected() } }) setFormValues(initialValues) // 生成预览用的Blob URL const modifiedBytes = await pdfDoc.save() const blob = new Blob([modifiedBytes], { type: "application/pdf" }) setPdfView(URL.createObjectURL(blob)) } catch (error) { console.error("Error loading PDF:", error) } } // 将React表单值同步到PDFDocument实例 const syncFormToPDF = () => { if (!pdfDocRef.current) return const form = pdfDocRef.current.getForm() Object.entries(formValues).forEach(([fieldName, value]) => { const field = form.getField(fieldName) if (!field) return if (field.constructor.name === "PDFTextField") { field.setText(value) } else if (field.constructor.name === "PDFDropdown") { field.select(value) } else if (field.constructor.name === "PDFRadioGroup") { field.select(value) } }) } // 保存修改后的PDF到服务器 const savePDF = async () => { if (!pdfDocRef.current) return // 先同步表单值到PDF实例 syncFormToPDF() const modifiedBytes = await pdfDocRef.current.save() const pdfBlob = new Blob([modifiedBytes], { type: "application/pdf" }) try { const formData = new FormData() formData.append("pdfFile", pdfBlob, "example.pdf") const response = await fetch(pdfUrl, { method: "PUT", body: formData, }) if (response.ok) { console.log("PDF saved successfully") // 更新预览内容 setPdfView(URL.createObjectURL(pdfBlob)) } else { console.error("Failed to save PDF") } } catch (error) { console.error("Error saving PDF:", error) } } // 渲染与PDF绑定的表单组件(需替换为实际PDF表单域名称) const renderFormFields = () => { return ( <div style={{ margin: "15px 0" }}> {/* 示例输入框 */} <div style={{ marginBottom: "10px" }}> <label>姓名:</label> <input type="text" value={formValues["username"] || ""} onChange={(e) => setFormValues(prev => ({...prev, username: e.target.value}))} style={{ marginLeft: "8px" }} /> </div> {/* 示例下拉框 */} <div style={{ marginBottom: "10px" }}> <label>部门:</label> <select value={formValues["department"] || ""} onChange={(e) => setFormValues(prev => ({...prev, department: e.target.value}))} style={{ marginLeft: "8px" }} > <option value="技术部">技术部</option> <option value="市场部">市场部</option> </select> </div> {/* 示例单选组 */} <div> <label>性别:</label> <input type="radio" name="gender" value="男" checked={formValues["gender"] === "男"} onChange={(e) => setFormValues(prev => ({...prev, gender: e.target.value}))} style={{ marginLeft: "8px" }} /> <span>男</span> <input type="radio" name="gender" value="女" checked={formValues["gender"] === "女"} onChange={(e) => setFormValues(prev => ({...prev, gender: e.target.value}))} style={{ marginLeft: "8px" }} /> <span>女</span> </div> {/* 其余表单域按此格式添加 */} </div> ) } return ( <div> <button onClick={loadPDF}>Load PDF</button> <button onClick={savePDF} style={{ marginLeft: "10px" }}>Save PDF</button> {/* 渲染可编辑表单,用户在此输入修改 */} {Object.keys(formValues).length > 0 && renderFormFields()} {/* PDF预览区域 */} <div style={{ marginTop: "20px" }}> {pdfView && ( <embed title="PDF Viewer" src={pdfView} width="100%" height="500px" style={{ border: "none" }} /> )} </div> </div> ) } export default PDFViewer
NestJS后端接口实现
方案1:保存到服务器文件系统
import { Controller, Put, UploadedFile, UseInterceptors, Res } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { diskStorage } from 'multer'; import { extname } from 'path'; import { Response } from 'express'; @Controller() export class PdfController { @Put('example.pdf') @UseInterceptors(FileInterceptor('pdfFile', { storage: diskStorage({ destination: './uploads', // 提前创建此目录并配置读写权限 filename: (req, file, cb) => { cb(null, `example${extname(file.originalname)}`); // 覆盖原文件 }, }), limits: { fileSize: 5 * 1024 * 1024 }, // 限制文件大小为5MB })) async updatePdf(@UploadedFile() file: Express.Multer.File, @Res() res: Response) { if (!file) { return res.status(400).send('No file uploaded'); } return res.status(200).send('PDF updated successfully'); } }
方案2:保存到PostgreSQL数据库
先创建存储PDF的表:
CREATE TABLE pdf_files ( id SERIAL PRIMARY KEY, filename VARCHAR(255) NOT NULL UNIQUE, data BYTEA NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
编写服务层代码:
// pdf.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { PdfFile } from './pdf-file.entity'; @Injectable() export class PdfService { constructor( @InjectRepository(PdfFile) private pdfRepository: Repository<PdfFile>, ) {} async updatePdf(filename: string, data: Buffer) { let pdf = await this.pdfRepository.findOne({ where: { filename } }); if (pdf) { pdf.data = data; pdf.updated_at = new Date(); } else { pdf = this.pdfRepository.create({ filename, data }); } return this.pdfRepository.save(pdf); } async getPdf(filename: string) { return this.pdfRepository.findOne({ where: { filename } }); } }
编写控制器代码:
// pdf.controller.ts import { Controller, Put, UploadedFile, UseInterceptors, Get, Res } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { PdfService } from './pdf.service'; import { Response } from 'express'; @Controller() export class PdfController { constructor(private readonly pdfService: PdfService) {} @Put('example.pdf') @UseInterceptors(FileInterceptor('pdfFile')) async updatePdf(@UploadedFile() file: Express.Multer.File) { await this.pdfService.updatePdf('example.pdf', file.buffer); return { message: 'PDF updated successfully' }; } @Get('example.pdf') async getPdf(@Res() res: Response) { const pdf = await this.pdfService.getPdf('example.pdf'); if (!pdf) { return res.status(404).send('PDF not found'); } res.setHeader('Content-Type', 'application/pdf'); res.send(pdf.data); } }
关键注意事项
- React表单的名称必须与PDF中的表单域名称完全一致,否则无法同步修改
- 服务器端的
uploads目录需提前创建,并确保进程有读写权限 - 生产环境需添加文件格式验证、大小限制等安全措施
- 若用PostgreSQL存储二进制数据,注意
BYTEA类型默认最大存储1GB
内容的提问来源于stack exchange,提问作者Sudhakar J
相关产品推荐
相关产品推荐

