Invoify 实战指南:用 Next.js + TypeScript + Shadcn 构建的 Web 发票生成器
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(国际化)。
上图是 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,见下文)。
安装步骤
-
克隆仓库并进入目录:
git clone https://github.com/al1abb/invoify.git cd invoify当前仓库镜像位于
GitHub_Trending/in/invoify,也可基于该目录直接进行本地开发。 -
安装依赖:
npm install -
创建
.env.local并写入邮件配置(仅邮件发送功能需要):NODEMAILER_EMAIL=your_email@example.com NODEMAILER_PW=your_email_password -
启动开发服务器:
npm run dev -
浏览器访问 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”:
- 按
body.details.pdfTemplate通过getInvoiceTemplate()(lib/helpers.ts)动态导入对应模板组件; - 用
ReactDOMServer.renderToStaticMarkup()将组件序列化为静态 HTML; - 根据环境选择浏览器内核:
- 生产环境:使用
puppeteer-core+@sparticuz/chromium,启动参数包含--disable-dev-shm-usage与--ignore-certificate-errors,适配 Serverless 环境; - 开发/其他环境:直接使用完整版
puppeteer,参数为--no-sandbox、--disable-setuid-sandbox;
- 生产环境:使用
- 注入 Tailwind CDN 样式,等待
networkidle0/load/domcontentloaded后调用page.pdf()输出 A4 PDF,并设置printBackground: true、preferCSSPageSize: true; - 通过
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 将“模板”与“签名”一带而过,仓库中这两块实现相当完整:
- 发票模板:
InvoiceTemplate1/InvoiceTemplate2配合 InvoiceLayout.tsx 与 DynamicInvoiceTemplate.tsx 提供布局骨架;public/assets/img 中的invoice-1-example.png、invoice-2-example.png即为两套模板的渲染示例图。 - 签名系统:通过 SignatureModal.tsx 提供“手绘 / 输入 / 上传”三种签名方式(
SignatureTabs枚举见 types.ts),并支持 3 种颜色与 4 种手写字体(lib/variables.ts),签名数据以 Data URL 形式存入表单的details.signature.data。 - 国际化:除 i18n/locales 的 16 个语言文件外,LanguageSelector.tsx 与 TranslationContext.tsx 表明运行时语言切换能力已经具备,README 将 i18n 归入规划中,可能更多是指 UI/模板翻译的完整覆盖尚未完成。
注意事项与已知问题
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。
