【手把手】Element UI&Plus里Loading的极致封装✨!只需0.5行超简洁使用

13,076 阅读6分钟

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

📖阅读须知,阅读本文你将学会以下知识:
1. 顺便复习下ElLoading的常规用法
2. 如何一步步将业务页面中常见的长达10行的代码块,通过封装浓缩成0.5行(不骗人)
3. 初步了解函数式编程思路

一、好用的ElLoading

作为Element-UIElement-Plus的忠实迷弟,我已经深度使用它们四年之久。
客观来评价,ElLoading组件完全配得上简单易用四个字。

用法一v-loading指令模式

<div v-loading="true">
    <h1>Hello</h1>
    <p>World</p>
</div>

效果:
loading1.gif

用法二ElLoading.service服务模式

const loadingInstance = ElLoading.service({text: '转一转' })
setTimeout(() => {
  // 关闭全局Loading
  loadingInstance.close()
}, 1000)

效果:
loading1.gif 尤其是上面这种用法二函数调用的形式做到了可以不定义响应式对象,降低页面逻辑复杂程度,是业务开发的利器之一。

二、最常见的ElLoading使用方式

如果你也经常写业务,那下面这段代码你一定不陌生吧🙂:

let loading
try {
  loading = ElLoading.service({
  lock: true,
  text: '转一转',
  background: 'rgba(0, 0, 0, 0.1)'
})
  await queryXXXX(params)
  loading.close()
} catch(err) {
  throw(err)
  loading.close()
}

发现问题的关键所在了吗?

2.1 问题一占用的篇幅实在太长了!!

好家伙,细细一数,仅仅是和loading相关的代码,就有足足6行,而且为了关闭这个loading,有时候我们还得再花4行来写:

try {
} catch(err) {
throw(err)
}

这样算来,经常我们仅仅是为了实现"await queryXXXX(params)时会有全屏loading效果"这么一件微不足道的事情,花费了整整4-10行代码。

如果当这样的逻辑在代码里更多时,我们甚至可能需要在一个.vue文件内花费几十行的代码处理这件事。

咀嚼.jpg

2.2 问题二粗心的同事狠可怕

上图这种写法虽然需要不停地+10行、+10行、+10行,但这并不是这种写法最可怕的地方。
如果你有一个同事,他不仅对代码没追求,对稳定性质量也没追求,经常写出如下这种代码:

  const loading = ElLoading.service({text: '转一转' })
  await queryXXXX(params)
  loading.close()

好家伙,我直接好家伙。
一旦queryXXXX(params) throw ERROR,你的整个前端项目都会被loading永远镇压,只能无奈按F5刷新页面了。

2.3 问题三需要不停地传入一模一样的配置

Element-UI和Element-Plus是面向所有后端业务的开发库,因此它需要支持各种各样的场景,和你的业务场景也许并不完全贴合。
比如,在我的业务中,我总是需要传入如下配置:

ElLoading.service(
    {
      lock: true,
      text: '正在加载',
      background: 'rgba(0, 0, 0, 0.1)'
    }
)

万一你司原来的UI同学离职了,来了个审美完全不同的新UI,好家伙,整个项目改几百个地方的配置....想想就酸爽。

三、动手封装ElLoading

如果你是一个没有代码洁癖,且认为代码能运行就行的同学,那么故事到此也就结束了。
但我相信屏幕前的你,也一定和我一样,对如何更优雅、更高效的完成工作有更高的追求,那就一定会对目前这种常见+10行、+10行、+10行且伴随着定时炸弹的写法表示不满的。
开始封装吧!

3.1 封装痛点一如何解决代码行数太多的问题

对此,我的思路是使用函数式编程,将所有的细节隐藏到函数内部。
设想中封装后的使用效果如下:

import withLoading from './loading'
// 将await queryXXXX(params)变成下方写法
await withLoading(queryXXXX)(params)
// 嘿嘿,这一行关于loading的逻辑,说自己是0.5行没毛病吧

估计有一些对函数式编程不太熟悉的同学,可能看到这行代码直接就要愣一秒,并且直接变成"问号脸.jpg"。

594654446b92054eb162b5a61f46d4a8.jpg

那么我换个写法,也许能更清晰一点:

// 将之前queryXXXX变成一个新的方法
const queryXXXWithLoading = withLoading(queryXXXX);
// 再按以前的方式来调用它
await queryXXXWithLoading(params)

这里不得不感叹一句,函数式编程的思想实在是太棒了,针对函数本身进行编程的思路实在是绝中绝。

那应该怎么实现上面withLoading的效果呢?如下:

// 传入一个方法
export const withLoading = (fn) => {
    // 创建一个新方法
    const newFn = (...args) => {
        // 当新方法被执行时,执行老方法,并返回它的返回
        // 等于新方法完全继承了老方法的入参、能力、返回值
        return fn(...args)
    }
    // 返回这个新函数
    return newFn
}

这就是一个最基础的函数式编程实例
OK,到这里我们已经确定了解决代码过长的解决方案。
那就看一看本步的后续实现代码:

// 在不考虑`错误处理`和`传入Options`的情况下,下面代码已经实现了我们上面的设计
export const withLoading = (fn) => {
  let loading;
  const showLoading = () => {
    loading = ElLoading.service()
  }
  const hideLoading = () => {
    if (loading) {
      loading.close()
    }
  }
  const newFn = (...args) => {
      showLoading()
      const result = fn(...args)
      hideLoading()
  }
  return newFn
}

3.2 封装痛点二解决默认传参和自定义传参的问题

这个点就很容易了:

// 定义一个默认配置
const defaultOptions = {
  lock: true,
  text: '正在加载',
  background: 'rgba(0, 0, 0, 0.1)'
}
// 增加第二个传参,可传入自定义配置
export const withLoading = (fn, options = {}) => {
  ...
  const showLoading = (options) => {
    loading = ElLoading.service(options)
  }
  // 进行assign
  const _options = Object.assign(defaultOptions, options)
  const newFn = (...args) => {
      showLoading(_options)
  }
  return newFn
}

3.3 封装痛点三异常处理,封装try catch

先考虑一个最简单的场景,那就是,当传入的fn为同步方法时,代码可以很简单,我们所需要做的,也仅仅是以下两点:

  • 保证在catch到Error后,关闭loading
  • 继续把catch到的Error扔出去
export const withLoading = (fn, options = {}) => {
    const newFn = (...args) => {
        try {
           let result
           showLoading(_options)
           result = fn(...args)
           hideLoading()
           return result
        } catch (err) {
          hideLoading()
          throw err
        }
    }
}

但这样很显然是不够的。
因为传入的方法可能是一个异步方法,这种情况下我们会直接返回一个Promise对象,并且立刻关闭loading
因此我们需要判断传入的方法是否是异步方法,判断方法如下:

const result = fn(...args)
// 判断返回的是否是Promise
const isPromise = result instanceof Promise

如果不是Promise,那就按上面同步逻辑进行return即可。
如果是Promise,那我们则需要进行catch,并进行错误处理

  return result
    .then((res) => {
      hideLoading()
      return res
    })
    .catch((err) => {
      hideLoading()
      throw err
    })

四、收工大吉,看看效果

写一个简单的Demo分别测试下正常异常的表现:

<template>
  <div>
    <el-button @click="queryBirds">获取人员</el-button>
    <el-button @click="queryCars">获取车辆</el-button>
  </div>
</template>
<script setup>
import { getBirds, getCars } from './mock';
import { withLoading } from './hooks/loading';
import { ElMessage } from 'element-plus';
const queryBirds = async () => {
  // 你看,一行代码都不增加
  const birds = await withLoading(getBirds)()
  ElMessage.success(birds.map(t => t.name).join())
}
const queryCars = async () => {
  try {
    // 你看,一行代码都不增加
    await withLoading(getCars)()
  } catch(err) {
    ElMessage.error(err.message)
  }
}
</script>

loading2.gif

完美!

xingye2.gif

五、源码奉上

# github 源码

github.com/zhangshichu…