USEGEAR

导航

学习unigui【44】如何和uinHTMLFrame打交道

一个极简、可复用的“UniGUI(M) + HTMLFrame/iframe 通信桥模板”。只保留关键段落:装桥、推数据、收事件、延迟关闭。你以后做水质、PLC、Luckysheet、任何 iframe UI,都按这个骨架套。


0)命名约定(统一协议)

  • 前端 → 父页面:{type:'ui_event', name:'close'|'refresh'|'save', payload:{...}}

  • 服务器 → 前端:{type:'ui_render', name:'render', payload:{...}}

  • ACK:{type:'ui_ack', name:'close'}(可选但强烈建议)


A) iframe 页面(xxx.html)— 最小模板

 
<script>
(function(){
  // ===== 发事件给父页面 =====
  function sendEvent(name, payload){
    var msg = { type:'ui_event', name:name, payload: payload||{}, ts: Date.now() };
    try{ window.top && window.top.postMessage(msg,'*'); }catch(e){}
    try{ window.parent && window.parent.postMessage(msg,'*'); }catch(e){}
  }

  // 关闭按钮
  document.getElementById('btnClose').addEventListener('click', function(e){
    e.preventDefault();
    sendEvent('close', {});
  });

  // ===== 收渲染指令 =====
  window.addEventListener('message', function(e){
    var d=e.data;
    if(!d || d.type!=='ui_render') return;

    if(d.name==='render'){
      // TODO: 用 d.payload 渲染 UI / ECharts / 表格
      // render(d.payload);
    }
  });

  // ===== 收 ACK(可选,用于调试)=====
  window.addEventListener('message', function(e){
    var d=e.data;
    if(d && d.type==='ui_ack'){
      // console.log('ACK:', d.name);
    }
  });
})();
</script>
View Code

 


B) Delphi:Frame(承载 HTMLFrame/iframe)— 最小模板

1)核心字段与公开方法(骨架)

 
 1 type
 2   TUniFrame_UiBridge = class(TUniFrame)
 3     UnimHTMLFrame1: TUnimHTMLFrame;
 4     procedure UniFrameCreate(Sender: TObject);
 5   private
 6     FInited: Boolean;
 7     FIFrameId: string;
 8     FCloseTimer: TUniTimer;
 9 
10     procedure InstallBridgeOnce;          // 装 JS 监听桥(只装一次)
11     procedure BindTarget;                 // 绑定 ajaxRequest 目标(固定为 UnimHTMLFrame1)
12     procedure HTMLAjaxEvent(Sender: TComponent; EventName: string; Params: TUniStrings);
13 
14     procedure DeferCloseOwnerForm;        // 延迟关闭(避免 session list 报错)
15     procedure DoCloseTimer(Sender: TObject);
16   public
17     procedure InitTemplate(const HtmlUrl: string);                 // 注入 iframe
18     procedure RenderToIframe(const PayloadJson: string);           // 推数据给 iframe
19   end;
View Code

 


2)InitTemplate:注入 iframe + 装桥 + 绑定目标

 
 1 procedure TUniFrame_UiBridge.InitTemplate(const HtmlUrl: string);
 2 begin
 3   if FInited then
 4   begin
 5     InstallBridgeOnce;
 6     BindTarget;
 7     Exit;
 8   end;
 9 
10   FInited := True;
11 
12   FIFrameId := UnimHTMLFrame1.JSId + '_ifUi';
13 
14   UnimHTMLFrame1.HTML.Text :=
15     '<iframe id="' + FIFrameId + '" src="' + HtmlUrl + '" ' +
16     'style="border:0;width:100vw;height:100vh;display:block;"></iframe>';
17 
18   InstallBridgeOnce;
19   BindTarget;
20 end;
View Code

 


3)InstallBridgeOnce:父页面监听 postMessage → ajaxRequest

这里最关键:把 iframe 的 {type:'ui_event',name:'close'} 转成 ajaxRequest(UnimHTMLFrame1,'ui_close',[])

 
 1 procedure TUniFrame_UiBridge.InstallBridgeOnce;
 2 begin
 3   UniSession.AddJS(
 4     'if(!window.__uiBridgeInstalled){' +
 5     '  window.__uiBridgeInstalled=true;' +
 6     '  window.addEventListener("message", function(e){' +
 7     '    var d=e.data; if(!d || d.type!=="ui_event") return;' +
 8 
 9     // ACK(可选)
10     '    try{ e.source && e.source.postMessage({type:"ui_ack",name:d.name,ts:Date.now()},"*"); }catch(ex){}' +
11 
12     // 固定目标:UnimHTMLFrame1 的 ajaxId(我们在 BindTarget 里写入)
13     '    var t = window.__uiAjaxTarget;' +
14     '    if(t && typeof ajaxRequest==="function"){' +
15     '      ajaxRequest(t, "ui_"+d.name, ["payload="+JSON.stringify(d.payload||{})]);' +
16     '    }' +
17     '  }, false);' +
18     '}'
19   );
20 end;
View Code

 


4)BindTarget:目标固定为 UnimHTMLFrame1(最稳)

不玩 JSId/Ext.getCmp 那套。目标就是 UniGUI 组件对象本身(通过 ComponentQuery 找到 UniGUI 的 ajax target)。

你已经验证“目标对象乱找”会出 Object not found,所以这里用 UniGUI 自己的“ajax target”方式绑定——最稳是:直接把 target 存成 UnimHTMLFrame1 的客户端对象引用。在 UniGUI 前端,组件一般都有 UnimHTMLFrame1.JSName 可直接引用;如果你环境里没有,就用 ComponentQuery 通过 UnimHTMLFrame1.JSId 找到 UniGUI 包装对象。

用 JSId 找对象(但找的是 UniGUI 组件,不是随便 Ext 控件):

 
 1 procedure TUniFrame_UiBridge.BindTarget;
 2 begin
 3   // 绑定 UnimHTMLFrame1 作为 ajaxRequest 的 target
 4   UniSession.AddJS(
 5     'try{' +
 6     '  var id="' + UnimHTMLFrame1.JSId + '";' +
 7     '  var t=null;' +
 8     '  if(window.Ext && Ext.ComponentQuery){' +
 9     '    var r=Ext.ComponentQuery.query("#"+id); if(r && r.length) t=r[0];' +
10     '  }' +
11     '  window.__uiAjaxTarget = t;' +
12     '  console.log("[bind] __uiAjaxTarget =", t, "id=", id);' +
13     '}catch(e){console.log(e);}'
14   );
15 end;
View Code

 


5)服务器端收事件:绑定到 UnimHTMLFrame1.OnAjaxEvent

 
 1 procedure TUniFrame_UiBridge.UniFrameCreate(Sender: TObject);
 2 begin
 3   FInited := False;
 4   FIFrameId := '';
 5 
 6   UnimHTMLFrame1.OnAjaxEvent := HTMLAjaxEvent;
 7 end;
 8 
 9 procedure TUniFrame_UiBridge.HTMLAjaxEvent(Sender: TComponent; EventName: string; Params: TUniStrings);
10 begin
11   UniSession.Log('UI AjaxEvent=' + EventName);
12 
13   if EventName = 'ui_close' then
14   begin
15     DeferCloseOwnerForm;   // ✅ 延迟关闭
16     Exit;
17   end;
18 
19   if EventName = 'ui_refresh' then
20   begin
21     // TODO: 服务器刷新数据后 RenderToIframe(...)
22     Exit;
23   end;
24 end;
View Code

 


6)延迟关闭(避免 session list object not found)

 
procedure TUniFrame_UiBridge.DeferCloseOwnerForm;
begin
  if FCloseTimer = nil then
  begin
    FCloseTimer := TUniTimer.Create(Self);
    FCloseTimer.Interval := 10;
    FCloseTimer.OnTimer := DoCloseTimer;
  end;

  FCloseTimer.Enabled := False;
  FCloseTimer.Enabled := True;
end;

procedure TUniFrame_UiBridge.DoCloseTimer(Sender: TObject);
begin
  FCloseTimer.Enabled := False;

  if Owner is TUniBaseForm then
    TUniBaseForm(Owner).Close;
end;
View Code

 


7)推数据给 iframe(RenderToIframe)

procedure TUniFrame_UiBridge.RenderToIframe(const PayloadJson: string);
var
  JS: string;
begin
  JS :=
    'try{' +
    '  var f=document.getElementById("' + FIFrameId + '");' +
    '  if(f && f.contentWindow){' +
    '    f.contentWindow.postMessage({type:"ui_render",name:"render",payload:' + PayloadJson + '}, "*");' +
    '  }' +
    '}catch(e){console.log(e);}';

  UniSession.AddJS(JS);
end;
View Code

 


C) 你要记住的“5 个硬规则”(像螺丝刀一样随手用)

  1. iframe 只负责 UI,不碰 UniGUI:只用 postMessage

  2. 父页面只干一件事message → ajaxRequest

  3. ajaxRequest 的 target 必须是“UniGUI session 对象”,不能乱拿 Ext id

  4. 关闭/销毁类动作必须延迟(Timer),避免 session list 报错

  5. 所有调试先看 ACK 和 bind log:ACK=消息到父;bind=目标存在;两者都对才可能进 Delphi


这个模板你可以直接复制成一个新的 TUniFrame_UiBridge,以后 PLC/水质/报表/编辑器都套它。你要我把它整理成你项目里能直接编译的完整 unit(含 dfm 绑定点、具体 uses、最少依赖)也可以。

 

 

更健康的策略(你现在已经走到这条路上了)

  1. 页面内部交互:局部闭环
    事件落点固定在本页面的 UnimHTMLFrame(或本 Form),逻辑清晰、可复用、可预测。

  2. 真正的全局规则:Delphi 端做“服务端真相源”
    统一退出、权限校验、状态栏信息、全局状态机——这些都放在服务器端(MainModule/ServerModule/统一服务类),前端只是“显示结果”和“提交请求”。

  3. 跨模块通讯:用“服务器端事件/状态”而不是“前端广播”
    比如:登录状态变化、权限变更、强制退出
    → 服务器端更新状态
    → 当前页面下一次交互/定时心跳/必要时推送时再响应(你也可以做很轻量的轮询或服务器推送,但那是另一套专门机制)

你可以记一个简单原则

前端不要当操作系统。
前端是 UI 和输入层;全局一致性、权限、生命周期这些“系统级规则”,交给 Delphi 端。

这条原则会让你未来做 Luckysheet、ECharts、各种 HTMLFrame 都更稳:每个页面都是一个小闭环,服务器端是唯一权威的大脑。

posted on 2026-01-12 15:48  USEGEAR  阅读(1)  评论(0)    收藏  举报