iT邦幫忙

2021 iThome 鐵人賽

DAY 28
0
Modern Web

ZK 30天速成系列 第 28

讓伺服器主動更新畫面

通常來說伺服器能變動頁面資料是因為瀏覽器發出 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,也能做為各個範疇內控制器之間的多對多溝通管道。


上一篇
當執行一個耗時較久動作時,提供良好的使用者體驗
下一篇
響應式設計
系列文
ZK 30天速成30

尚未有邦友留言

立即登入留言