react-pdf分片获取服务器pdf文件,并进行身份验证
笔者最近想要在next项目中渲染pdf文件,了解到了react-pdf库,通过访问url来获取文件。但是当pdf文件较大时,会受制网速,使得体验不佳,于是想要优化。
实现逻辑:因为是用url去获取文件,所以会用延时的问题,如果文件过大,那么就会造成界面的长时间空白,这样肯定是不行的,那么就要进行切片获取——即每次请求只拿到部分数据,这样就会快很多。
使用分片操作,即后端允许分片返回数据,当前端访问该接口时候,知道可以分片返回,react-pdf就会自动调用分片获取,从而极大的优化获取速度,提升使用体验。
react-pdf基于pdfjs开发的,因为pdfjs是默认支持分片获取的,所以只有后端返回的接口符合分片结果,react-pdf就会自动进行分片请求,从而提升体验。
后端实现
后端使用fastapi
代码
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from middlewares.auth import AuthBearer
from fastapi.responses import StreamingResponse
import os
import re
pdf_router = APIRouter()
# get specific pdf
@pdf_router.get("/pdf", tags=["PDF"])
async def get_pdf_handler(
request: Request,
):
file_path = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"pdfs",
"example.pdf",
)
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail=f"{file_path} not found")
file_size = os.path.getsize(file_path)
range_header = request.headers.get("Range", None)
byte_range = 1024 * 1024
start = 0
end = file_size - 1
if range_header:
try:
match = re.search(r"bytes=(\d+)-(\d+)?", range_header)
if match:
start = int(match.group(1))
if match.group(2):
end = int(match.group(2))
else:
end = min(start + byte_range - 1, file_size - 1)
except ValueError:
raise HTTPException(status_code=416, detail="Invalid range header")
def iterfile(file_path, start, end):
try:
with open(file_path, "rb") as f:
f.seek(start)
while start <= end:
bytes_to_read = min(byte_range, end - start + 1)
data = f.read(bytes_to_read)
if not data:
break
start += len(data)
yield data
except IOError:
raise HTTPException(status_code=500, detail="Error reading file")
headers = {
"Content-Range": f"bytes {start}-{end}/{file_size}",
"Accept-Ranges": "bytes",
"Content-Length": str(end - start + 1),
"Content-Type": "application/pdf",
"Access-Control-Expose-Headers": "Accept-Ranges,Content-Range"
}
return StreamingResponse(
iterfile(file_path, start, end), headers=headers, status_code=206
)
由后端代码可以看出,返回的headers是包含Range头的,这就表明了可以分片获取,然后返回流式数据,即实现了笔者需求。
身份验证
笔者使用的后端服务是supabase,提供了auth验证
因为react-pdf接受的输入是url,而为了传输身份信息需要在请求头添加Bearer信息,这样的话在身份验证之间和pdf渲染之间就有矛盾。
笔者了解到可以先获取数据然后将数据转化为数据流传给react-pdf,但是笔者还是想要最直观的方式来解决该问题。
于是考虑从输入到react-pdf中的url入手,既然单纯的url无法实现身份信息传输,那么就借由next的api,进行一个请求转发,在转发的过程中添加身份信息,并且保证了在react-pdf中的输入依然是一个url。
代码
import { NextRequest, NextResponse } from "next/server";
import { ApiPath } from "@/app/constant";
async function handle(
req: NextRequest,
{ params }: { params: { query: string[] } },
) {
const [pdfFile, access_token] = params.query;
req.headers.set("Authorization", `Bearer ${access_token}`);
const fetchOptions: RequestInit = {
method: req.method,
headers: req.headers,
};
const baseUrl = process.env.NEXT_PUBLIC_BACKEND_URL;
const path = ApiPath.Pdf;
const fetchUrl = `${baseUrl}${path}?query=${pdfFile}`;
try {
const res = await fetch(fetchUrl, fetchOptions);
const responseHeaders = new Headers(res.headers);
responseHeaders.set("Content-Type", "application/pdf");
return new NextResponse(res.body, {
status: res.status,
statusText: res.statusText,
headers: responseHeaders,
});
} catch (e) {
console.error("[pdf api]", e);
}
}
export const GET = handle;
该代码写在app/api/pdf/[...query]/route.ts
中,之后通过访问本地的/api/pdf端口就可以实现pdf获取,并且实现了身份信息的添加。
但是可以看出,access_token
是传输进来,因为ts文件无法获取supabase的token,为了达到身份信息的要求,无奈摈弃了实现的方式的优雅,在访问/api/pdf端口的时候传入了pdfFile和accessi_token。(如果有读者有更好的实现反方式麻烦教授一下)
参考文章
github:react-pdf-p2