當你想實現阻止Electron視窗關閉,並彈出詢問對話方塊,提示使用者:“文章尚未儲存,是否要關閉視窗”這類業務時,那麼你99%會碰到這個BUG:
https://github.com/electron/electron/issues/24994
這是我在去年8月份發現的BUG,Electron的作者也已經確認了這個BUG,但遺憾的是現在還沒有修復。下面我們就聊聊這個問題,以及應對這個問題的方案。
問題描述
要阻止視窗關閉,必須在視窗的關閉事件中,執行preventDefault操作才行,如下程式碼所示:
- win.on("close", (e) => {
- e.preventDefault();
- });
然而這個preventDefault的操作,必須同步呼叫才能生效,所有非同步呼叫preventDefault的操作都沒有任何效果,程式碼如下所示:
- win.on("close", async (e) => {
- console.log("win close");
- await new Promise((resolve) => setTimeout(resolve, 1000));
- e.preventDefault(); //沒有任何作用
- });
上述程式碼中的preventDefault操作就不會起任何作用。這就帶來了一個業務問題:我們往往在詢問使用者並獲得使用者的允可後才會阻止視窗關閉,比如:“文章尚未儲存,您確認關閉視窗嗎?”開發者無法在這種非同步的詢問通知前執行preventDefault操作,就無法正確的阻止視窗關閉。
可能你會想到用dialog模組的showMessageBoxSync方法來完成這個詢問操作,如下程式碼所示:
- win.webContents.on('will-prevent-unload', event => {
- let choice = dialog.showMessageBoxSync({
- title:'do you want to close',
- buttons:['cancel','yes']
- });
- if(choice === 1) event.preventDefault();
- //...
- })
沒錯showMessageBoxSync是一個同步方法,但這也會導致整個主程序的JavaScript執行緒阻塞,你預期在未來發生的所有事件,以及這些事件的回撥方法,都不會再執行了(想想看,你的setInterval的回撥方法不會定時執行的結果)。
直到使用者關閉showMessageBoxSync方法開啟的視窗,主程序的JavaScript執行緒才會恢復,如果使用者永遠不做出這個選擇,那麼整個JavaScript執行緒就會一直等待下去。
應對方案
為了解決這個問題,我們可以通過額外的哨兵變數來處理,程式碼如下所示:
- //import { app, BrowserWindow, dialog } from "electron";
- let winCanBeClosedFlag = false;
- win.on("close", async (e) => {
- if (!winCanBeClosedFlag) {
- e.preventDefault();
- let choice = await dialog.showMessageBox(win, {
- title: "do you want to close",
- message: "你確定要關閉視窗嗎?",
- buttons: ["否", "是"],
- });
- if (choice.response == 1) {
- winCanBeClosedFlag = true;
- win.close();
- return;
- }
- }
- winCanBeClosedFlag = false;
- });
預設情況下winCanBeClosedFlag 這個變數的值是false,即不允許使用者關閉視窗(此處的preventDefault是同步操作),當我們詢問過使用者,並且使用者做出了確認關閉的選擇後,這個變數才會被設定為true。此時立即呼叫視窗的close方法,這個視窗的close事件被再次觸發,因為winCanBeClosedFlag 變數已經被置為true了,所以不會執行preventDefault操作,視窗被正常關閉。
視窗被關閉的同時winCanBeClosedFlag變數又被置為false,以備下一次使用者的操作。