前情提要
Nocobase V2 引入了JS Block 和 RunJS特性,这些特性允许我们在Nocobase中以插件的方式书写任意代码,扩充官方没有实现的特性。
我们主要在 JS Block 中开发了一个基于 RunJS 的打印模板生成器,可以实现空行占位(如不足四行补齐四行)、按行数换页(如满四行换页)等Nocobase模板打印插件无法实现的编程式打印模板生成。
这在复杂的财务、行政、审计场景是无可避免的。
基于Runjs的模板打印生成
熟悉Nocobase V2的同学应该知道,Runjs提供了一个前端沙箱环境,其内部的ctx对象提供了我们二开nocobase几乎一切的API,支持任意数据访问与页面交互功能。
只需要使用ctx.request访问nocobase生成的数据源API,即可轻松完成跨表联查,如下面的代码就演示了我们如何从报销表出发,联表差旅明细、审批单据、出差地点表等,完整查出出差报销单据的过程。
const response = await ctx.request({
url: `exp_reimburse:get?filterByTk=${recordId}&appends=f_travel_exp_detail,f_applicant,f_dept,f_travel_exp_detail.f_start_location,f_travel_exp_detail.f_end_location,f_travel_exp_detail.f_address_detail,f_travel_confirmation.f_travel_start_date,f_travel_confirmation.f_travel_end_date`,
});
const record = response?.data?.data;
if (!record) {
ctx.render('<div style="color:red;padding:20px;font-family:SimSun,serif;font-weight:bold;">获取数据失败,请检查网络或权限。</div>');
return;
}
后面就是纯js生成html,构建表格的过程。
引入外部库实现PDF生成
在具备外网的开发环境中,我们可以轻松使用await ctx.importAsync('https://cdn.jsdelivr.net/npm/js-html2pdf@1.1.4/lib/html2pdf.min.js');导入外部库,实现HTML元素生成PDF打印文件,供企业员工进行打印。
如下面代码片段所示
btn.onclick = function () {
console.log('Exporting PDF...');
const opt = {
margin: 0,
filename: `差旅费报销单_${baseInfo.budgetNo}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, logging: false },
jsPDF: { unit: 'mm', format: 'a5', orientation: 'landscape' },
pagebreak: { mode: ['avoid-all', 'css', 'legacy'] }
};
if (typeof html2pdf === 'function') {
html2pdf().from(wrap).set(opt).save().catch(err => {
console.error('PDF Export Error:', err);
});
} else {
console.error('PDF Export Error:', err);
}
};
内网环境挑战
然而,在迁移到内网时,整个事情变得具备挑战起来。首先是沙箱环境对UMD支持有限(无法访问window全局对象),意味着我们不能使用await ctx.requireAsync('echarts@5/dist/echarts.min.js'); 的方式引入外部库,我们使用了诸多方式引入html2pdf库,最后发现只有ESM才可以在nocobase的沙箱环境运行。
这个时候,我们必须在公网、内网私有化各部署一个esm.sh服务,把公网的esm.sh构建结果拷贝到内网,这样子内网的nocobase才可以获取到依赖。 思路可参考:esm-server/README.zh-CN.md at 34cb9b0ee032135ac15334aedd7fd0021ed123e1 · nocobase/esm-server · GitHub
关于esm.sh在内网部署的文章可以查询到的不多,官方也并没有进行很详尽的描述,我们踩了很多坑,如权限问题/版本锁定问题/关联依赖访问问题等,我们直接给出可以稳定使用的方法,下面全部基于docker版描述,需要读者有基础:
- 假设我们的依赖是html2pdf.js,则必须在公网实例访问的时候,固化版本,使用bundle+standalone方式构建:即
http://<公网esm.sh>:8060/html2pdf.js@0.14.0?bundle&standalone,访问这个链接即会触发esm.sh拉取和完成本地化构建,形成构建缓存 - 这个时候公网的esm.sh会固化依赖版本,并把html2pdf的所有依赖都打包在一个单体文件里,确保访问者拉取库的时候不会请求其他依赖文件
- 拷贝公网实例的storage目录到内网实例,启动内网的esm.sh
- 这个时候,可以在生产环境使用下面语法导入内网的离线依赖
let html2pdf;
const module = await ctx.importAsync('http://<内网esm.sh>:8060/html2pdf.js@0.14.0?bundle&standalone');
html2pdf = module.default || module;
案例延展
本文除了介绍模板打印的另外一种开发思路,还介绍了如何在 Nocobase v2 RunJS / JS Block 引入任何ESM依赖的方法,不仅适用于内网环境,也适用于公网的依赖自行打包加速。
通过私有化 esm.sh 与 Nocobase V2 结合,我们可以充分自行掌握 Nocobase V2 自开发模块的依赖构建、分发和引入过程。而不需要修改Nocobase源码本身。
其他分享
- 我们使用的esm.sh docker-compose文件,官方分享的似乎有坑
version: "3.9"
services:
esm-server:
image: ghcr.io/esm-dev/esm.sh:latest
restart: always
ports:
- "8060:80"
volumes:
- ./data/esm:/etc/esmd
environment:
- NODE_ENV=production
- NPM_REGISTRY=https://registry.npmmirror.com
- STORAGE_TYPE=fs
# 关键:将存储端点指向挂载目录
- STORAGE_ENDPOINT=/etc/esmd/storage
user: "root"
- 为什么需要固化版本、使用bundle+standalone模式?
如果不固化版本,esm.sh每次都会尝试请求公网获取latest元数据,导致 DNS 解析直接报错;而不使用bundle+standalone模式,虽然主库可能已缓存,但其底层依赖仍会以“藕断丝连”的远程链接形式存在,一旦触发对子依赖的动态拉取,便会因无法访问外网而导致整个模块加载失败。通过该模式,我们可以将所有依赖形成完全自包含的本地静态文件,彻底切断对外部环境的依赖。 - 欢迎联系我询问 Nocobase 开发问题/答疑/思路,深度、专业NCB实施团队为你服务 (jan@easypus.com) 主题请带 nocobase
