6
头图

1. Preface

Web page pop-ups are a very common function, such as when you need to inform the user of a message (Alert), when the user is required to confirm (Confirm), when the user is required to add a little information (Prompt)... You can even pop-up a box to allow users to fill in the form (Modal Dialog).

After the popup, the developer needs to know when the popup is closed for the next operation.

In the older UI components, this thing is done through event callbacks, and it looks like this:

showDialog(content, title, {
    closed: function() { console.log("对话框已关闭"); }
})

But the behavior of the dialog box. You see, it pops up, but it will not block the code behind, and the developer does not know when to close it, because this is a user behavior. Since it is asynchronous, it is await syntax to call. A simple package can be like this:

async function asyncShowDialog(content, title, options) {
    return new Promise(resolve => {
        showDialog(content, title, {
            ...options,
            closed: resolve
        });
    });
}

(async () => {
    await asyncShowDialog(content, title);
    console.log("对话框已关闭");
})();

The basic asynchronous behavior of the bullet box is as simple as that, so it ends like this? If you are unwilling, study and study!

2. Look for two bullet frame components

Since it is a research, first look for the existing wheels. Choose two here, both based on the Vue3 framework

Ant Design Vue uses the event form. Clicking the "OK" button will trigger the ok event, and clicking "Cancel" or the close button in the upper right corner will trigger the cancel event. These two event handling functions are onOk and onCancel attributes of the parameter object. It seems plain and unremarkable, but if the processing event returns a Promise object, a loading animation will appear after the button is clicked and the dialog will not be closed until the Promise object is completed. This design combines the asynchronous waiting animation into the bullet frame, which is concise and intuitive, and the code is also very convenient to write. Take the confirm dialog box as an example:

Modal.confirm({
    ...
    onOk() {
        // 点击「确定」按钮后,会显示加载动画,并在一秒后关闭对话框
        return new Promise(resolve => {
            setTimeout(resolve, 1000);
        });
    }
    ...
});

Element Plus uses the Promise form. When opening the dialog box, instead of passing in the confirmation or cancellation processing function as a parameter, it directly returns a Promise object for developers to process through .then()/.catch() or await . Example:

try {
    await ElMessageBox.confirm(...);
    // 按下确定按钮在这里处理
} catch(err) {
    // 按下取消按钮在这里处理
}

With this processing method of Element Plus, the business can only be processed after the dialog box is closed. This is also the limitation of using Promise-for an already encapsulated Promise object, it is difficult to insert new logic into it. If you are using ElMessageBox and want to perform some asynchronous operations before closing like Ant Design, you can only look for it to see if it provides processing events before closing. I found it after looking for it beforeClose incident. The signature of the event handler is beforeClose(action, instance, done) :

  • action indicates which button is pressed, the value may be "confirm" , "cancel" and "close" (no need to explain it).
  • instance is an instance of MessageBox, you can use it to control some interface effects, such as

    instance.confirmButtonLoading = true will display the loading animation on the "OK" button, instance.confirmButtonText can be used to change the button text... These operations can provide a better user experience when performing asynchronous waits.

  • done is a function, calling it indicates that beforeClose() is completed, and the dialog box can now be closed!

So a process similar to Ant Design can be written like this:

try {
    await ElMessageBox.confirm({
        ...
        beforeClose: async (action, instance, done) => {
            await new Promise(resolve => setTimeout(resolve, 1000));
            done();
        }
    });
    // 按下确定按钮在这里处理
} catch(err) {
    // 按下取消按钮在这里处理
}

3. One's own liver

After analyzing the behavior processing of two bullet frame components, we already know that a well-experienced bullet frame component should have the following characteristics:

  1. Provides Promise-based asynchronous control capabilities (Although Ant Design Vue does not provide it, it can be encapsulated as in "Preface").
  2. Allow some operations before closing, even asynchronous operations.
  3. Provide interface feedback during the asynchronous loading process, and it is best not to need the developer to control (from this point, Ant Design is more convenient than Element Plus).

captured-1.gif

Go to CodePen to see the demo code

Next, let's write one ourselves and see how to achieve the above features. However, since we are mainly studying behavior rather than data processing, we do not use the Vue framework, directly use DOM manipulation, and then introduce jQuery to simplify DOM processing.

The HTML skeleton of the dialog box is also relatively simple: the bottom layer is a mask, the upper layer is a fixed-size <div> layer, and the inside is divided into three parts: title, content, and operation area with <div>

<div class="dialog" id="dialogTemplate">
  <div class="dialog-window">
    <div class="dialog-title">对话框标题</div>
    <div class="dialog-content">对话框的内容</div>
    <div class="dialog-operation">
      <button type="button" class="ensure-button">确定</button>
      <button type="button" class="cancel-button">取消</button>
    </div>
  </div>
</div>

Here we define it as a template, and hope to clone a DOM from it every time it is rendered, and it will be destroyed when it is closed.

The content of the style sheet is longer and can be obtained from the sample link below. The code and the evolution of the code are the focus of this article.

The simplest presentation is to use jQuery to clone a display, but before displaying, be sure to delete the id attribute and add it to <body> :

$("#dialogTemplate").clone().removeAttr("id").appendTo("body").show();

Encapsulate it into a function, and add the processing of the "OK" and "Cancel" buttons:

function showDialog(content, title) {
    const $dialog = $("#dialogTemplate").clone().removeAttr("id");

    // 设置对话框的标题和内容(简单示例,所以只处理文本)
    $dialog.find(".dialog-title").text(title);
    $dialog.find(".dialog-content").text(content);

    // 通过事件代理(也可以不用代理)处理两个按钮事件
    $dialog
        .on("click", ".ensure-button", () => {
            $dialog.remove();
        })
        .on("click", ".cancel-button", () => {
            $dialog.remove();
        });

    $dialog.appendTo("body").show();
}

The basic logic of the bullet frame comes out. Now do two optimizations: ① $dialog.remove() into a function to facilitate unified processing of the closed dialog box (code reuse) ② Use .show() render it too blunt, and change it to fadeIn(200) ; for the same reason, it should be .remove() before fadeOut(200) .

function showDialog(...) {
    ...

    const destory = () => {
        $dialog.fadeOut(200, () => $dialog.remove());
    };

    $dialog
        .on("click", ".ensure-button", destroy)
        .on("click", ".cancel-button", destroy);

    $dialog.appendTo("body").fadeIn(200);
}

3.1. Encapsulate Promise

At this point, the pop-up box can pop up/close normally, but there is no way to inject the logic code of "OK" or "Cancel". As mentioned earlier, the interface can be provided in two forms, event or Promise, and Promise is used here. Click "OK" to resolve, click "Cancel" to reject.

function showDialog(...) {
    ...

    const promise = new Promise((resolve, reject) => {
        $dialog
            .on("click", ".ensure-button", () => {
                destroy();
                resolve("ok");
            })
            .on("click", ".cancel-button", () => {
                destroy();
                reject("cancel");
            });
    });

    $dialog.appendTo("body").fadeIn(200);
    return promise();
}

The encapsulation is complete, but there is a problem: destroy() is an asynchronous process, but the code does not wait for it to end, so showDialog() completes the asynchronous processing, the fadeOut() operation and the remove() operation are still performed. To solve this problem, only encapsulate destory() . Of course, don’t forget to add await when calling, and add await to declare the outer function as async :

function showDialog(...) {
    ...

    const destory = () => {
        return new Promise(resolve => {
            $dialog.fadeOut(200, () => {
                $dialog.remove();
                resolve();
            });
        });
    };

    const promise = new Promise((resolve, reject) => {
        $dialog
            .on("click", ".ensure-button", async () => {
                await destroy();
                resolve("ok");
            })
            .on("click", ".cancel-button", async () => {
                await destroy();
                reject("cancel");
            });
    });

    ...
}

3.2. Allow asynchronous waiting when determined

No matter "OK" or "Cancel", you can keep the pop-up display and wait asynchronously. But as an example, only the case of "OK" is handled here.

This asynchronous waiting process needs to be injected into the slave pop-up window, only in the form of parameter injection. It is necessary to showDialog() add a options parameter handler allowing to inject a onOk property, if the handler returns Promise Like, it waits asynchronously.

First modify the showDialog() interface:

function showDialog(conent, title, options = {}) { ... }

Then deal with the $dialog.on("click", ".ensure-button", ...) event:

$dialog
    .on("click", ".ensure-button", async () => {
        const { onOk } = options;
        // 从 options 中拿到 onOk,如果它是一个函数才需要等待处理
        if (typeof onOk === "function") {
            const r = onOk();
            // 判断 onOk() 的结果是不是一个 Promise Like 对象
            // 只有 Promise Like 对象才需要异步等待
            if (typeof r?.then === "function") {
                const $button = $dialog.find(".ensure-button");
                // 异步等待过程中需要给用户一定反馈
                // 这里偷懒没有使用加载动画,只用文字来进行反馈
                $button.text("处理中...");
                await r;
                // 因为在完成之后,关闭之前有 200 毫秒的渐隐过程,
                // 所以把按钮文本改为“完成”,给用户及时反馈是有必要的
                $button.text("完成");
            }
        }
        await destroy();
        resolve("ok");
    })

Now the behavior of this bullet box is basically processed, the example of calling:

const result = await showDialog(
    "你好,这里是对话框的内容",
    "打个招呼",
    {
        onOk: () => new Promise((resolve) => { setTimeout(resolve, 3000); })
    }
).catch(msg => msg);  // 这里把取消引起的 reject 变成 resolve,避免使用 try...catch...

console.log(result === "ok" ? "按下确定" : "按下取消");

3.3. Perfect details

There are dialog boxes. Finally, console.log(...) n't it be better to directly pop up the message?

But now showDialog() only deal with the Confirm box bomb, bomb did not deal with Alert box ...... not a big problem, in options Riga type better. If type is "alert" kill the "Cancel" button.

async function showDialog(content, title, options = {}) {
    ...
    
    if (options.type === "alert") {
        $dialog.find(".cancel-button").remove();
    }
    
    ...
}

Then, the final console.log(...) can be evolved a bit:

showDialog(result === "ok" ? "按下确定" : "按下取消", "提示", { type: "alert" });

3.4. Reform

If you don't like options , you can also inject it in the returned Promise object in another way. First in .ensure-button events in the const { onOk } = options changed const { onOk } = promise , that is, from promise get injected in onOk . Then change the calling part:

const dialog = showDialog("你好,这里是对话框的内容", "打个招呼");
// 把处理函数注入到 promise 的 onOk
dialog.onOk = () => new Promise((resolve) => { setTimeout(resolve, 3000); });
const result = await dialog.catch(msg => msg);

showDialog(result === "ok" ? "按下确定" : "按下取消", "提示", { type: "alert" });

Here are a few points to note:

  1. dialog must only be directly returned by showDialog() If you call .catch() will get another Promise object. Injecting onOk will not showDialog() the Promise object generated in 0619d7981cae73.
  2. showDialog() cannot be declared as async , otherwise the returned Promise object will not be the one generated in it.
  3. Don't forget await .

边城
59.8k 声望29.6k 粉丝

一路从后端走来,终于走在了前端!