IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Loading ... done

    边城发表于 2023-02-26 20:23:35
    love 0

    引子

    在前面界面开发的过程中,为了增强在与后端交互过程中的用户体验,通常会显示 Loading 动画。Loading 动画会在与后端交互结束的时候关闭。这是一个很常规的需求,技术实现也不复杂。

    showLoading();
    axios.request(...)
        .then(...)
        .finally(() => hideLoading());

    Node.js 和大部分浏览器都在 2018 年实现了对 Promise.prototype.finally() 的支持。Deno 在 2020 年发布的 1.0 中也已经支持 finally() 了。即使不支持,使用 await 也很容易处理。

    showLoading()
    try {
        await axios.request(...);
    }
    finally {
        hideLoading();
    }

    而在更早的时候,jQuery 在 jqXHR 中就已经通过 always() 提供了支持。

    showLoading();
    $.ajax(...)
        .done(...)
        .always(() => hideLoading());

    拦截器中的 Loading ... done 逻辑

    接下来,为了所有接口调用的行为一致,也为了在一个地方处理相同的事情以达到复用的目的,Loading ... done 的逻辑开始被写在一些拦截器中。这对单个远程接口调用来说,没有问题。但如果有这样一个业务逻辑会怎么样:

    function async doSomething() {
        const token = await fetchToken();
        const auth = await remoteAuth(token);
        const result = await fetchBusiness(auth);
    }

    假设上面的每个调用都使用了 Axios,而 Axios 在拦截器中注入了 showLoading() 和 hideLoading() 的逻辑。那么这段代码会依次弹出三个 Loading 动画。一个业务弹多个 Loading 动画确实是个不太好的体验。

    给 Loading 记数

    其实这个问题我们可以在 showLoading() 和 hideLoading() 中去想办法。我们把这两个方法放入一个闭包环境,然后用一个变量来记录调用次数:

    const { showLoading, hideLoading } = (() => {
        let count = 0;
        function showLoading() {
            count++;
            if (count > 1) { return; }
            // TODO show loading view
        }
        function hideLoading() {
            count--;
            if (count > 1) { return; }
            // TODO hide loading view
        }
    })();

    包装业务逻辑代替拦截器方案

    作者观点

    我个人并不赞同在拦截器里去处理界面上的事情。拦截器中应该处理与请求本身强相关的事情,比如对参数的预处理,对响应的后处理等。

    我不太赞同在拦截器中去处理界面上的东西。像这种情况,可以设计一个 wrap 函数来处理 Loading 的呈现并调用通过参数传入的业务逻辑。这个 wrap 函数可以这样写:

    async function wrapLoading(fn) {
        showLoading();
        try {
            return await fn();
        }
        finally {
            hideLoading();
        }
    }

    在使用的时候可以这样用:

    // 单个远程调用,不带参数
    await wrapLoading(fetchSomething);
    
    // 单个远程调用,带参数
    await wrapLoading(() => fetchSomething(arg1, arg2, arg3));
    
    // 多个调用的组合逻辑
    const result = await wrapLoading(() => {
        const token = await fetchToken();
        const auth = await remoteAuth(token);
        return await fetchBusiness(auth);
    });

    下沉包装函数降低业务处理复杂度

    为了应用内更自由地统一化处理,建议对底层 Ajax 框架进行一次封装。业务远程调用时使用封装的接口,避免直接使用 Ajax 库接口。比如对 Axios request 进行一层封装。

    async function request(url, config) {
        config.url = url;
        return await axios.request(config);
    }

    如果需要显示 Loading,可以扩展 config,加一个 withLoading 选项:

    async function request(url, config) {
        const { withLoading, ...cfg } = config;
        cfg.url = url;
        
        if (!withLoading) { return await axios.request(cfg); }
    
        try {
            showLoading();
            return await axios.request(cfg);
        }
        finally {
            hideLoading();
        }
    }

    如果扩展的业务参数比较多,可以考虑封装成一个对象,比如 config.options,也可以给封装的 request 多加一个参数:request(url, config, options),这些实现都不难,就不细说了。

    有了这层封装之后,如果以后想更换 Ajax 框架也相对容易,只需要修改封装的 request 函数即可,做到了业务层与框架/工具的解耦。



沪ICP备19023445号-2号
友情链接