让服务器主动更新画面

通常来说服务器能变动页面资料是因为浏览器发出 request 所得到的 response 因而更新了画面,所以一般来说如果浏览器都不发 request 服务器也无法主动送资料到浏览器。不过如果用了服务器推送 (server push) 技术,浏览器就可以不定时接受服务器的资料。

如果你自己实作,就要用 javascript 处理浏览器与服务器的通讯细节,ZK 将这过程细节封装简化用 event queue 的概念让 Java 开发者使用。

概念

你可以创建 event queue 并可以有多个发送者(publisher) 与订阅者 (subscriber)。发送者发送 event 来呼叫订阅者的倾听器,发送者跟订阅者身分可以重叠。我可以在订阅者的倾听器中送出事件给 event queue。

https://ithelp.ithome.com.tw/upload/images/20211013/20050621GwrvEmeKDC.jpg

订阅者可以注册两种事件倾听器:

同步倾听器

这种倾听器就跟控制器上的类似,会在有 Execution 可存取的情境下被呼叫,可在其中呼叫元件 setter method 来变动画面。它底层就是透过 server push 技术将服务器资料传回浏览器。

非同步倾听器

ZK 会在一个独立於 servlet thread 之外的 working thread 执行这个倾听器,因此在其中不能呼叫元件 setter method (会有 runtime error),因此主要用来执行纯资料处理的耗时动作。

范畴

ZK event queue 分成几个范畴: desktop, session, application

代表服务器可以把资料传送与推送的范围有多大,例如 session 代表发送者与订阅者必须在同一个 session,当一个发送者送出 event 时,该范畴内的所有订阅者都会收到该事件。

不中断使用者操作并执行耗时运算

前一篇提到在事件倾听器中实作一个耗时动作,会因为浏览器在等待 au request 的回应,导致浏览器那段等待期间都无法回应使用者。如果我们不想卡住使用者操作,就不能在事件倾听器中实作那个耗时动作。假如事件倾听器能把耗时动作放到另一个新的工作执行绪 (working thread) 中执行,就不用让浏览器在线等了,亦即使用者不用等待服务器执行耗时动作,立刻就能得到 au response,这样就还能跟其他UI 元件互动。等到工作执行绪中的耗时动作执行完毕,再把结果通知浏览器更新即可。

因此我可以发事件让非同步倾听器做耗时运算,完成之後再发事件到同步倾听器更新画面:

https://ithelp.ithome.com.tw/upload/images/20211013/20050621EcglS7CqM8.jpg

我用以下范例说明,假设按下该按钮会在 working thread 执行一个耗时的动作,因为是在独立的 thread 执行不会卡住 UI。但我不希望使用者在还没完成前再进行另一次耗时动作,因此如果在执行动作期间再点一次会显示「忙碌中,请等待」讯息,这也代表执行该耗时动作并没有阻挡使用者与元件互动。直到working thread 执行完毕,就会主动将「动作完成!」讯息推送到页面上。

https://ithelp.ithome.com.tw/upload/images/20211013/20050621lwPPtW8Vtd.jpg

以上的页面设计很简单,就一个按钮、一个讯息区:

<button label="非同步耗时动作"ㄥ>
<vlayout id="info" style="border:3px solid grey; width: fit-content; margin: 10px 0; padding: 5px"/>

触发执行耗时动作

控制器内的倾听器可这麽做(请看注解说明):

@Listen(Events.ON_CLICK + "=button")
public void start() {

    EventQueue queue = EventQueues.lookup(QUEUE_NAME); //新建 event queue
    //订阅非同步倾听器进行耗时运算
    queue.subscribe(new EventListener() {
        public void onEvent(Event evt) {
            if ("doLongOp".equals(evt.getName())) {
                org.zkoss.lang.Threads.sleep(3000); //模拟耗时运算
                result = "动作完成!"; //储存结果
                queue.publish(new Event("endLongOp")); //通知同步倾听器
            }
        }
    }, true); //true 代表非同步

    display("请等3秒"); //将讯息显示到页面讯息区上
    queue.publish(new Event("doLongOp")); //送出自订evet 来呼叫上面注册的非同步倾听器
}
  • EventQueues.lookup() 预设范畴为 desktop

动作完成更新画面

@Listen(Events.ON_CLICK+ "=button")
public void start() {

    EventQueue queue = EventQueues.lookup(QUEUE_NAME); //新建 event queue
    //订阅非同步倾听器进行耗时运算
    queue.subscribe(new EventListener() {
        ...
        }
    }, true); //true 代表非同步

    //注册一个同步倾听器更新画面,这里可以呼叫元件 API
    queue.subscribe(new EventListener() {
        public void onEvent(Event evt) {
            if ("endLongOp".equals(evt.getName())) {
                display(result); //show the result to the browser
                EventQueues.remove(QUEUE_NAME);
            }
        }
    }); //同步倾听器

    ...
}

void display(String msg) {
    new Label(msg).setParent(info); //呼叫元件 API 将文字加到讯息区
}
  • 当画面更新完之後,把 event queue 移掉,因此我用此来判定前一个耗时运算是否结束,也可不移除用一个同步的 flag 来显示状态。

显示忙碌中讯息

@Listen(Events.ON_CLICK+ "=button")
public void start() {
    if (EventQueues.exists(QUEUE_NAME)) {
        display("忙碌中,请等待");
        return; //busy
    }
    ...
}

强制登出同一 session 下的页面

常见使用 session 范畴 event queue 的应用就是:

一个使用者开了多了浏览器 tab,当他在其中一个 tab 登出之後,就强制将其他 tab (同一个 session)也登出,以免他误以为别的 tab 仍可以操作,或是忘了关 tab 有安全资讯泄露的问题。

因此每个页面控制器都预设要订阅一个 session scope event queue,并在其倾听器实作登出逻辑。

当使用者在某一个页面按下登出键时,控制器也同时发出一个自订事件到 session scope event queue,因此所有同一 session 的浏览器 tab 都会收到通知而将自己的 session 登出。

聊天室

订阅 application scope event queue 就能做到类似全站广播的效果,一个人送出,所有连上这个应用程序的人都能收到。我用聊天室的例子来说明:

两个使用者用不同的浏览器连上聊天室页面都能看到彼此的讯息:

https://ithelp.ithome.com.tw/upload/images/20211013/20050621hKn78pygQy.jpg

画面设计

为求容易理解,画面主要就是一个输入元件,另一个显示讯息区:

<window title="Chat" border="normal">
    <textbox onOK="post(self)" onChange="post(self)" placeholder="写入你的讯息"/>
    <separator bar="true"/>
    <vlayout id="messageHistory"/>
</window>

订阅 event queue


//创建 queue 时要指定 application 范畴
EventQueue queue = EventQueues.lookup("chat", EventQueues.APPLICATION, true);
queue.subscribe(new EventListener() {
	public void onEvent(Event evt) {
		new Label(evt.getData()).setParent(messageHistory);
	}
});
  • evt.getData() 存放使用者输入的讯息
  • 每次都将讯息转成 Label 元件,呼叫 setParent() 加到 messageHistory

送事件到 event queue

将使用者输入的讯息用 publish() 送入 event queue

String userName = "user " + session.getNativeSession().getId().substring(6, 10);
public void post(Textbox tb) {
   String text = tb.value;
   if (text.length() > 0) {
      tb.value = "";
      queue.publish(new Event("onChat", null, userName +": " + text));
   }
}

event queue 除了可让你轻易使用 server push,也能做为各个范畴内控制器之间的多对多沟通管道。


<<:  用 Python 畅玩 Line bot - 09:Video message

>>:  关於code signing [程序码签章] 这档事 ...

[Day18] 跟我一起从头学 React 吧!Let's start learning React from Codecademy! ~ Intro to JSX 篇

前言 前面文章提到发现在 Codecademy 上面有 React 的教学! Codecademy ...

D4 第二周 (回忆篇)

今天会是比较划水的回忆篇,可以斟酌看看。 这周开始正式学习 javascript,然後那时候疫情还没...

[13th][Day9] docker image-1

docker 将这样的 file system 称为 image(映像/镜像)。一个 image 可...

Day2:非同步执行与 Callback 的问题

在前一篇文章中,我们知道依据程序的执行顺序分成两种执行方式,一种是同步(Synchronous) 、...

Day 2 (html)

特别叮嘱禁止的错误 1.不要行内包区块 行内:(inline) span 区块:(block) p ...