ToolJet PDF 组件完全实践指南:通过 URL 与 Base64 嵌入、缩放控制及下载实现的 PDF 文档展示方案
本文基于 ToolJet 官方文档 docs/docs/widgets/pdf.md 与前端源码 frontend/src/AppBuilder/Widgets/PDF.jsx、组件配置 frontend/src/AppBuilder/WidgetManager/widgets/pdf.js,系统讲解 ToolJet PDF 组件的嵌入方式(URL / Base64 Data URL)、全部配置属性的源码级默认值、翻页控制与下载功能的底层实现,以及浏览器兼容性边界。读完本文,你可以独立在 ToolJet 应用中嵌入 PDF 文档,理解其渲染管线(react-pdf + pdf.js),并掌握从数据库加载 Base64 编码 PDF 的完整实战链路。
组件概览与渲染管线
PDF 组件用于在 ToolJet 画布中嵌入 PDF 文件,支持两种数据来源:直接提供 PDF 的 URL,或通过 Base64 编码(Data URL 形式)提供文件内容。组件的定位描述为 "Embed PDF documents"(嵌入 PDF 文档),这一点可以从组件注册配置 frontend/src/AppBuilder/WidgetManager/widgets/pdf.js#L1-L5 中确认:
export const pdfConfig = {
name: 'PDF',
displayName: 'PDF',
description: 'Embed PDF documents',
component: 'PDF',
// ...
};
从源码结构看,组件的渲染管线由两部分组成:
- react-pdf(frontend/package.json 中声明版本为
^10.1.0)提供<Document>与<Page>两个 React 组件,负责 PDF 文档加载与逐页渲染; - pdfjs-dist(固定版本
5.3.93)作为底层 PDF 解析引擎,其 Web Worker 在组件加载时被显式初始化:
// PDF.jsx
import { Document, Page, pdfjs } from 'react-pdf';
// PDF.js v5 worker setup for react-pdf v10: provide a URL string to the worker bundle
pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
见 frontend/src/AppBuilder/Widgets/PDF.jsx#L2-L13。Worker 的引入方式(new URL(..., import.meta.url))对 Webpack 5、Vite 等打包器均兼容,保证了 PDF 解析在独立线程中进行,不阻塞 UI 主线程。
组件默认尺寸为宽 20 网格 × 高 640 像素(defaultSize),并注册于 frontend/src/AppBuilder/_helpers/editorHelpers.js#L90 的懒加载映射中(const PDF = lazy(() => import('@/AppBuilder/Widgets/PDF'))),即按需加载,不影响应用构建体积。
两种嵌入方式:URL 与 Base64 Data URL
方式一:直接填写 File URL
在 Inspector 中找到 File URL 属性(属性键为 url,类型为 code,即支持 {{fx}} 动态表达式),填入 PDF 文件的 URL 即可。组件配置中该属性带有一个默认示例值(Wikipedia 上的示例 PDF),见 frontend/src/AppBuilder/WidgetManager/widgets/pdf.js#L7-L14:
url: {
type: 'code',
displayName: 'File URL',
validation: {
schema: { type: 'string' },
defaultValue: 'https://upload.wikimedia.org/wikipedia/commons/general.pdf',
},
},
由于属性类型是 code,URL 可以是动态表达式。例如从查询结果取值:
{{queries.getFiles.data[0].url}}
方式二:Base64 Data URL
文档明确说明:Base64 格式同样受支持,输入必须以 data:application/pdf;base64, 前缀开头,后接 Base64 编码字符串。典型的构造方式是拼接查询返回的 Base64 数据:
{{'data:application/pdf;base64,' + queries.getFiles.data[0].pdf}}
官方 how-to 文档 docs/docs/how-to/loading-image-pdf-from-db.md 给出了完整链路:用 Filepicker 组件上传 PDF(将 Accept file types 设为 {{"pdf/*"}}),通过其暴露变量 {{components.pdfPicker.file[0].base64Data}} 取 Base64 串,写入 ToolJet Database 表(varchar 列),再查询取回后拼成 Data URL 填入 PDF 组件的 File URL 属性。该文档中使用的示例为:
{{'data:pdf;base64,' + queries.getFiles.data[0].pdf}}
提示:文档主页面推荐的 MIME 前缀为
data:application/pdf;base64,;浏览器对data:pdf;base64,这类简写前缀也能解析,两者在 Data URL 机制上等价,建议以官方 PDF 组件文档给出的data:application/pdf;base64,为准。
组件内部对两种来源不做区分——URL 字符串原样传给 react-pdf 的 <Document file={url}>,见 frontend/src/AppBuilder/Widgets/PDF.jsx#L144-L151。当 url 为空字符串时,组件渲染占位提示 No PDF file specified(PDF.jsx#L202),便于在调试期发现未配置问题。
核心配置属性详解(含源码级默认值)
文档的 Properties 小节列出了 4 个核心属性;结合 frontend/src/AppBuilder/WidgetManager/widgets/pdf.js#L15-L29 的配置,可以补全每个属性的属性键、控件类型与默认值:
| 属性(Inspector 显示名) | 属性键 | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
| File URL | url |
code | 示例 URL(见上) | PDF 文件地址;支持 data:application/pdf;base64, 前缀的 Base64 格式 |
| Scale page to width | scale |
toggle | true |
自动调整 PDF 以铺满组件整个宽度 |
| Show page controls | pageControls |
toggle | true |
开启后悬停/底部显示上一页、下一页按钮与页码 |
| Show download button | showDownloadOption |
toggle | true |
显示/隐藏 PDF 组件上的 Download 按钮 |
其中 Scale page to width 的底层逻辑在渲染函数中可以直接看到(PDF.jsx#L152-L160):开启 scale 时,<Page> 只传 width={width - 12}(组件宽度扣除 12px 边距),高度自适应页面比例,从而让每页横向铺满组件;关闭 scale 时则传固定的 height,按组件高度渲染,页面可能不占满宽度。
<Page
pageNumber={index + 1}
width={scale ? width - 12 : undefined}
height={scale ? undefined : height}
key={`page_${index + 1}`}
inputRef={(el) => (pageRef.current[index] = el)}
/>
文档原文中对各属性的描述如下,与源码行为一致:
- File URL:可在此属性下输入要展示的 PDF 文件 URL;也支持 Base64 格式,输入需加
data:application/pdf;base64,前缀。 - Scale page to width:自动将 PDF 调整为填充组件整个宽度。
- Show page controls:默认情况下,鼠标悬停在 PDF 文件上时,会显示上一页/下一页按钮及页码;该开关可将其打开或关闭。
- Show the download:PDF 组件上的 Download 按钮允许下载 PDF 文件;默认开启,关闭后从组件上移除 Download 按钮。
翻页控制与页码同步的源码实现
文档说"悬停时显示上一页/下一页按钮与页码",源码进一步揭示了其完整机制(frontend/src/AppBuilder/Widgets/PDF.jsx#L50-L82):
- IntersectionObserver 页码感知:组件挂载后为
#pdf-wrapper内的每个.react-pdf__Page节点注册IntersectionObserver(threshold: 0.7,即页面元素 70% 可见时才认为"进入视口"),当用户滚动容器时,自动把当前页码同步到底部状态栏的X of N显示中:
const options = {
root: document.querySelector('#pdf-wrapper'),
rootMargin: '0px',
threshold: 0.7,
};
- 按钮翻页:底部控制条包含
‹(上一页)与›(下一页)两个按钮及{pageNumber} of {numPages}页码文本。点击按钮触发updatePage(offset),通过pageRef中缓存的各页 DOM 引用滚动到目标页顶部,并更新pageNumber状态;在首页时上一页按钮disabled,末页时下一页按钮disabled:
const updatePage = useCallback(
(offset) => {
const { offsetTop } = pageRef.current[pageNumber + offset - 1];
documentRef.current.scrollTop = offsetTop;
setButtonClick(true);
setPageNumber((prevPageNumber) => (prevPageNumber || 1) + offset);
},
[pageNumber]
);
- 防抖处理滚动反馈:手动点击翻页按钮后,
handleScroll会将hasButtonClicked置位,避免按钮触发的程序化滚动被误判为用户滚动,保证页码显示与交互意图一致(PDF.jsx#L177-L183)。
控制条整体只在 !error && !pageLoading 且(showDownloadOption || pageControls)为真时渲染(PDF.jsx#L204-L209)——即只有当文档加载成功且至少开启了其中一项功能时才出现底部栏;两项都关闭时底部栏完全不显示。
Download 按钮的实现细节
Show download button 开关控制下载按钮显隐,默认开启(defaultValue: true,pdf.js#L25-L29)。点击后的实现是标准的 fetch → Blob → Object URL → 虚拟 <a> 触发下载 流程(PDF.jsx#L164-L175):
async function downloadFile(url, pdfName) {
const pdf = await fetch(url);
const pdfBlog = await pdf.blob();
const pdfURL = URL.createObjectURL(pdfBlog);
const anchor = document.createElement('a');
anchor.href = pdfURL;
anchor.download = pdfName; // 文件名取自组件名(componentName)
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(pdfURL);
}
两个可留意的实现事实:
- 下载文件名使用组件名(
componentName)而非原始 URL 中的文件名,因此建议把 PDF 组件重命名为有业务含义的名称(如displayPDF),下载产物即为displayPDF; - 下载走
fetch,因此对跨域 URL 需要有 CORS 支持,或者使用同域/允许跨域的资源;Base64 Data URL 场景下无跨域问题。
密码保护 PDF 的处理
源码中实现了一套文档未展开但实际存在的能力:针对加密 PDF 的密码提示流程(PDF.jsx#L6-L10、PDF.jsx#L97-L121)。组件通过 react-pdf 的 onPassword 回调区分两种原因:
NEED_PASSWORD:弹出prompt('Enter the password to open this PDF file.')让用户输入密码;INCORRECT_PASSWORD:弹出prompt('Invalid password. Please try again.')让用户重试。
若用户直接关闭了密码输入框(password === null),组件进入 "Password prompt closed" 状态并渲染一个 Retry 按钮,点击后重置状态重新走加载流程(PDF.jsx#L123-L143)。此外,url 变化时会自动重置 isPasswordPromptClosed,保证切换文件后密码流程不残留。
通用能力:Tooltip、设备可见性与样式
Tooltip
如需在用户悬停 PDF 组件时显示说明文字,在 Tooltip 属性下输入文本即可(文档 General 小节原文)。
Devices(设备可见性)
配置文件中注册了两个设备切换属性(pdf.js#L35-L38),文档 Devices 小节描述如下:
| 属性 | 说明 | 取值方式 |
|---|---|---|
| Show on desktop | 使组件在桌面视图中可见 | 可直接拨动开关,或点击 fx 输入逻辑表达式动态控制 |
| Show on mobile | 使组件在移动视图中可见 | 同上 |
值得注意的默认值:新建组件时 showOnDesktop 默认为 {{true}},而 showOnMobile 默认为 {{false}}(pdf.js#L64-L68),即移动端默认不显示,如需移动端可见请显式开启。
Styles(样式)
文档 Styles 小节列出 Visibility:控制组件可见性,可开关拨动或点击 fx 输入逻辑表达式动态配置。源码中 PDF 组件实际还注册了两个样式属性(pdf.js#L40-L62):
| 属性 | 属性键 | 类型 | 默认值 |
|---|---|---|---|
| Visibility | visibility |
toggle | true |
| Border color | borderColor |
colorSwatches | var(--cc-weak-border) |
| Border radius | borderRadius |
numberInput | 6 |
渲染时 visibility 为假直接以 display: none 隐藏外层容器,边框颜色与圆角应用到内层容器(PDF.jsx#L185-L194):
<div style={{ display: visibility ? 'flex' : 'none', width: width - 3, height, boxShadow }} data-cy={dataCy}>
<div
className="d-flex position-relative h-100 flex-column"
style={{
margin: '0 auto',
overflow: 'hidden',
borderRadius: `${borderRadius}px`,
border: `1px solid ${borderColor}`,
}}
>
事件与暴露变量
文档明确说明了两点"空集"事实,组件配置文件与之完全对应(events: {} 与 exposedVariables: {},pdf.js#L39、pdf.js#L63):
- Component Specific Actions (CSA):当前没有实现用于规约或控制该组件的组件专属动作;
- Exposed Variables:当前该组件没有暴露变量——因此你无法像 Filepicker 那样通过
{{components.xxx.file}}读取 PDF 组件内部状态,动态交互只能依赖其入参(url等属性的 fx 表达式)与通用事件体系。
浏览器兼容性
文档给出的兼容性矩阵(仅列出官方声明值,以文档为准):
| Browser | Version |
|---|---|
| Chrome | 92 或更高 |
| Edge | 92 或更高 |
| Safari | 15.4 或更高 |
| Firefox | 90 或更高 |
文档同时说明:若 PDF 组件被集成到你的应用中,它只会在上述支持的浏览器中渲染。适用前提:这是针对组件运行时浏览器环境的约束,构建/部署环境与编辑器环境不受此表限制;若目标终端用户存在低版本浏览器,应通过 Devices 开关或 Visibility 动态表达式做降级隐藏策略。
实战案例:从数据库加载 Base64 PDF 的完整链路
将本文前面各节串联起来,官方 docs/docs/how-to/loading-image-pdf-from-db.md 给出了"上传 → 存储 → 查询 → 展示"的端到端示例,关键步骤为:
- 在 ToolJet Database 建表
testDB,新增pdf、image两个varchar列; - 拖入两个 Filepicker 组件(
imagePicker、pdfPicker),将pdfPicker的 Accept file types 改为{{"pdf/*"}}; - 新建查询
uploadFiles(ToolJet Database /testDB/ Create Row),列值分别写{{components.pdfPicker.file[0].base64Data}}与{{components.imagePicker.file[0].base64Data}}; - 新建 Button 组件,配置
On click→Run Query→uploadFiles,点击即把 Base64 串写入数据库; - 新建查询
getFiles(List rows)并开启 Run this query on application load?; - 拖入 PDF 组件,在 File URL 中填入:
{{'data:application/pdf;base64,' + queries.getFiles.data[0].pdf}}
该方案的价值在于:PDF 文件不依赖外部静态资源托管,数据随应用数据一起入库,配合组件的 scale、pageControls、showDownloadOption 属性即可完成内部工具中合同、报表、发票等文档的在线预览场景。
关键文件索引
| 内容 | 路径 |
|---|---|
| PDF 组件官方文档(本文主体来源) | docs/docs/widgets/pdf.md |
| Base64 图片/PDF 加载实战指南 | docs/docs/how-to/loading-image-pdf-from-db.md |
| 组件渲染实现(react-pdf + pdf.js 5.x Worker、翻页、下载、密码提示) | frontend/src/AppBuilder/Widgets/PDF.jsx |
| 组件注册配置(属性、默认值、默认尺寸、设备开关、样式) | frontend/src/AppBuilder/WidgetManager/widgets/pdf.js |
| 懒加载注册入口 | frontend/src/AppBuilder/_helpers/editorHelpers.js#L90 |
| 依赖版本(react-pdf ^10.1.0 / pdfjs-dist 5.3.93) | frontend/package.json |
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00