Invoify 实战指南:用 Next.js + TypeScript + Shadcn 构建的 Web 发票生成器

原创2026-10-01 12:53:301,381 阅读
文章标签:前端后端企业应用

Invoify 实战指南:用 Next.js + TypeScript + Shadcn 构建的 Web 发票生成器

导读

Invoify 是一个开源的 Web 端发票生成应用,以 Next.js、TypeScript 与 Shadcn UI 为核心技术栈,通过表单化的向导流程让用户快速创建、保存、导出专业发票。本文将围绕项目 README.md 的文档骨架,结合仓库内的真实源码(Zod 校验 Schema、Puppeteer PDF 生成服务、Nodemailer 邮件发送服务、多格式导出服务等)展开讲解,读完本文你将领会其表单校验体系、PDF 生成链路、邮件发送配置与本地化布局,并能够在本地把它完整跑起来。

项目概览与技术选型

Invoify 是一个基于 Web 的发票生成器,定位是“快速创建并管理专业发票”。README 声明其基于 Next.js 13 构建,而当前仓库 package.json 中实际锁定的版本为 next: 15.3.8,读者在阅读旧文档与新版依赖时需注意这一差异。

核心技术栈

技术 用途 仓库证据
Next.js React 框架,提供 SSR 与客户端导航 package.json
TypeScript JavaScript 超集,静态类型约束 types.ts
Shadcn UI 基于 Radix 的 UI 组件库 components/ui
Tailwind CSS 原子化 CSS 框架 tailwind.config.js
React Hook Form 表单状态管理 InvoiceForm.tsx
Zod TypeScript 优先的 Schema 校验 lib/schemas.ts
Puppeteer 无头浏览器 PDF 生成 generatePdfService.ts
Nodemailer Node.js 邮件发送 sendPdfToEmailService.ts
Lucide Icons 可定制 SVG 图标集 package.json

除此之外,package.json 还揭示了若干关键依赖:react-hook-form 与 @hookform/resolvers(表单 + Zod 桥接)、@react-email/*(邮件模板渲染)、react-signature-canvas(手写签名)、react-use-wizard(分步向导)、xlsx/xml2js/@json2csv/node(多格式导出)、number-to-words(金额转大写)以及 next-intl(国际化)。

Invoify 应用界面截图

上图是 README.md 中展示的应用主界面,发票编辑表单与实时预览同屏呈现。

功能路线图全解

README 用一份勾选清单勾勒了项目当前状态,以下按“已完成”与“规划中”分类说明,并结合源码给出实现位置。

已完成功能

  • 快速创建发票:通过表单向导快速生成发票。表单被拆分为 BillFrom、BillTo、InvoiceDetails、Items、Charges 等分节组件,见 app/components/invoice/form/sections。
  • 本地保存与检索:发票数据存入浏览器本地存储(invoify:invoiceDraft,见 lib/variables.ts),配合 InvoiceLoaderModal 从保存列表加载,对应组件 app/components/modals/invoice。
  • 灵活下载:可直接下载 PDF,也可通过邮件发送 PDF 附件,对应 app/api/invoice 下的 generate 与 send 两个接口。
  • 模板选择:内置 2 套发票模板,即 InvoiceTemplate1 与 InvoiceTemplate2,位于 app/components/templates/invoice-pdf,由 lib/helpers.ts 的 getInvoiceTemplate() 按 pdfTemplate 字段动态导入。
  • 实时预览:编辑表单时右侧 LivePreview 实时同步渲染,组件见 LivePreview.tsx。
  • 多格式导出:支持 JSON、CSV、XML(README 亦列出 XLSX),服务端实现见 exportInvoiceService.ts。

规划中功能

  • I18N 支持:README 将其标记为未完成,但从仓库现状看,i18n/locales 已存在 16 个语言文件,lib/variables.ts 中 LOCALES 也声明了 14 种语言(含中文 zh-CN、日文 ja 等),并已引入 next-intl 依赖。可以推断国际化基础设施已经铺好,但 README 层面仍将其列为 roadmap 未完成项。
  • 主题化模板:为发票选择主题色,尚未实现。
  • 自定义输入项:允许用户定义默认构建器之外的字段(如 VAT 税号),尚未实现。
  • 行项目独立税:为单个行项目添加独立于总税的税额,尚未实现。

快速开始:本地安装与运行

环境要求

按 README 的说明,本地运行只需 Node.js 与 npm。若要体验“邮件发送 PDF”功能,还需准备一个可用的邮箱凭据(源码层面 Nodemailer 固定使用 Gmail 的 SMTP service,见下文)。

安装步骤

  1. 克隆仓库并进入目录:

    git clone https://github.com/al1abb/invoify.git
    cd invoify
    

    当前仓库镜像位于 GitHub_Trending/in/invoify,也可基于该目录直接进行本地开发。

  2. 安装依赖:

    npm install
    
  3. 创建 .env.local 并写入邮件配置(仅邮件发送功能需要):

    NODEMAILER_EMAIL=your_email@example.com
    NODEMAILER_PW=your_email_password
    
  4. 启动开发服务器:

    npm run dev
    
  5. 浏览器访问 http://localhost:3000。

环境变量的作用域与校验逻辑

仓库在 lib/variables.ts 中读取这两个变量:NODEMAILER_EMAIL = process.env.NODEMAILER_EMAIL、NODEMAILER_PW = process.env.NODEMAILER_PW。在 sendPdfToEmailService.ts 中,isEmailConfigured() 会在两者同时存在时返回 true;否则服务直接抛错并提示 “Email service not configured”。也就是说,即使不配置邮件,发票的创建、保存、导出与 PDF 下载功能依然可用,只有调用 /api/invoice/send 时会受限。

此外 package.json 还提供了几个常用脚本:

脚本 命令 说明
dev next dev 启动开发服务器
build next build 生产构建
start next start 运行生产构建
lint next lint 代码检查
analyze cross-env ANALYZE=true next build 借助 @next/bundle-analyzer 分析打包体积

核心链路源码解读

1. 表单数据模型:React Hook Form + Zod 双层校验

发票表单的数据结构由 lib/schemas.ts 中的 Zod Schema 定义,并被 types.ts 通过 z.infer 导出为 TypeScript 类型:

export type InvoiceType = z.infer<typeof InvoiceSchema>;
export type ItemType = z.infer<typeof ItemSchema>;

整体模型分三层:sender(发票发送方)、receiver(接收方)、details(发票明细),其中 details 又包含行项目 items、支付信息、税/折扣/运费、签名、模板编号等。字段校验规则示例:

  • name:2~50 个字符;
  • address:2~70 个字符;
  • email:必须为合法邮箱且长度 5~30;
  • quantity:必须大于 0;
  • unitPrice:必须大于 0 且不超过 Number.MAX_SAFE_INTEGER;
  • invoiceDate/dueDate:z.date() 并转换为 en-US 长日期字符串(复用 lib/variables.ts 的 DATE_OPTIONS)。

同时 lib/variables.ts 的 FORM_DEFAULT_VALUES 提供了表单默认值(默认货币 USD、默认模板 pdfTemplate: 1、默认一个空行项目),lib/variables.ts 的 FORM_FILL_VALUES 则是一份仅供开发测试的自动填充数据。

2. PDF 生成:React 组件序列化 + 无头浏览器打印

PDF 生成链路位于 generatePdfService.ts,核心思路是“把 React 组件渲染成 HTML,再交给无头 Chromium 打印为 PDF”:

  1. 按 body.details.pdfTemplate 通过 getInvoiceTemplate()(lib/helpers.ts)动态导入对应模板组件;
  2. 用 ReactDOMServer.renderToStaticMarkup() 将组件序列化为静态 HTML;
  3. 根据环境选择浏览器内核:
    • 生产环境:使用 puppeteer-core + @sparticuz/chromium,启动参数包含 --disable-dev-shm-usage 与 --ignore-certificate-errors,适配 Serverless 环境;
    • 开发/其他环境:直接使用完整版 puppeteer,参数为 --no-sandbox、--disable-setuid-sandbox;
  4. 注入 Tailwind CDN 样式,等待 networkidle0/load/domcontentloaded 后调用 page.pdf() 输出 A4 PDF,并设置 printBackground: true、preferCSSPageSize: true;
  5. 通过 finally 块确保关闭所有页面与浏览器实例,避免资源泄漏。

对应 API 路由 app/api/invoice/generate/route.ts 声明了 runtime = "nodejs" 与 maxDuration = 60,说明该端点需要在 Node.js 运行时内完成浏览器启动与渲染,超时上限为 60 秒。

3. 邮件发送:Nodemailer + React Email 模板

app/api/invoice/send/route.ts 接收 multipart/form-data,其核心逻辑在 sendPdfToEmailService.ts:

  • 使用 Gmail SMTP 创建 transporter(service: "gmail");
  • 从表单读取 email、invoicePdf(File)、invoiceNumber 三个字段;
  • 用 @react-email/render 将 SendPdfEmail.tsx 渲染为 HTML 正文,主题为 Invoice Ready: #${invoiceNumber};
  • 通过 lib/helpers.ts 的 fileToBuffer() 将上传的 PDF 文件转为 Buffer,作为 invoice.pdf 附件发出。

4. 多格式导出:JSON / CSV / XML 的统一服务

app/api/invoice/export/route.ts 根据 ?format= 查询参数分发到 exportInvoiceService.ts:

format 实现 响应头
JSON JSON.stringify(body) application/json,下载名 invoice.json
CSV @json2csv/node 的 AsyncParser 解析整个发票对象 text/csv,下载名 invoice.csv
XML xml2js 的 Builder.buildObject() 将 JSON 转为 XML application/xml,下载名 invoice.xml

值得留意的是:README 的 roadmap 将 XLSX 列为已完成,且 types.ts 的 ExportTypes 枚举也包含 XLSX 与 DOCX,但当前服务端实现中 XLSX 分支被注释掉(相关代码块在 exportInvoiceService.ts),flattenObject() 辅助函数(lib/helpers.ts)正是为电子表格扁平化而准备。读者如需 XLSX 导出,可参考注释代码自行恢复该分支,仓库中同时存在 xlsx 依赖。

5. 金额转大写与货币细节

lib/helpers.ts 还包含两个实用的发票格式化函数:

  • formatNumberWithCommas():以 en-US 风格输出千分位 + 两位小数的金额字符串;
  • formatPriceToString():结合 public/assets/data/currencies.json 中每个货币的 decimals、beforeDecimal(如 Dirham)、afterDecimal(如 Fils),用 number-to-words 生成“金额大写”文本(如 Eight Hundred Fifty),在没有货币明细时退化为按小数位数动态拆分。

模板、签名与本地化的扩展能力

虽然 README 将“模板”与“签名”一带而过,仓库中这两块实现相当完整:

注意事项与已知问题

README 明确指出:当前应用在 Mozilla Firefox 浏览器下存在已知问题(对应上游 Issue #11),建议优先使用 Chrome/Chromium 系浏览器体验。这一点与 PDF 生成依赖无头 Chromium、签名画布使用 react-signature-canvas 等浏览器特性直接相关。

部署层面,项目包含 Dockerfile 与 puppeteer.config.cjs(Puppeteer 运行时配置),生产环境的 PDF 服务依赖 @sparticuz/chromium 提供二进制,说明该项目可被容器化部署或在 Vercel 等 Serverless 平台上运行,同时需关注 maxDuration 与内存限制对浏览器启动的影响。

小结

Invoify 是一个“小而完整”的发票生成器范例:表单层用 React Hook Form + Zod 保证数据质量,渲染层用 React 组件直出 PDF 模板,服务层用三条 Route Handler 分别承担生成、发送与导出,本地存储则让用户无需后端即可保存历史发票。围绕 README.md 的指引,配合 lib/schemas.ts、services/invoice/server 等源码,读者可以快速复现整套流程,也可以基于其 Schema 与模板机制扩展自己的发票业务(如恢复 XLSX 导出、补充 i18n 模板翻译)。项目以 MIT 协议开源,详见 LICENSE。

登录后查看全文
invoify