當你想實現阻止Electron視窗關閉,並彈出詢問對話方塊,提示使用者:“文章尚未儲存,是否要關閉視窗”這類業務時,那麼你99%會碰到這個BUG:

https://github.com/electron/electron/issues/24994

這是我在去年8月份發現的BUG,Electron的作者也已經確認了這個BUG,但遺憾的是現在還沒有修復。下面我們就聊聊這個問題,以及應對這個問題的方案。

問題描述

要阻止視窗關閉,必須在視窗的關閉事件中,執行preventDefault操作才行,如下程式碼所示:

  1. win.on("close", (e) => {
  2. e.preventDefault();
  3. });

然而這個preventDefault的操作,必須同步呼叫才能生效,所有非同步呼叫preventDefault的操作都沒有任何效果,程式碼如下所示:

  1. win.on("close", async (e) => {
  2. console.log("win close");
  3. await new Promise((resolve) => setTimeout(resolve, 1000));
  4. e.preventDefault(); //沒有任何作用
  5. });

上述程式碼中的preventDefault操作就不會起任何作用。這就帶來了一個業務問題:我們往往在詢問使用者並獲得使用者的允可後才會阻止視窗關閉,比如:“文章尚未儲存,您確認關閉視窗嗎?”開發者無法在這種非同步的詢問通知前執行preventDefault操作,就無法正確的阻止視窗關閉。

可能你會想到用dialog模組的showMessageBoxSync方法來完成這個詢問操作,如下程式碼所示:

  1. win.webContents.on('will-prevent-unload', event => {
  2. let choice = dialog.showMessageBoxSync({
  3. title:'do you want to close',
  4. buttons:['cancel','yes']
  5. });
  6. if(choice === 1) event.preventDefault();
  7. //...
  8. })

沒錯showMessageBoxSync是一個同步方法,但這也會導致整個主程序的JavaScript執行緒阻塞,你預期在未來發生的所有事件,以及這些事件的回撥方法,都不會再執行了(想想看,你的setInterval的回撥方法不會定時執行的結果)。

直到使用者關閉showMessageBoxSync方法開啟的視窗,主程序的JavaScript執行緒才會恢復,如果使用者永遠不做出這個選擇,那麼整個JavaScript執行緒就會一直等待下去。

應對方案

為了解決這個問題,我們可以通過額外的哨兵變數來處理,程式碼如下所示:

  1. //import { app, BrowserWindow, dialog } from "electron";
  2. let winCanBeClosedFlag = false;
  3. win.on("close", async (e) => {
  4. if (!winCanBeClosedFlag) {
  5. e.preventDefault();
  6. let choice = await dialog.showMessageBox(win, {
  7. title: "do you want to close",
  8. message: "你確定要關閉視窗嗎?",
  9. buttons: ["否", "是"],
  10. });
  11. if (choice.response == 1) {
  12. winCanBeClosedFlag = true;
  13. win.close();
  14. return;
  15. }
  16. }
  17. winCanBeClosedFlag = false;
  18. });

預設情況下winCanBeClosedFlag 這個變數的值是false,即不允許使用者關閉視窗(此處的preventDefault是同步操作),當我們詢問過使用者,並且使用者做出了確認關閉的選擇後,這個變數才會被設定為true。此時立即呼叫視窗的close方法,這個視窗的close事件被再次觸發,因為winCanBeClosedFlag 變數已經被置為true了,所以不會執行preventDefault操作,視窗被正常關閉。

視窗被關閉的同時winCanBeClosedFlag變數又被置為false,以備下一次使用者的操作。