
管理系统的价值闭环,往往不在系统里,而在系统外。审批完成了要给申请人发一封通知邮件,数据算好了要导出一份 Excel 给领导过目,报表做出来了要能直接打印存档——这些"出系统"的动作就是办公自动化工具集要解决的问题,也是很多技术方案在演示环境里光彩照人、上线之后被业务部门吐槽"连个邮件都发不了"的原因。这类需求单看每一件都不难,难的是工程化:邮件要处理 SMTP 认证与附件流,Excel 要处理样式与大数据量,打印要处理纸型与分页。自己攒一套,三个月能跑就不错;换人维护更是灾难。geejing WebBuilder 快速开发平台在 example/tools 目录里给出了四件套答案:邮件发送(mail-sender)、Excel 报表(excel-report)、Excel 表单(excel-form)、SQL 导 Excel(sql-to-excel)。本篇重点拆解其中最有代表性的邮件与 Excel 三条链路。
先看运行界面。mail-sender.xwl 是一个单列表单:SMTP 服务器、用户名、密码、发件人、收件人、抄送、主题、正文,以及一个支持多选的附件控件:

页面上每个输入项都配置了 required: 'true'(必填项),发送按钮的点击事件是标准的"先校验、后提交"两段式:
click: |-
if (!Wb.verify(app.viewport1))
return;
Wb.ajax({
url: xpath + '/send',
comps: app.viewport1,
success() {
Wb.tipSucc('The email has been successfully sent.');
}
});两行核心调用值得写成肌肉记忆:Wb.verify(app.viewport1) 校验容器内全部控件的必填与格式规则,不通过时自动在控件旁标注错误并返回 false;comps: app.viewport1 则告诉 Wb.ajax 把整个容器的控件值一并收集提交——你不需要逐个 getValue,也不需要手工组装 FormData,控件的 cid 就是参数名。顺带体会一下这种校验的体验设计:错误提示出现在控件旁边而不是弹一个全局 alert,用户可以一眼看到所有要补的空,改一处补一处。很多自研系统的表单校验之所以被嫌弃,不是因为规则不全,而是因为提示方式让人崩溃——平台把"提示位置"这个 UX 细节也替你做对了。附件控件 attachFiles 配置了 multiple: 'true',多文件会随表单一起流式上传。
服务端的 mail-sender/send.xwl 才 30 行,却是教学级范本:
function main() {
let sender, attachFiles, submitFiles = [];
sender = new Wb.MailSender(
Params.smtp,
Params.username,
Params.password,
Params.authUsername,
Params.authPassword,
{ 'mail.smtp.auth': true, 'mail.smtp.sendpartial': true }
);
attachFiles = Wb.getParams('attachFiles');
attachFiles.forEach(file => {
submitFiles.push({ name: file.name, data: file.stream });
});
try {
sender.send(Params.from, Params.to, Params.title, Params.content, submitFiles, Params.cc);
} finally {
sender.close();
}
}
main();四个要点:第一,Wb.MailSender 是平台内置的邮件客户端类,前五个参数依次是服务器、账号、密码、认证用户名、认证口令——很多企业邮箱要求独立的 SMTP 认证凭据(认证账号与发件地址不同),所以平台把它拆成了两组参数,这在国内企业邮箱环境里非常实用;第六个参数是 Java Mail 属性对象,示例开了 mail.smtp.sendpartial,允许部分收件人失败时其余照常投递。第二,Wb.getParams('attachFiles') 取回的是文件对象数组,每个元素带 name 和 stream——流式处理,再大的附件也不会把内存撑爆。第三,try ... finally 结构确保 sender.close() 无条件执行,SMTP 连接不泄漏。第四,整个脚本包在 main() 函数里显式调用,这是服务端脚本的常见组织方式,比裸语句更利于日后扩展。
把这套示例产品化,通常只需三步:把 SMTP 配置从表单挪到系统参数表;把收件人改成按业务事件查出来的用户组;在业务模块的关键节点(审批完成、报告生成)异步调用这个 send 模块。骨架不动,壳子现成。
展开说说邮件在企业系统里的三种典型角色,因为它们对可靠性的要求完全不同:通知型(审批结果、告警提醒)追求必达,要设计重试与失败记录表;交互型(带确认链接的邀请函)要考虑链接有效期与防伪造签名;报送型(定时报表附件)则重在幂等——同一个任务跑两次就发两份,业务方会来找你麻烦。示例给的是骨架,这三种角色的差异要在你自己的调度与记录逻辑里补齐。另外提醒一句安全细节:示例表单里的密码框如果产品化,务必改用平台的加密存储,而不是每次让用户明文输入——demo 展示的是能力,不是安全姿势。

excel-report.xwl 演示的是"服务端渲染 HTML 报表 + 前端展示导出打印"的完整链路。先看它的结构:整个模块就是一个 Toolbar 加一个 layout: middle 的居中容器 container1,容器配了 autoScroll,报表内容长出来之后自行滚动。没有表格组件、没有数据绑定——因为表格本身也是服务端拼好 HTML 送过来的,前端容器只是一个"画布"。工具栏三个按钮对应三个动作:
// Reload:向服务端请求报表 HTML,灌进容器
click: |-
Wb.ajax({
url: xpath + '/actions&xaction=reload',
success(resp) {
app.container1.html = '';
app.container1.html = resp;
}
});
// Export:直接下载服务端生成的文件
click: Wb.download(xpath + '/actions&xaction=export');
// Print:对容器内容执行打印,指定 A3 横向纸型
click: Wb.print(app.container1, 'A3 landscape');三个按钮、三种交付形态,恰好覆盖报表的三个消费场景:看(浏览器里实时渲染)、存(下载成文件归档)、交(打印出来签字上会)。特别注意 Wb.print 的第二个参数 'A3 landscape'——财务类宽表在 A4 纵向里必然被截得七零八落,能按内容指定纸型是打印功能是否"可用"的分水岭。另外 Wb.download 与普通 ajax 的区别也要点一下:它不是把响应读进 JS 内存再另存,而是让浏览器走原生下载通道,服务端直接把文件流写进响应,内存零占用,大文件也不怕。模块的 initialize 事件里还有一处细节:onLoad 时自动触发了一次 Reload 按钮的事件(app.reloadItem.fireEvent('click')),页面打开即有数据,不让用户面对一个空容器。fireEvent 这个小 API 在实际开发里出镜率极高——复用某个按钮的完整逻辑(含它绑定的所有事件)时,比把逻辑复制一份到处贴体面得多。
这个设计的聪明之处在于报表的渲染职责放在了服务端:actions 的 reload 分支返回的是拼装好的 HTML 表格。服务端渲染意味着样式、分页、汇总行全部由代码统一控制,前端容器只负责展示;同一份报表逻辑可以同时供浏览器、导出、打印三条渠道复用,不会出现"屏幕上是一回事、打出来是另一回事"的经典尴尬。
四件套里的最后一位 excel-form 则展示了另一种思路——模板驱动。它的服务端只有三个 action:select 用 Wb.sendDict 给表单供数;getForm 调用 Wb.getExcelHtml('form.xlsx', { now: new Date().dateText }),直接把一个设计好的 xlsx 模板文件转成 HTML 表单回传展示;export 接收用户改过的数据,用 Wb.getExcelFile('my-form.xlsx', 'form.xlsx', data) 按模板填充生成新文件供下载。也就是说,业务人员用 Excel 设计好样式复杂的公文格式(红头、盖章位、签字栏),开发人员把文件丢进模板目录,两行 API 就完成了"模板转表单、数据填模板"的闭环。对比一下三种 Excel 方案的适用边界:excel-report 适合"代码全控"的动态报表,sql-to-excel 适合"结构即数据"的批量导出,excel-form 适合"样式即公文"的固定格式套打——三条路线覆盖了企业 Excel 需求的绝大多数光谱。
如果说 excel-report 是"给业务看"的报表,sql-to-excel 就是"给系统用"的数据出口——把任意 SQL 的结果集原样变成标准 xlsx 文件。它的服务端模块 create.xwl 精炼到可以全文抄录:
function main() {
let bos, rowx, cols;
bos = new ByteArrayOutputStream();
rowx = Wb.getRowx({ sql: 'select * from wb_staff', rs: -1 });
cols = rowx.columns;
cols.shift(); // shift row num col
cols.forEach(col => col.width = parseInt(col.width));
com.wb.office.SheetWriter.createExcel(bos, Wb.toJava(cols), Wb.toJava(rowx.items), 0, 'Staff List', null);
Wb.exportData(bos.toByteArray(), 'staff.xlsx');
}
main();逐行拆解:ByteArrayOutputStream 是 JVM 提供的内存字节流(服务端脚本可以直接使用 Java 类,这是 WebBuilder 服务端 JS 的独门优势);Wb.getRowx({ sql, rs: -1 }) 一次取回"列元数据 + 全部行数据"的复合结构,rs: -1 表示不分页拉全量;cols.shift() 去掉第一列行号列,宽度的字符串值转成整数;com.wb.office.SheetWriter.createExcel 是平台 office 引擎的静态方法,第二个、第三个参数用 Wb.toJava(...) 把 JS 对象转成 Java 集合——JS 与 Java 之间的类型桥接只在这一处出现,其余代码感受不到两种运行时的边界;最后 Wb.exportData 把字节流以 staff.xlsx 文件名输出给浏览器。
关于这个"独门优势"值得多说两句。WebBuilder 11 的服务端脚本运行在 JVM 之上,于是 JavaScript 第一次在服务端拥有了整个 Java 生态当"标准库":需要字节流就 new 一个 ByteArrayOutputStream,需要压缩就找 ZipUtil,需要调用公司已有的 Java SDK 也只是 import 与 new 的区别。而 Wb.toJava 与它的逆向 Wb.toJs 则是两种类型系统的摆渡船——JS 的对象数组过去变成 Java 的 List/Map,Java 侧返回的集合回来又变回 JS 对象。写惯了 Node.js 的同学可以把这套模型理解为"运行在 JVM 上的脚本引擎 + 平台级 FFI",理解了这一点,示例里那些 Java 类名就不再陌生。这七行代码背后是一条完整的工程链路:SQL 执行、结果集映射、样式列宽、Excel 编码、HTTP 下载,全部由平台代办。你需要关心的只有两件事:SQL 怎么写、文件叫什么名。
这七行代码背后是一条完整的工程链路:SQL 执行、结果集映射、样式列宽、Excel 编码、HTTP 下载,全部由平台代办。你需要关心的只有两件事:SQL 怎么写、文件叫什么名。
把三个示例的能力合到一起,可以支撑的企业场景远比想象中多。工具集的定位是"系统与人的最后一公里":数据在系统里流转得再漂亮,最终都要以人能消费的形态(邮件、文件、纸张)交付出去。常见落地场景有四类:
四类场景共同印证了一个选型原则:凡是要跟"人"打交道的输出,宁可调用平台工具集,也不要手搓。手搓 SMTP 你要面对 STARTTLS、授权码、附件编码三座大山;手搓 Excel 你要面对样式、列宽、兼容性三座大山;平台工具集的意义就是把这些"与业务无关但缺一不可"的坑提前填平。
落地时的三条忠告:SMTP 账号必须进配置表而不是散落在代码里;大结果集导出走异步任务并限制单次行数,避免一次百万行导出把服务拖垮;邮件正文里不要拼接用户可控的 HTML 片段,防止注入风险。再补一条容易被忽略的细节:附件文件名里如果含中文,务必交给 Wb.exportData 与平台的上传组件处理编码,自拼 Content-Disposition 头是乱码问题的头号来源——平台的下载 API 默认按 RFC 规范处理了多浏览器兼容,站在它的肩膀上就好。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。