学习unigui【41】htmlframe的交互现象
场景:
1、mainform中有许多控件,其中有htmlframe。那就有回调等等交互。
2、htmlframe中显示相关指标。要初始化和定时刷新数据
问题:死活和htmlframe交互不到。why?我不懂web原理。
有老师精通的,请指导我。这个如果是坑,那么死在这里的的人不会少。所以大家去学vue等等了。
代码仅仅记录该坑爹的过程:
1、htmlframe中有代码(我也不懂js)
1 <script> 2 3 // ===== 初始化占位符(Delphi 替换这里)===== 4 // 替换成:{"header":{...},"metricsTitle":"...","metrics":[...]} 5 const __INIT_WATER_DATA__ = {[&WATER_INIT_JSON]}; 6 7 8 // ===== 渲染 ===== 9 function fmt(v){ 10 if (v === null || v === undefined) return '--'; 11 if (typeof v === 'number') return (Math.round(v*100)/100).toString(); 12 return String(v); 13 } 14 15 function renderMetrics(list){ 16 const grid = document.getElementById('grid'); 17 grid.innerHTML = ''; 18 (list || []).forEach(m => { 19 const el = document.createElement('div'); 20 el.className = 'mCard'; 21 el.innerHTML = ` 22 <div class="mHead"> 23 <div class="mName">${m.name ?? ''}</div> 24 <div class="mUnit">${m.unit ? '('+m.unit+')' : ''}</div> 25 </div> 26 <div class="mVals"> 27 <div class="mVal"><div class="mNum">${fmt(m.in)}</div><div class="mCap">进水</div></div> 28 <div class="mVal"><div class="mNum">${fmt(m.out)}</div><div class="mCap">出水</div></div> 29 </div>`; 30 grid.appendChild(el); 31 }); 32 } 33 34 // ===== 顶部提示条 ===== 35 const hint = document.getElementById('refreshHint'); 36 function showHint(text, loading){ 37 if (!hint) return; 38 hint.textContent = text; 39 hint.style.display = 'flex'; 40 if (loading) hint.classList.add('loading'); else hint.classList.remove('loading'); 41 } 42 function hideHint(){ 43 if (!hint) return; 44 hint.classList.remove('loading'); 45 hint.style.display = 'none'; 46 } 47 48 // ===== 统一入口:收到数据就更新并停转圈 ===== 49 window.setDashboardData = function(data){ 50 data = data || {}; 51 document.getElementById('heroTitle').textContent = (data.header && data.header.title) || '开发区第三污水处理厂'; 52 document.getElementById('metricsTitle').textContent = data.metricsTitle || '水质监测'; 53 renderMetrics(data.metrics || []); 54 hideHint(); 55 if (window.__waterRefreshDone) window.__waterRefreshDone(); // ★关键:刷新完成解锁 56 }; 57 58 // ★立即跑一次:不依赖 load(UnimHTMLFrame 动态注入时经常不触发 load) 59 try { window.setDashboardData(__INIT_WATER_DATA__); } catch(e) {} 60 61 // ===== 收父页面回推 ===== 62 window.addEventListener('message', function(e){ 63 const d = e.data; 64 if (!d || !d.type) return; 65 if (d.type === 'dashboardData'){ 66 // ★如果父页面推空数据,就别覆盖初始化界面 67 if (!d.payload || !d.payload.metrics || d.payload.metrics.length === 0) return; 68 window.setDashboardData(d.payload); 69 } 70 }); 71 72 // ===== 下拉刷新:只发消息给父页面 ===== 73 // ===== 下拉刷新(时间门禁·不抢滚动版)===== 74 // 规则:必须从“顶部热区”开始下拉;达到阈值后必须“停住不动”HOLD_MS 才触发。 75 // 全程不 preventDefault,MainForm 滚动不会被 iframe 抢走。 76 (function(){ 77 const PULL_PX = 35; // 比你原来 25 更不敏感 78 const HOLD_MS = 350; // 比你原来 450 更稳 79 const EDGE_PX = 70; // 只有从屏幕顶部 70px 内开始的手势才算“刷新手势” 80 const MAX_DY = 220; // 防止夸张拉动造成误判 81 82 let state = 'idle'; // idle | tracking | armed | refreshing 83 let startY = 0; 84 let dy = 0; 85 let holdTimer = null; 86 87 function clearHold(){ 88 if (holdTimer){ 89 clearTimeout(holdTimer); 90 holdTimer = null; 91 } 92 } 93 94 function doRefresh(){ 95 if (state === 'refreshing') return; 96 state = 'refreshing'; 97 clearHold(); 98 showHint('正在更新水质数据…', true); 99 window.parent.postMessage({type:'refreshWaterData'}, '*'); 100 } 101 102 // 只允许“从顶部热区”开始上膛(否则一律放行给 MainForm 滚动) 103 document.addEventListener('touchstart', function(e){ 104 if (state === 'refreshing') return; 105 106 const y0 = e.touches[0].clientY; 107 108 // ★关键:不是从顶部热区开始的手势,直接忽略(不进入 tracking) 109 if (y0 > EDGE_PX){ 110 state = 'idle'; 111 clearHold(); 112 return; 113 } 114 115 startY = y0; 116 dy = 0; 117 state = 'tracking'; 118 clearHold(); 119 }, {passive:true}); 120 121 document.addEventListener('touchmove', function(e){ 122 if (state !== 'tracking' && state !== 'armed') return; 123 if (state === 'refreshing') return; 124 125 const y = e.touches[0].clientY; 126 dy = y - startY; 127 128 // 只认向下;向上/回弹就取消 129 if (dy <= 0){ 130 hideHint(); 131 state = 'tracking'; 132 clearHold(); 133 return; 134 } 135 136 // 限幅,避免夸张拖动误判 137 if (dy > MAX_DY) dy = MAX_DY; 138 139 // ★注意:这里绝对不 preventDefault,避免抢 MainForm 滚动 140 141 // 小于阈值:只提示,不计时 142 if (dy < PULL_PX){ 143 showHint('下拉继续保持可刷新…', false); 144 state = 'tracking'; 145 clearHold(); 146 return; 147 } 148 149 // >=阈值:提示“保持”,并且——关键:每次 move 都重置 timer 150 showHint('保持不动即可刷新…', false); 151 state = 'armed'; 152 153 clearHold(); 154 holdTimer = setTimeout(function(){ 155 // 只有用户真的“停住不动”了 HOLD_MS 才触发 156 if (state === 'armed' && dy >= PULL_PX){ 157 doRefresh(); 158 } 159 }, HOLD_MS); 160 161 }, {passive:true}); // passive:true:不阻止滚动,完全放行给外层 162 163 document.addEventListener('touchend', function(){ 164 if (state === 'armed' || state === 'tracking'){ 165 hideHint(); 166 clearHold(); 167 state = 'idle'; 168 } 169 // refreshing:等数据回来解锁 170 }, {passive:true}); 171 172 document.addEventListener('touchcancel', function(){ 173 if (state !== 'refreshing'){ 174 hideHint(); 175 clearHold(); 176 state = 'idle'; 177 } 178 }, {passive:true}); 179 180 // 刷新完成解锁(你 setDashboardData 里已经调用了它) 181 window.__waterRefreshDone = function(){ 182 clearHold(); 183 hideHint(); 184 state = 'idle'; 185 }; 186 })(); 187 188 189 190 // ====== waterIframeReady:握手重试(不依赖 load,立即执行)====== 191 (function(){ 192 if (window.__waterReadyInited) return; // 防重复 193 window.__waterReadyInited = true; 194 195 var ok = false; 196 var t = null; 197 198 function onMsg(e){ 199 var d = e.data; 200 if (d && d.type === 'waterIframeAck'){ 201 ok = true; 202 try { if (t) clearInterval(t); } catch(ex){} 203 } 204 } 205 window.addEventListener('message', onMsg); 206 207 function ping(){ 208 try { window.parent.postMessage({type:'waterIframeReady'}, '*'); } catch(ex){} 209 } 210 211 // 立即发一次 + 重试 212 ping(); 213 t = setInterval(function(){ 214 if (ok){ try{ clearInterval(t); }catch(ex){}; return; } 215 ping(); 216 }, 300); 217 218 // 10 秒兜底停止 219 setTimeout(function(){ 220 try { if (t) clearInterval(t); } catch(ex){} 221 }, 10000); 222 })(); 223 224 225 226 227 228 229 230 231 window.addEventListener('load', function(){ 232 // 让提示条先出现,避免“空白” 233 if (typeof showHint === 'function') showHint('正在加载水质数据…', true); 234 235 // ★首次进入先用“初始化 JSON”渲染一遍 236 try { window.setDashboardData(__INIT_WATER_DATA__); } catch(e){} 237 238 239 }); 240 241 242 243 244 </script>
2、在mainform要初始化htmlframe中显示的数据:我很蠢,刚开始没有办法,就采用使用占位符初始显示数据(理解之后就多此一举了)
3、接下来需要在移动设备中定时刷新数据。就这简单。死活搞不定。
关键片段1:
procedure TMainmForm.UnimFormAfterShow(Sender: TObject); begin UniSession.AddJS( 'if(!window.__waterBridge){' + ' window.__waterBridge=true;' + ' window.__waterFrameWin=null;' + ' window.addEventListener("message", function(e){' + ' var d=e.data;' + ' if(!d || !d.type) return;' + // ✅ ① iframe ready:记住 e.source(iframe window)+ 回 ACK + 触发首次刷新 ' if(d.type==="waterIframeReady"){' + ' window.__waterFrameWin = e.source;' + ' try{ window.__waterFrameWin.postMessage({type:"waterIframeAck"}, "*"); }catch(ex){}' + ' var frm = (window.UniAppManager && UniAppManager.getActiveForm) ? UniAppManager.getActiveForm() : null;' + ' if(frm){ ajaxRequest(frm, "refreshWaterData", []); }' + ' return;' + ' }' + // ✅ ② 下拉触发刷新:同样记住 e.source,再刷新 ' if(d.type==="refreshWaterData"){' + ' window.__waterFrameWin = e.source;' + ' var frm2 = (window.UniAppManager && UniAppManager.getActiveForm) ? UniAppManager.getActiveForm() : null;' + ' if(frm2){ ajaxRequest(frm2, "refreshWaterData", []); }' + ' return;' + ' }' + ' });' + '}' ); end;
关键片段2:
procedure TMainmForm.UnimHTMLFrame1AjaxEvent(Sender: TComponent; EventName: string; Params: TUniStrings); var jsonStr: string; begin UniSession.Log('调用UnimFormAjaxEvent EventName=' + EventName); if EventName = 'refreshWaterData' then begin var q : TFDQuery; q := TFDQuery.Create(nil); q.Connection := MainModule.UniMainModule.FDConnection1; try q.Close; q.SQL.Text := 'select user001.fn_water_dashboard_init_json()::text as jsonStr'; // 例:刷新函数 q.Open; jsonStr := q.FieldByName('jsonStr').AsString; finally q.Free; end; UniSession.AddJS( 'if(window.__waterFrameWin){' + ' window.__waterFrameWin.postMessage({type:"dashboardData", payload:' + jsonStr + '}, "*");' + '}' ); end; end;
关键片段3:
procedure TMainmForm.unmtmr1Timer(Sender: TObject); begin UniSession.AddJS( '(function(){' + ' try{' + ' if(!window.__waterFrameWin) return;' + ' if(typeof ' + UnimHTMLFrame1.JSName + ' === "undefined") { console.log("frame js not ready"); return; }' + ' ajaxRequest(' + UnimHTMLFrame1.JSName + ', "refreshWaterData", []);' + ' }catch(e){ console.log("timer refresh err", e); }' + '})();' ); end;
下面是ai给出的总结:
我来理解:mainform--htmlFrame之间要搭桥。我们在通过maimform操作,就能到达htmlFrame,是不是?
对,你理解到点子上了。
在 UniGUI 里你可以把它想成两层世界:
-
MainForm(UniGUI/ExtJS 世界):按钮、工具条、定时器、ajaxRequest 都在这里。
-
UnimHTMLFrame(WebView/iframe 世界):你那段 HTML/JS 在这里跑,
window.setDashboardData()也在这里。
这两层默认互相“看不见”,所以必须“搭桥”。
这座桥到底是什么?
桥的核心就是:MainForm 拿到 HTMLFrame 对应的 window 引用,也就是你代码里的:
window.__waterFrameWin = e.source;
这句的意义是:
“以后 MainForm 想给 HTMLFrame 发消息,就用这个 window 去 postMessage。”
没有它,你 MainForm 就只能在外面干瞪眼:发不进去、推不进 iframe。
桥怎么搭起来?
必须让 HTMLFrame 主动喊一声“我在这”:
HTML 里:
MainForm 里监听到后:
-
记住
e.source(这就是 iframe 的 window) -
回一个 ACK(可选但强烈建议)
-
立刻触发一次
ajaxRequest(..., "refreshWaterData")
这一步之后,桥就通了。
搭桥后 MainForm 能做什么?
✅ MainForm 可以“到达” HTMLFrame:
-
定时器:自动拉数据 →
postMessage推给 HTMLFrame → HTMLFrame 更新 UI -
按钮:手动刷新 → 同样走一条链路
-
HTMLFrame 下拉:iframe 发
refreshWaterData→ MainForm 收到 → 拉数据 → 回推
这就是你看到的现象:
“下拉一次后,按钮/定时器才工作”
因为下拉那次刚好把桥(__waterFrameWin)建立了。
最关键的一句话
你不是“通过 MainForm 直接操作 HTMLFrame DOM”,而是:
MainForm 通过桥(保存的 iframe window)给 HTMLFrame 发消息,让 HTMLFrame 自己更新。
这才是 UniGUI 里最稳、最干净的玩法。
接下来你只要记住一个原则:任何能触发刷新数据的动作,都必须保证桥已建立(__waterFrameWin != null),就不会再出现“某些情况下能刷,某些情况下不能刷”的鬼故事了。
浙公网安备 33010602011771号