政务低代码平台实战⑤:双输出模式——存DB与导出JSP的完整链路

政务低代码平台实战⑤:双输出模式------存DB与导出JSP的完整链路

文章目录

非科班野生程序员,深耕政务信息化20年。表单设计器设计好了,设计结果存哪?运行时怎么还原?这篇拆解设计器的两条出路------存数据库动态渲染,和导出JSP静态加载。两种模式还能互相转换,导出的JSP可以载回设计器继续编辑。最后感谢豆包、智谱、OpenCode,决策是我做的,代码是我搓的,文字是他们总结的。


背景

表单设计器解决了"怎么设计"的问题。但设计结果有两条出路:

  1. 开发阶段:频繁调整布局、改字段、改SQL。需要存数据库,随时载回设计器修改,运行时动态渲染。
  2. 上线后:表单不再变化。动态渲染每次都读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;
}

导出做了三件事:

  1. 把设计器的HTML和JS分别写成 .jsp.js 文件
  2. JSP文件包含标准的 <%@ page %> 声明和 config.jsp 引用
  3. 更新 T_FORMtype=3filepath

三种表单类型的运行时调度

万能表单 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导出 #动态渲染 #表单管理 #政务信息化

相关推荐
南京码讯光电技术有限公司1 小时前
What Does Ex tb IIIC T80°C Db Mean?
服务器·网络·数据库
做萤石二次开发的哈哈2 小时前
萤石蓝海AIoT一站式工作台计费说明
网络·数据库·萤石开放平台·蓝海aiot一站式工作台
竹枝溪2 小时前
MySQL数据库零基础入门
数据库·sql·mysql·navicat·ddl·dml·dql
龙仔7252 小时前
人大金仓Kingbase V8 自动全量备份部署完整笔记
数据库·笔记·备份·人大金仓
王志来137944730082 小时前
从选型到交付,工控服务器机箱一站式采购平台匀天如何实现服务闭环?
数据库
ShuiShenHuoLe3 小时前
HmarkX(码笺)的本地数据库模块重构实战
数据库·oracle·重构
Databend3 小时前
从 Kafka 到 Databend Cloud:万亿级 Agent Trace 接入链路的工程实践
大数据·数据库·agent
倔强的石头_3 小时前
SQL Server数据迁移,不只是把数据搬过去
数据库
MC皮蛋侠客3 小时前
Redis 系列(五):持久化——RDB、AOF 与混合持久化
数据库·redis·bootstrap