likes
comments
collection
share

点击无限快,操作无限稳:释放前端操作的无限可能

作者站长头像
站长
· 阅读数 31

前言

  大家好,我是沐浴在曙光下的贰货道士。其实很多时候,我们没有必要重复地去修改各种loading状态,来控制页面的加载效果。为此,我们可以封装一个防抖组件,用于解决页面中千篇一律的防抖问题。本文旨在提供一种防抖思想:利用element-ui组件库提供的loading效果,使用error-first原则Promise进行二次处理,来实现页面的防抖功能。有喜欢本文的朋友,欢迎一键三连哦~

页面防抖的意义

  页面防抖是一种常用的前端优化技术,它的主要目的是减少不必要的网络请求或函数执行次数,从而降低页面的性能消耗和提升用户体验。页面防抖在某些场景下具有至关重要的意义:

  • 减少网络请求: 在某些场景下,用户可能频繁地触发某个操作(比如点击按钮),而这些操作可能会触发网络请求(例如搜索建议请求)。使用页面防抖技术可以确保用户在操作停止一段时间后才发起网络请求,避免不必要的请求频率,减轻服务器负载和网络流量。
  • 优化函数执行: 在某些场景下,页面上的某些函数可能会被频繁调用,例如窗口大小改变事件或滚动事件的处理函数。使用页面防抖技术可以确保只有在用户停止操作一段时间后才执行函数,避免频繁的函数执行,提升页面的响应速度和性能。
  • 提升用户体验: 通过页面防抖,可以减少页面上不必要的操作和请求,使用户界面更加流畅和响应迅速。用户在操作页面时,不会因为频繁的请求和函数执行而感到卡顿或延迟,从而提升用户体验和满意度。
  • 节约资源消耗: 频繁的网络请求和函数执行会消耗大量的网络带宽、服务器资源和客户端设备资源。通过页面防抖,可以有效减少不必要的资源消耗,节约服务器和客户端的资源开销,提高系统的性能和可扩展性。

防抖按钮组件封装

思路分析:

  • 提供默认插槽,用于显示按钮上的文本信息,这是组件封装的常用套路;

  • $attrs(父组件传递过来的el-button上的属性)和按钮默认配置项组合成新对象,绑定在el-button上,这也是组件封装的常用套路;

    tips: 个人习惯使用{ ...defaultOption, ...$attrs }这种展开语法。如果父组件传入的属性和默认配置项产生冲突, 传入的属性会覆盖配置的默认属性。习惯使用Object.assign的朋友,需要在第一个参数上添加{},防止defaultOption被污染。即Object.assign({}, { ...defaultOption }, { ...$attrs })

  • 核心:el-button上绑定的事件,由$listeners(父组件传递过来的el-button上的事件)和防抖组件上的点击事件组成,即防抖组件上的点击事件会覆盖父组件的点击事件。而在防抖组件的点击事件中,我们可以通过this.$listeners.click.fns(e, this)拿到父组件的点击事件,这样就可以在子组件中愉快地添加loading效果了。

具体实现:

`处理promise, 方便await使用,函数的返回遵循error-first原则`
window.awaitWrap = (promise) => {
  `error-first原则,根据这个约定,回调函数的第一个参数err,用于传递错误信息。`
  `如果没有错误发生,err为null或undefined,表示操作成功。`
  `如果发生了错误,err将包含描述错误的对象、字符串或其他适当的错误信息。`
  
  `回调函数的第二个参数,用于显示操作结果`
  if (!(promise instanceof Promise)) return [null, promise]
  return promise.then((data) => [null, data]).catch((err) => [err, null])
}
<template>
  <el-button :loading="loading" v-bind="new$attrs" v-on="new$listeners">
    <slot></slot>
  </el-button>
</template>

<script>
export default {
  data() {
    return {
      loading: false,
      `提供防抖按钮的默认配置项, 小伙伴们可以结合实际业务进行修改`
      defaultOption: {
        size: 'small',
        type: 'primary'
      }
    }
  },

  computed: {
    new$attrs({ defaultOption, $attrs }) {
      return { ...defaultOption, ...$attrs }
    },

    new$listeners({ $listeners }) {
      return { ...$listeners, click: this.onclick }
    }
  },

  methods: {
    async onclick(e) {
      `如果按钮状态为true,说明此刻正在请求接口。为达到防抖效果,直接return,不需要重新请求接口`
      `如果父组件没有点击事件,也不会去请求接口,此时也直接return`
      if (this.loading || !this.$listeners.click) return
      this.loading = true
      await awaitWrap(this.$listeners.click.fns(e, this))
      this.loading = false
    }
  }
}
</script>

<style></style>

使用方法:

<loading-btn size="mini" @click="handleSave">保存</loading-btn>

export default {
  methods: {
    async handleSave() {
      `...do something you want`
    }
  }
}

tips: 如果在父组件的点击事件中,没有发起异步请求调用后端接口,则不需要特意使用loading-btn组件

全局防抖方法封装

思路分析:

具体实现:

import { Message, Loading } from 'element-ui'

`返回Promise的错误信息及结果`
window.awaitWrap = (promise) => {
  if (!(promise instanceof Promise)) return [null, promise]
  return promise.then((data) => [null, data]).catch((err) => [err, null])
}

`使用callbacks对象, 配置各类信息,这是方法封装的常用套路`
`返回Promise的处理结果,根据配置信息,提示Promise成功/失败的文案信息,以及成功/失败的回调`
window.awaitResolve = async (promise, option = false, callbacks = {}) => {
  typeof option === 'boolean' && (option = { isMsg: option })
  typeof option === 'string' && (option = { isMsg: true, sucMsg: option })
  let { isMsg, sucMsg = '操作成功', errMsg, mustCheckCode = true } = option

  callbacks = callbacks || option.callbacks
  typeof callbacks === 'function' && (callbacks = { success: callbacks })
  let { success, error } = callbacks

  let [err, res] = await awaitWrap(promise)
  if ((mustCheckCode || hasOwnProperty(res, 'code')) && !$SUC(res)) {
    `1001000: 是我们前后端约定俗成的一种特殊状态码标识`
    `$SUC(res): 全局封装的方法,用于判断接口返回的状态码是否处于200到300之间或者是否为0`
    if (err && /1001000/.test(err.code) && err.message) {
      Message.warning(err.message)
    }
    res = false
  }

  if (res) {
    isMsg && sucMsg && Message.success(sucMsg)
    typeof success === 'function' && success()
  } else {
    isMsg && errMsg && Message.warning(errMsg)
    typeof error === 'function' && error()
  }

  return res
}

`根据配置的loading提示文案,在Promise执行期间,开启全局Loading,在Promise执行完毕后,关闭Loading服务`
window.awaitLoading = async (promise, option = '请稍候') => {
  typeof option === 'string' && (option = { lock: true, text: option })
  option.customClass = (option.customClass || '') + 'zIndexMax'
  `开启全局Loading服务`
  const loading = Loading.service(option)
  await awaitWrap(promise)
  `关闭Loading服务`
  loading.close()
  `返回awaitLoading包裹的promise`
  return promise
}

`获取接口返回的结果:res.detail,是我们对axios二次封装后的data值`
window.awaitResolveDetail = async (promise) => {
  let res = await awaitResolve(promise)
  return res ? res?.detail : undefined
}

`获取接口返回的data数据,并添加全局loading效果`
window.awaitResolveDetailLoading = async (promise, option) => {
  return awaitLoading(awaitResolveDetail(promise, option))
}
// common.scss

.zIndexMax {
  z-index: 99999999 !important;
}

使用方法:

<el-button size="mini" type="primary" @click="handleSave">保存</loading-btn>

export default {
  methods: {
    async handleSave() {
      `awaitResolveDetailLoading包裹Promise请求`
      const res = awaitResolveDetailLoading(xxApi({ xxParams }))
      if(!res) return
      `...do something you want, this.doSomething()`
    }
  }
}
<el-button size="mini" type="primary" @click="handleSave">保存</loading-btn>

export default {
  methods: {
    async handleSave() {
      const valid = this.validate()
      if(!valid) return
      `假定saveHandler函数中存在两次loading,这显然是不可取的,那么我们可以将这两个loading状态合并为一个`
      `即:在这方法开始执行时,就给它loading状态`
      
      `重点: 由于async函数返回的是一个Promise对象,因此我们也可以直接将awaitResolveDetailLoading方法`
      `加在async方法上,再去掉saveHandler方法中的其他loading状态,点击保存,就只会loading一次了`
      awaitResolveDetailLoading(this.saveHandler()) 
    },
    
    async saveHandler() {
      `...做提交操作`
    }
  }
}

思维兼容发散——防抖按钮组件plus

思路分析:

  • awaitResolveDetailLoading引入到loading-btn组件中。通过loadingLock这个prop,来判断当前按钮操作,属于全局loading还是局部loading;

基于loading-btn组件的代码改动:

export default {
  `新增props`
  props: {
    loadingLock: Boolean
  },
  
  methods: {
    async onclick(e) {
      if (this.loading || !this.$listeners.click) return
      this.loading = true
      `如果父组件传递的loadingLock为true,则开启全局loading, 否则为局部loading`
      await (this.loadingLock ? awaitLoading : awaitWrap)(this.$listeners.click.fns(e, this))
      this.loading = false
    }
  }
}

结语

往期精彩推荐(强势引流):

  大概就这样吧~

  更多精彩文章正在快马加鞭创作中,敬请期待哦~