一個Electron的設計缺陷及應對方案
- 2021 年 9 月 30 日
- 筆記
- javascript/jQuery/ExtJs, Node.js
當你想實現阻止Electron窗口關閉,並彈出詢問對話框,提示用戶:「文章尚未保存,是否要關閉窗口」這類業務時,那麼你99%會碰到這個BUG:
//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,以備下一次用戶的操作。