政务低代码平台实战⑤:双输出模式------存DB与导出JSP的完整链路
文章目录
非科班野生程序员,深耕政务信息化20年。表单设计器设计好了,设计结果存哪?运行时怎么还原?这篇拆解设计器的两条出路------存数据库动态渲染,和导出JSP静态加载。两种模式还能互相转换,导出的JSP可以载回设计器继续编辑。最后感谢豆包、智谱、OpenCode,决策是我做的,代码是我搓的,文字是他们总结的。
背景
表单设计器解决了"怎么设计"的问题。但设计结果有两条出路:
- 开发阶段:频繁调整布局、改字段、改SQL。需要存数据库,随时载回设计器修改,运行时动态渲染。
- 上线后:表单不再变化。动态渲染每次都读DB、解析组件树、生成HTML,有性能开销。不如导出成JSP文件直接部署。
两条路都要走通,而且要能互相切换。
两条出路的数据流
设计器(formEdit.jsp)
│
├── 保存到DB(add()按钮)
│ ├── T_FORM → 表单定义(HTML、JS脚本、参数)
│ ├── T_FIELD → 字段定义(名称、类型、代码表、默认值)
│ ├── T_TABLE → 数据源绑定(form_id关联)
│ ├── T_EL → 元素树(组件ID、父ID、类型、JSON配置、事件)
│ └── T_EL_COLUMS → 元素属性(每个组件的详细属性键值对)
│ │
│ ↓ type=1/2
│ onlyFrom.jsp 从DB读取 → 动态渲染
│
└── 导出JSP(saveto()按钮)
├── 写 xxx.jsp 文件到磁盘
├── 写 xxx.js 文件到磁盘
└── T_FORM.type=3, filepath='xxx.jsp'
│
↓ type=3
运行时读JSP文件直接输出
保存到数据库
涉及的5张表
T_FORM(表单主表):
| 字段 | 含义 |
|---|---|
| ID_ | 表单唯一ID |
| NAME_ | 表单名称 |
| DESCRIPTION_ | 描述 |
| TYPE_ | 类型:1=列表, 2=表单, 3=JSP文件 |
| HTML_ | 表单HTML内容(CLOB) |
| SCRIPT_ | 表单JS脚本(CLOB) |
| COLS | 列数 |
| H | 高度 |
| W | 宽度 |
| PARAMETERS_ | 参数 |
| REPORT_ | 打印模板 |
| SQLID | 关联的SQL编号 |
| FILEPATH | JSP文件路径(type=3时) |
T_FIELD(字段表): 每个绑定数据的控件一条记录
| 字段 | 含义 |
|---|---|
| FIELDID | 字段ID |
| ID_ | 所属表单ID |
| NAME_ | 英文名(binding字段名) |
| DESCRIPTION_ | 中文名 |
| COLTPYE | 控件类型 |
| DATATYPE | 数据类型 |
| N_COLS | 新增时列宽 |
| D_COLS | 显示时列宽 |
| P_VAL | 是否流程变量 |
| COMMENS | 自定义选项JSON |
| CODENAME | 二级代码编码 |
| READONLY | 是否只读 |
T_EL(元素树): 设计器组件树的每个节点
| 字段 | 含义 |
|---|---|
| EL_ID | 元素ID(组件ID) |
| PARENT_EL_ID | 父元素ID |
| TYPE_ | 组件类型(browise.form.TextBox等) |
| JSON_ | 创建时的JSON配置 |
| OTHER_ | 其他参数 |
| ORDERID | 排序 |
| EVENT_ | 事件绑定JSON |
T_EL_COLUMS(元素属性): 每个组件的详细属性
| 字段 | 含义 |
|---|---|
| EL_ID | 元素ID |
| COL_ | 属性名(dojoType/width/binding/disabled等) |
| VALUE_ | 属性值 |
T_TABLE(数据源绑定): 表单关联的数据表
| 字段 | 含义 |
|---|---|
| ID_ | 表单ID |
| FORM_ID | 数据源form的ID |
保存过程
uiglobal.js 中的 add() 函数:
javascript
function add() {
// 1. 收集所有组件信息
var nodes = getformorgrid(); // 获取所有Form和Grid节点
// 2. 遍历组件树,生成HTML
var shtml = "";
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].declaredClass == 'browise.form.Form') {
shtml += gettable(nodes[i].id, parent_id); // Form+Table序列化为HTML
} else if (nodes[i].declaredClass == 'browise.grid.Grid') {
shtml += getgrid(nodes[i].id, parent_id); // Grid序列化为HTML
}
}
// 3. 发送到后端
var dc = new browise.ds.DataCenter();
dc.addStore(dsForm); // 表单元数据
dc.addStore(dsField); // 字段定义
dc.addStore(dsEl); // 元素树
dc.addStore(dsElCol); // 元素属性
dc.addStore(dstable); // 数据源
var URL = webRootDir + "/BusinessAction?Business=formManager&Action=Business";
browise.Action.requestData({ url: URL, load: function(dc1) {
alert("保存成功!");
}}, dc, true);
}
后端 formManagerControl.Business() 接收数据:
java
// 如果是type=2(修改),先删后插
if ("2".equals(type)) {
formService.deleteform(id); // 删表单
formService.deletefield(id); // 删字段
formService.deleteel(id); // 删元素
formService.deleteelcloum(id); // 删元素属性
formService.deletetable(id); // 删数据源
}
// 重新插入
formService.insertform(dsForm);
formService.insertfield(dsField);
formService.insertel(dsEl);
formService.insertelcloum(dsElCol);
formService.inserttable(dsTable);
"先删后插"的策略虽然粗暴,但保证了数据一致性------不会出现修改后组件树和属性不同步的问题。
导出JSP文件
uiglobal.js 中的 saveto() 弹出对话框输入文件名和路径,然后调用后端:
java
// formManagerControl.SaveToJSP()
public DataCenter SaveToJSP(DataCenter center, ...) throws Exception {
DataStore dsea01 = center.getStore("ea01");
String filename = dsea01.getRowset().getrow(0).getItemStringValue("name_");
String path = dsea01.getRowset().getrow(0).getItemStringValue("description_");
String html = dsea01.getRowset().getrow(0).getItemStringValue("html_");
String script = dsea01.getRowset().getrow(0).getItemStringValue("script_");
// 拼JSP文件头
String jspContent = "<!DOCTYPE html PUBLIC ...>\n"
+ "<%@page contentType=\"text/html; charset=UTF-8\"%>\n"
+ "<%\n"
+ "\tString sessionid = \"0000000000000000000\";\n"
+ "%>\n"
+ "<html>\n<head>\n"
+ "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n"
+ "<%@ include file=\"/browise/ria3.3/pages/config.jsp\"%>\n";
// 拼JS文件引用
jspContent += "<script type=\"text/javascript\" src=\"" + filename + ".js\"></script>\n";
// 拼HTML body
jspContent += "</head>\n<body class=\"unieap\">\n" + html + "\n</body>\n</html>";
// 写JSP文件
String jspPath = request.getRealPath("/") + "pages/" + path + "/" + filename + ".jsp";
FileOutputStream fos = new FileOutputStream(jspPath);
fos.write(jspContent.getBytes("UTF-8"));
fos.close();
// 写JS文件
String jsPath = request.getRealPath("/") + "pages/" + path + "/" + filename + ".js";
fos = new FileOutputStream(jsPath);
fos.write(script.getBytes("UTF-8"));
fos.close();
// 更新T_FORM:type=3, filepath
dsea01.getRowset().getrow(0).setItemValue("type_", "3");
dsea01.getRowset().getrow(0).setItemValue("filepath", "pages/" + path + "/" + filename + ".jsp");
formService.updateform(dsea01);
return dc;
}
导出做了三件事:
- 把设计器的HTML和JS分别写成
.jsp和.js文件 - JSP文件包含标准的
<%@ page %>声明和config.jsp引用 - 更新
T_FORM的type=3和filepath
三种表单类型的运行时调度
万能表单 onlyFrom.jsp 根据type决定渲染方式:
jsp
<%
String type1 = formds.getRowset().getrow(0).getItemStringValue("type");
if ("3".equals(type1)) {
// type=3:读JSP文件直接输出
String html = FileUtil.readJspFile(
formds.getRowset().getrow(0).getItemStringValue("filepath"));
out.println(html);
} else {
// type=1/2:从数据库的html_字段读HTML
String html = String.valueOf(formds.getRowset().getrow(0).getItemValue("html_"));
out.println(html);
}
%>
| type | 来源 | 性能 | 适用场景 |
|---|---|---|---|
| 1 | T_FORM.HTML_字段 | 中 | 列表页面(开发阶段) |
| 2 | T_FORM.HTML_字段 | 中 | 表单页面(开发阶段) |
| 3 | JSP文件 | 优 | 上线后(不再修改) |
type=1和type=2的区别不在渲染,而在设计器模式------UI版本设计列表页(含Grid+条件),UI2版本设计表单页(纯表单,oneSql=true)。
载入回设计器
已保存的表单可以载回设计器继续编辑。openload() 弹窗选择表单,后端 detail() 方法查出T_FORM、T_FIELD、T_EL、T_EL_COLUMS数据返回前端,loadformdb() 函数还原组件树:
javascript
function loadformdb(rowdata) {
// rowdata包含T_EL元素列表
// 按orderid排序,逐个还原:
// 1. 先创建Form容器(createFormofDb)
// 2. 再创建Table(createTABLEofDb)
// 3. 创建TR(createTRofDb)
// 4. 创建TD(createTDofDb)
// 5. 在TD里创建控件(根据type_创建对应组件)
// 6. 恢复属性(从T_EL_COLUMS读回)
// 7. 恢复事件绑定
}
还原顺序严格按照 orderid------先父后子,保证DOM树层级正确。
完整闭环
设计器设计表单
↓
保存到DB(type=1/2)
↓
运行测试 → 不满意? → 载回设计器修改
↓ ↓
满意了 修改完再保存
↓ ↓
导出JSP(type=3) 继续测试
↓
上线运行(读JSP文件,高性能)
↓
政策变了要改表单?→ 载回设计器(type改回1/2)→ 修改 → 再导出
设计器 → DB → 测试 → JSP → 上线 → 载回修改 → 再导出。整个生命周期都走通了。
决策原则
开发阶段求灵活,上线后求性能。
存DB的好处是随时可编辑、可载回、可修改。导JSP的好处是零数据库访问、直接输出HTML。两种模式不是互斥的,是同一个设计结果在不同阶段的不同表现形式。
关键设计是 T_FORM.type 字段------一个字段控制渲染方式,业务代码不用变,只有渲染层根据type走不同的分支。而且type可以随时改回来:导出JSP后,载入设计器时type自动改回1/2,修改完再导出。
你项目里的表单是怎么管理生命周期的?欢迎评论区聊聊。
系列导航:
- 总纲:政务低代码平台实战------从元数据引擎到可视化设计器的五个关键决策
- 上一篇:政务低代码平台实战④:可视化表单设计器------拖拽、合并拆分、代码生成
- 框架系列:Java Web自研框架18年架构决策复盘(系列文总纲)
- 工作流系列:政务工作流实战(二)------BPMN画不出来的七个设计决策
作者:许彰午 | 非科班野生程序员,深耕政务信息化20年
标签: #Java #低代码 #JSP导出 #动态渲染 #表单管理 #政务信息化