17
头图

Once and for all click constraint solution

R&D background, what problem to solve

  • Click Constraint: After a button triggers a click, it cannot be triggered again until the interface call has a result. Avoid multiple clicks by the user and trigger multiple interface calls.
  • General solution: For each button, define a variable to record its click state, and control the clickable state of the button through the variable. Such as in the element library <el-button type="primary" :loading="true">Loading</el-button> . Controlled by the loading variable.
  • Problems with conventional solutions:

    • The variables are redundant, and a variable needs to be defined for each button to record its state, and the use cost and maintenance cost are relatively high.
    • The compatibility is not strong, it depends on the element component library, and the usage method is not universal.

  • The solution in this article can solve the above problems and has the following characteristics:

    • The use cost is low, the code can be pasted and copied, no need to install npm package, only 180 lines of code (including css style and js).
    • It has strong compatibility and does not rely on third-party libraries. vue technology projects can be accessed. has nothing to do with technology, and can be used by front-end projects.
    • The implementation principle is simple, the code is not encrypted, and there is no confusion. Customized style adjustments can be made according to business needs.
    • In addition to the click constraint, the loading mask effect of the content area can also be achieved.
    • Support regular matching, realize numerical matching, precise matching and other functions

VUE custom command version online example

  • The source code of the page can be viewed through the console

has nothing to do with the architecture, native JS version online example

  • The source code of the page can be viewed through the console

VUE2 + Element UI custom version

  • Use Element UI default style, more beautiful and elegant

VUE3 + Element Plus Custom

  • Use Element Plus default style, more beautiful and elegant

Native JS version, how to use (only two steps)

  • Introduce files https://blog.luckly-mjw.cn/tool-show/vue-loading/dom-loading.js before all js libraries

    • It is recommended to download to local storage
    • Introduced before all js libraries to listen to XMLHttpRequest
  • On the target dom, add the data-loading attribute
    <div class="btn" data-loading="'get::loading::http://yapi.luckly-mjw.cn/mock/50/test/users?pageIndex=1'">Send a single request loading </div>

    • Data-loading parameter format introduction

      • data-loading="'get::loading::/mock/50/test/users'"
      • Where "get" indicates the type of interface request to be monitored
      • "loading" identifies the loading style when the constraint is clicked. There are three kinds of one, namely "loading", "waiting" and "disabled". It can be left blank, the default is "waiting".
      • "/mock/50/test/users" identifies the interface name that needs to be monitored. The essence is to perform string matching through url.indexOf(targetUrl) and indexOf.
      • Supports regular number matching, "/mock/\\d+/test/users" is equivalent to "/mock/50/test/users"
    • Listen for multiple requests

      • <div data-loading="['get::waiting::/test/users?pageIndex=2', 'get::/test/users?pageIndex=1']"></div>
      • In the form of an array, multiple requests that need to be monitored are passed in.
      • The loading style will be displayed when the interface is called, and will not be eliminated until all the interface requests that initiate the request are called.
      • The interface that does not initiate the request will not be affected even if it is written in the array.
      • The second data of the array does not specify the loading style of the second parameter. This parameter is optional, and the default style is "waiting"
  • ajax path regex match

    • [Value matching] "/mock/\\d+/test/users", where "\\d+" matches multiple values, and "/mock/\\d+/test/users" matches "/mock/50/test" /users”
    • 【Exact match】End by adding "$" at the end, such as "/mock/50/test$" will ignore the match of "/mock/50/test/users". Only "/mock/50/test" will hit the match
  • Pass in a request configuration function like axios

    • <div class="btn" v-waiting="importAxiosFun" @click="ajaxFromImport">pass request function</div>
    • The underlying implementation principle is to call Function.prototype.toString() to obtain the source code of the request configuration function, and to parse the request configuration parameters in the function
    • It is suitable for projects that independently extract and manage interface request configurations, and shield specific request URL paths.
    • 【Special attention】Due to the limitation of Function.prototype.toString , the function whose this has been modified by bind cannot be parsed. For example, for a function defined in vue methods, toString gets only function () { [native code] } , and the function source code cannot be obtained. However, this problem does not exist in general interface management functions.
    • The native JS version does not currently support this function

VUE custom command version, how to use (only two steps)

  • Register a global custom instruction (the amount of code is small, and the style should be customized. Therefore, no npm package is provided, just copy the code directly)

    /** 核心代码,监听 ajax,自动为匹配到的 dom 元素添加点击约束  **/
    // eslint-disable-next-line
    // <div id="1" v-waiting="['get::waiting::/test/users?pageIndex=2', 'get::/test/users?pageIndex=1']" @click="test"></div>
    // <div id="2" v-waiting="'get::loading::http://yapi.luckly-mjw.cn/mock/50/test/users?pageIndex=2'" @click="test">copy</div>
    // <div id="3" v-waiting="'get::disable::http://yapi.luckly-mjw.cn/mock/50/test/users?pageIndex=2'" @click="test">copy</div>
    // <div v-waiting="userApi.postLogin" @click="test1">兼容传入 axios 请求函数</div>
    
    // 兼容转换传入的函数,转化为 URL 字符串
    function cmptFunStrToUrl(targetList) {
    targetList = Array.isArray(targetList) ? targetList : [targetList] // 兼容转化为数组
    return targetList.map(targetItem => {
      if (typeof targetItem === 'function') { // 如果传入的是函数
        const funStr = targetItem.toString() // 将函数转化为字符串,进行解析
        if (funStr === 'function () { [native code] }') {
          throw new Error(`点击约束,因 Function.prototype.toString 限制,this 被 bind 修改过的函数无法解析, 请显式输入 url 字符串。 ${targetItem.name},详情可参考 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/toString`)
        }
        const [, method, apiURL] = (funStr.match(/\.(get|post|delete|put|patch)\(['"`]([^'"`]+)/) || [])
        if (!method || !apiURL) {
          throw new Error(`点击约束,传入的函数解析失败, ${targetItem.name}`)
        }
        return `${method}::${apiURL}`
      }
      return targetItem
    })
    }
    
    Vue.directive('waiting', {
    bind: (targetDom, binding) => {
      // 注入全局方法
      (function() {
        if (window.hadResetAjaxForWaiting) { // 如果已经重置过,则不再进入。解决开发时局部刷新导致重新加载问题
          return
        }
        window.hadResetAjaxForWaiting = true
        window.waittingAjaxMap = {} // 接口映射 'get::http://yapi.luckly-mjw.cn/mock/50/test/users?pageIndex=1': dom
    
        let OriginXHR = window.XMLHttpRequest
        let originOpen = OriginXHR.prototype.open
    
        // 重置 XMLHttpRequest
        window.XMLHttpRequest = function() {
          let targetDomList = [] // 存储本 ajax 请求,影响到的 dom 元素
          let realXHR = new OriginXHR() // 重置操作函数,获取请求数据
    
          realXHR.open = function(method, url, asyn) {
            Object.keys(window.waittingAjaxMap).forEach(key => {
              let [targetMethod, type, targetUrl] = key.split('::')
              if (!targetUrl) { // 设置默认类型
                targetUrl = type
                type = 'v-waiting-waiting'
              } else { // 指定类型
                type = `v-waiting-${type}`
              }
              if (
                targetMethod.toLocaleLowerCase() === method.toLocaleLowerCase()
                && (url.indexOf(targetUrl) > -1 || new RegExp(targetUrl).test(url))
              ) {
                targetDomList = [...window.waittingAjaxMap[key], ...targetDomList]
                window.waittingAjaxMap[key].forEach(dom => {
                  if (!dom.classList.contains(type)) {
                    dom.classList.add('v-waiting', type)
                    if (window.getComputedStyle(dom).position === 'static') { // 如果是 static 定位,则修改为 relative,为伪类的绝对定位做准备
                      dom.style.position = 'relative'
                    }
                  }
                  dom.waitingAjaxNum = dom.waitingAjaxNum || 0 // 不使用 dataset,是应为 dataset 并不实时,在同一个时间内,上一次存储的值不能被保存
                  dom.waitingAjaxNum++
                })
              }
            })
            originOpen.call(realXHR, method, url, asyn)
          }
    
          // 监听加载完成,清除 waiting
          realXHR.addEventListener('loadend', () => {
            targetDomList.forEach(dom => {
              dom.waitingAjaxNum--
              dom.waitingAjaxNum === 0 && dom.classList.remove(
                'v-waiting',
                'v-waiting-loading',
                'v-waiting-waiting',
                'v-waiting-disable',
              )
            })
          }, false)
          return realXHR
        }
      })();
    
      // 注入全局 css
      (() => {
        if (!document.getElementById('v-waiting')) {
          let code = `
         .v-waiting {
      pointer-events: none;
      /*cursor: not-allowed; 与 pointer-events: none 互斥,设置 pointer-events: none 后,设置鼠标样式无效 */
    }
    .v-waiting::before {
      position: absolute;
      content: '';
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      opacity: 0.7;
      z-index: 9999;
      background-color: #ffffff;
    }
    .v-waiting-waiting::after {
      position: absolute;
      content: '数据加载中';
      top: 50%;
      left: 0;
      width: 100%;
      max-width: 100vw;
      color: #666666;
      font-size: 20px;
      text-align: center;
      transform: translateY(-50%);
      z-index: 9999;
      animation: v-waiting-v-waiting-keyframes 1.8s infinite linear;
    }
     @-webkit-keyframes v-waiting-v-waiting-keyframes {
      20% {
        content: '数据加载中.';
      }
      40% {
        content: '数据加载中..';
      }
      60% {
        content: '数据加载中...';
      }
      80% {
        content: '数据加载中...';
      }
    }
    .v-waiting-loading::after {
      position: absolute;
      content: '';
      left: 50%;
      top: 50%;
      width: 30px;
      height: 30px;
      z-index: 9999;
      cursor: not-allowed;
      animation: v-waiting-v-loading-keyframes 1.1s infinite linear;
      background-position: center;
      background-size: 30px 30px;
      background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAWlBMVEUAAABmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZJqDNWAAAAHXRSTlMAgOKKPykV1K5JDbIf9OzNvGHGpZ5lNi8Hl3RVbc989bAAAAE8SURBVEjH5ZRZcsMgEEQR2li0WbuXvv81k5ARTllAQZV/Un5fAnWbYdwj9iaKXM9Zgr7EDzxav9cw5LGGB4gq0iBArEFZtTb0lIEoQ3oNoN/MoyQ93wP62lb9rOnil9sqxO9y4YCW9mXfnxo2gVC0sannyxZoq9MN/PdsXPs56WtPm8dTT8lwYy5W6YiPadOdxbM/RL6x/4sqk+SNBupb0jxS0sLITNp5NJhlOJ4ZJSVmgiub/gLEENKTrPh7QvjaqgPQmcyPMLSBXFDYaup+fZwWRhXKNmDsppJ9InLu9JKgzwL/9jLPp2iu8Gf2jm+ml80rGbg7ducPygCi8MQOmfuEznuCfLkXGa40tTkf7E/mVKuzJtLT4nBw7piuS9/abXGUHQuHQaQapmiDTiyJWt8rFu8YWy4q9g6+AGYbJ4l/4YQUAAAAAElFTkSuQmCC);
    }
    @-webkit-keyframes v-waiting-v-loading-keyframes {
      from {
        transform: translate(-50%, -50%) rotate(0deg);
      }
      to {
        transform: translate(-50%, -50%) rotate(360deg);
      }
    }        `
          let style = document.createElement('style')
          style.id = 'v-waiting'
          style.type = 'text/css'
          style.rel = 'stylesheet'
          style.appendChild(document.createTextNode(code))
          let head = document.getElementsByTagName('head')[0]
          head.appendChild(style)
        }
      })()
    
      // 添加需要监听的接口,注入对应的 dom
      /*
        postLogin(body) {
          return api.post('/api/operation/user/login', body)
        }
        则传入 postLogin,会自动解析该函数的配置
        <div v-waiting="userApi.postLogin" @click="test1">兼容传入 axios 请求函数</div>
      */
      const targetUrlList = cmptFunStrToUrl(binding.value)
      targetUrlList.forEach(targetUrl => {
        window.waittingAjaxMap[targetUrl] = [targetDom, ...(window.waittingAjaxMap[targetUrl] || [])]
      })
    },
    
    // 参数变化
    update: (targetDom, binding) => {
      if (binding.oldValue !== binding.value) {
        const preTargetUrlList = cmptFunStrToUrl(binding.oldValue)
        preTargetUrlList.forEach(targetUrl => {
          const index = (window.waittingAjaxMap[targetUrl] || []).indexOf(targetDom)
          index > -1 && window.waittingAjaxMap[targetUrl].splice(index, 1)
        })
    
        // 添加需要监听的接口,注入对应的 dom
        const targetUrlList = cmptFunStrToUrl(binding.value)
        targetUrlList.forEach(targetUrl => {
          window.waittingAjaxMap[targetUrl] = [targetDom, ...(window.waittingAjaxMap[targetUrl] || [])]
        })
      }
    },
    
    // 指令被卸载,消除消息监听
    unbind: (targetDom, binding) => {
      const targetUrlList = typeof binding.value === 'object' ? binding.value : [binding.value]
      targetUrlList.forEach(targetUrl => {
        const index = window.waittingAjaxMap[targetUrl].indexOf(targetDom)
        index > -1 && window.waittingAjaxMap[targetUrl].splice(index, 1)
        if (window.waittingAjaxMap[targetUrl].length === 0) {
          delete window.waittingAjaxMap[targetUrl]
        }
      })
    }
    })
    
  • On the target dom, add the v-waiting attribute
    <div class="btn" v-waiting="'get::loading::http://yapi.luckly-mjw.cn/mock/50/test/users?pageIndex=1'" @click="ajaxSingleTest ">Send a single request loading</div>

    • v-waiting parameter format introduction

      • v-waiting="'get::loading::/mock/50/test/users'"
      • Where "get" indicates the type of interface request to be monitored
      • "loading" identifies the loading style when the constraint is clicked. There are three kinds of one, namely "loading", "waiting" and "disabled". It can be left blank, the default is "waiting".
      • "/mock/50/test/users" identifies the interface name that needs to be monitored. The essence is to perform string matching through url.indexOf(targetUrl) and indexOf.
      • Supports regular number matching, "/mock/\\d+/test/users" is equivalent to "/mock/50/test/users"
    • Listen for multiple requests

      • <div v-waiting="['get::waiting::/test/users?pageIndex=2', 'get::/test/users?pageIndex=1']" @click="test"></div>
      • In the form of an array, multiple requests that need to be monitored are passed in.
      • The loading style will be displayed when the interface is called, and will not be eliminated until all the interface requests that initiate the request are called.
      • The interface that does not initiate the request will not be affected even if it is written in the array.
      • The second data of the array does not specify the loading style of the second parameter. This parameter is optional, and the default style is "waiting"
  • ajax path regex match

    • [Value matching] "/mock/\\d+/test/users", where "\\d+" matches multiple values, and "/mock/\\d+/test/users" matches "/mock/50/test" /users”
    • 【Exact match】End by adding "$" at the end, such as "/mock/50/test$" will ignore the match of "/mock/50/test/users". Only "/mock/50/test" will hit the match

Implementation principle

  • Rewrite "XMLHttpRequest" to implement the underlying universal monitoring of ajax, add styles when the interface is initiated, and eliminate it after returning the result.

    • To shield the differences in the tool layer, whether you use axios, jquery-ajax, or native XMLHttpRequest, you can implement monitoring.
    • Listen for different requests by string matching, url.indexOf(targetUrl) > -1 .
    • Based on practical application experience, the conflict of interfaces with the same name is not considered for the time being.
    • Students who need it can submit "issues", and the author will give feedback in time.
  • The display of loading content is displayed through pseudo-class elements "::before" and "::after".

    • Among them, "::before" realizes the mask layer effect,
    • Use the content of the "::after" element to display "Loading..." and "Rotating loading icon"
    • No need to insert new dom elements
    • Reduce impact on dom layout

Native JS version, the source code is as follows

// 注入全局方法
(function() {
  if (window.hadResetAjaxForWaiting) { // 如果已经重置过,则不再进入。解决开发时局部刷新导致重新加载问题
    return
  }
  window.hadResetAjaxForWaiting = true
  window.waittingAjaxMap = {} // 接口映射 'get::http://yapi.luckly-mjw.cn/mock/50/test/users?pageIndex=1': dom

  let OriginXHR = window.XMLHttpRequest
  let originOpen = OriginXHR.prototype.open
  let isSameSpace = false // 是否在同一个宏任务中,避免频繁触发

  // 检测使用到的 dom 对象
  function checkDom() {
    if (!isSameSpace) { // 节流
      isSameSpace = true
      window.waittingAjaxMap = {} // 重置为空,重新寻找匹配的 dom
      const domList = document.querySelectorAll('[data-loading]')
      Array.prototype.forEach.call(domList, targetDom => {
        targetDom.dataset.loading.split(',').forEach(targetUrl => {
           targetUrl = targetUrl
              .replace(/['"[\]]/ig, '') // 去除冗余字符
              .replace(/\\\\/ig, '\\').trim() // 将双反斜杠转为单反斜杠,适配原生模式正则匹配
          window.waittingAjaxMap[targetUrl] = [targetDom, ...(window.waittingAjaxMap[targetUrl] || [])]
        })
      })
      setTimeout(() => isSameSpace = false) // 下一个宏任务中,重新开放该方法
    }
  }

  // 重置 XMLHttpRequest
  window.XMLHttpRequest = function() {
    let targetDomList = [] // 存储本 ajax 请求,影响到的 dom 元素
    let realXHR = new OriginXHR() // 重置操作函数,获取请求数据

    realXHR.open = function(method, url) {
      checkDom()
      Object.keys(window.waittingAjaxMap).forEach(key => {
        let [targetMethod, type, targetUrl] = key.split('::')
        if (!targetUrl) { // 设置默认类型
          targetUrl = type
          type = 'v-waiting-waiting'
        } else { // 指定类型
          type = `v-waiting-${type}`
        }
         if (
           targetMethod.toLocaleLowerCase() === method.toLocaleLowerCase()
           && (url.indexOf(targetUrl) > -1 || new RegExp(targetUrl).test(url))
         ) {
          targetDomList = [...window.waittingAjaxMap[key], ...targetDomList]
          window.waittingAjaxMap[key].forEach(dom => {
            if (!dom.classList.contains(type)) {
              dom.classList.add('v-waiting', type)
              if (window.getComputedStyle(dom).position === 'static') { // 如果是 static 定位,则修改为 relative,为伪类的绝对定位做准备
                dom.style.position = 'relative'
              }
            }
            dom.waitingAjaxNum = dom.waitingAjaxNum || 0 // 不使用 dataset,是应为 dataset 并不实时,在同一个时间内,上一次存储的值不能被保存
            dom.waitingAjaxNum++
          })
        }
      })
      console.log(url)
      originOpen.call(realXHR, method, url)
    }

    // 监听加载完成,清除 waiting
    realXHR.addEventListener('loadend', () => {
      targetDomList.forEach(dom => {
        dom.waitingAjaxNum--
        dom.waitingAjaxNum === 0 && dom.classList.remove(
          'v-waiting',
          'v-waiting-loading',
          'v-waiting-waiting',
          'v-waiting-disable',
        )
      })
    }, false)
    return realXHR
  }
})();

// 注入全局 css
(() => {
  if (!document.getElementById('v-waiting')) {
    let code = `
       .v-waiting {
    pointer-events: none;
    /*cursor: not-allowed; 与 pointer-events: none 互斥,设置 pointer-events: none 后,设置鼠标样式无效 */
  }
  .v-waiting::before {
    position: absolute;
    content: '';
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    opacity: 0.7;
    z-index: 9999;
    background-color: #ffffff;
  }
  .v-waiting-waiting::after {
    position: absolute;
    content: '数据加载中';
    top: 50%;
    left: 0;
    width: 100%;
    max-width: 100vw;
    color: #666666;
    font-size: 20px;
    text-align: center;
    transform: translateY(-50%);
    z-index: 9999;
    animation: v-waiting-v-waiting-keyframes 1.8s infinite linear;
  }
   @-webkit-keyframes v-waiting-v-waiting-keyframes {
    20% {
      content: '数据加载中.';
    }
    40% {
      content: '数据加载中..';
    }
    60% {
      content: '数据加载中...';
    }
    80% {
      content: '数据加载中...';
    }
  }
  .v-waiting-loading::after {
    position: absolute;
    content: '';
    left: 50%;
    top: 50%;
    width: 30px;
    height: 30px;
    z-index: 9999;
    cursor: not-allowed;
    animation: v-waiting-v-loading-keyframes 1.1s infinite linear;
    background-position: center;
    background-size: 30px 30px;
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAWlBMVEUAAABmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZJqDNWAAAAHXRSTlMAgOKKPykV1K5JDbIf9OzNvGHGpZ5lNi8Hl3RVbc989bAAAAE8SURBVEjH5ZRZcsMgEEQR2li0WbuXvv81k5ARTllAQZV/Un5fAnWbYdwj9iaKXM9Zgr7EDzxav9cw5LGGB4gq0iBArEFZtTb0lIEoQ3oNoN/MoyQ93wP62lb9rOnil9sqxO9y4YCW9mXfnxo2gVC0sannyxZoq9MN/PdsXPs56WtPm8dTT8lwYy5W6YiPadOdxbM/RL6x/4sqk+SNBupb0jxS0sLITNp5NJhlOJ4ZJSVmgiub/gLEENKTrPh7QvjaqgPQmcyPMLSBXFDYaup+fZwWRhXKNmDsppJ9InLu9JKgzwL/9jLPp2iu8Gf2jm+ml80rGbg7ducPygCi8MQOmfuEznuCfLkXGa40tTkf7E/mVKuzJtLT4nBw7piuS9/abXGUHQuHQaQapmiDTiyJWt8rFu8YWy4q9g6+AGYbJ4l/4YQUAAAAAElFTkSuQmCC);
  }
  @-webkit-keyframes v-waiting-v-loading-keyframes {
    from {
      transform: translate(-50%, -50%) rotate(0deg);
    }
    to {
      transform: translate(-50%, -50%) rotate(360deg);
    }
  }        `
    let style = document.createElement('style')
    style.id = 'v-waiting'
    style.type = 'text/css'
    style.rel = 'stylesheet'
    style.appendChild(document.createTextNode(code))
    let head = document.getElementsByTagName('head')[0]
    head.appendChild(style)
  }
})()

VUE custom command version, the source code is as follows

/** 核心代码,监听 ajax,自动为匹配到的 dom 元素添加点击约束  **/
// eslint-disable-next-line
// <div id="1" v-waiting="['get::waiting::/test/users?pageIndex=2', 'get::/test/users?pageIndex=1']" @click="test"></div>
// <div id="2" v-waiting="'get::loading::http://yapi.luckly-mjw.cn/mock/50/test/users?pageIndex=2'" @click="test">copy</div>
// <div id="3" v-waiting="'get::disable::http://yapi.luckly-mjw.cn/mock/50/test/users?pageIndex=2'" @click="test">copy</div>
// <div v-waiting="userApi.postLogin" @click="test1">兼容传入 axios 请求函数</div>

// 兼容转换传入的函数,转化为 URL 字符串
function cmptFunStrToUrl(targetList) {
  targetList = Array.isArray(targetList) ? targetList : [targetList] // 兼容转化为数组
  return targetList.map(targetItem => {
    if (typeof targetItem === 'function') { // 如果传入的是函数
      const funStr = targetItem.toString() // 将函数转化为字符串,进行解析
      if (funStr === 'function () { [native code] }') {
        throw new Error(`点击约束,因 Function.prototype.toString 限制,this 被 bind 修改过的函数无法解析, 请显式输入 url 字符串。 ${targetItem.name},详情可参考 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/toString`)
      }
      const [, method, apiURL] = (funStr.match(/\.(get|post|delete|put|patch)\(['"`]([^'"`]+)/) || [])
      if (!method || !apiURL) {
        throw new Error(`点击约束,传入的函数解析失败, ${targetItem.name}`)
      }
      return `${method}::${apiURL}`
    }
    return targetItem
  })
}

Vue.directive('waiting', {
  bind: (targetDom, binding) => {
    // 注入全局方法
    (function() {
      if (window.hadResetAjaxForWaiting) { // 如果已经重置过,则不再进入。解决开发时局部刷新导致重新加载问题
        return
      }
      window.hadResetAjaxForWaiting = true
      window.waittingAjaxMap = {} // 接口映射 'get::http://yapi.luckly-mjw.cn/mock/50/test/users?pageIndex=1': dom

      let OriginXHR = window.XMLHttpRequest
      let originOpen = OriginXHR.prototype.open

      // 重置 XMLHttpRequest
      window.XMLHttpRequest = function() {
        let targetDomList = [] // 存储本 ajax 请求,影响到的 dom 元素
        let realXHR = new OriginXHR() // 重置操作函数,获取请求数据

        realXHR.open = function(method, url, asyn) {
          Object.keys(window.waittingAjaxMap).forEach(key => {
            let [targetMethod, type, targetUrl] = key.split('::')
            if (!targetUrl) { // 设置默认类型
              targetUrl = type
              type = 'v-waiting-waiting'
            } else { // 指定类型
              type = `v-waiting-${type}`
            }
            if (
              targetMethod.toLocaleLowerCase() === method.toLocaleLowerCase()
              && (url.indexOf(targetUrl) > -1 || new RegExp(targetUrl).test(url))
            ) {
              targetDomList = [...window.waittingAjaxMap[key], ...targetDomList]
              window.waittingAjaxMap[key].forEach(dom => {
                if (!dom.classList.contains(type)) {
                  dom.classList.add('v-waiting', type)
                  if (window.getComputedStyle(dom).position === 'static') { // 如果是 static 定位,则修改为 relative,为伪类的绝对定位做准备
                    dom.style.position = 'relative'
                  }
                }
                dom.waitingAjaxNum = dom.waitingAjaxNum || 0 // 不使用 dataset,是应为 dataset 并不实时,在同一个时间内,上一次存储的值不能被保存
                dom.waitingAjaxNum++
              })
            }
          })
          originOpen.call(realXHR, method, url, asyn)
        }

        // 监听加载完成,清除 waiting
        realXHR.addEventListener('loadend', () => {
          targetDomList.forEach(dom => {
            dom.waitingAjaxNum--
            dom.waitingAjaxNum === 0 && dom.classList.remove(
              'v-waiting',
              'v-waiting-loading',
              'v-waiting-waiting',
              'v-waiting-disable',
            )
          })
        }, false)
        return realXHR
      }
    })();

    // 注入全局 css
    (() => {
      if (!document.getElementById('v-waiting')) {
        let code = `
       .v-waiting {
    pointer-events: none;
    /*cursor: not-allowed; 与 pointer-events: none 互斥,设置 pointer-events: none 后,设置鼠标样式无效 */
  }
  .v-waiting::before {
    position: absolute;
    content: '';
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    opacity: 0.7;
    z-index: 9999;
    background-color: #ffffff;
  }
  .v-waiting-waiting::after {
    position: absolute;
    content: '数据加载中';
    top: 50%;
    left: 0;
    width: 100%;
    max-width: 100vw;
    color: #666666;
    font-size: 20px;
    text-align: center;
    transform: translateY(-50%);
    z-index: 9999;
    animation: v-waiting-v-waiting-keyframes 1.8s infinite linear;
  }
   @-webkit-keyframes v-waiting-v-waiting-keyframes {
    20% {
      content: '数据加载中.';
    }
    40% {
      content: '数据加载中..';
    }
    60% {
      content: '数据加载中...';
    }
    80% {
      content: '数据加载中...';
    }
  }
  .v-waiting-loading::after {
    position: absolute;
    content: '';
    left: 50%;
    top: 50%;
    width: 30px;
    height: 30px;
    z-index: 9999;
    cursor: not-allowed;
    animation: v-waiting-v-loading-keyframes 1.1s infinite linear;
    background-position: center;
    background-size: 30px 30px;
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAWlBMVEUAAABmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZJqDNWAAAAHXRSTlMAgOKKPykV1K5JDbIf9OzNvGHGpZ5lNi8Hl3RVbc989bAAAAE8SURBVEjH5ZRZcsMgEEQR2li0WbuXvv81k5ARTllAQZV/Un5fAnWbYdwj9iaKXM9Zgr7EDzxav9cw5LGGB4gq0iBArEFZtTb0lIEoQ3oNoN/MoyQ93wP62lb9rOnil9sqxO9y4YCW9mXfnxo2gVC0sannyxZoq9MN/PdsXPs56WtPm8dTT8lwYy5W6YiPadOdxbM/RL6x/4sqk+SNBupb0jxS0sLITNp5NJhlOJ4ZJSVmgiub/gLEENKTrPh7QvjaqgPQmcyPMLSBXFDYaup+fZwWRhXKNmDsppJ9InLu9JKgzwL/9jLPp2iu8Gf2jm+ml80rGbg7ducPygCi8MQOmfuEznuCfLkXGa40tTkf7E/mVKuzJtLT4nBw7piuS9/abXGUHQuHQaQapmiDTiyJWt8rFu8YWy4q9g6+AGYbJ4l/4YQUAAAAAElFTkSuQmCC);
  }
  @-webkit-keyframes v-waiting-v-loading-keyframes {
    from {
      transform: translate(-50%, -50%) rotate(0deg);
    }
    to {
      transform: translate(-50%, -50%) rotate(360deg);
    }
  }        `
        let style = document.createElement('style')
        style.id = 'v-waiting'
        style.type = 'text/css'
        style.rel = 'stylesheet'
        style.appendChild(document.createTextNode(code))
        let head = document.getElementsByTagName('head')[0]
        head.appendChild(style)
      }
    })()

    // 添加需要监听的接口,注入对应的 dom
    /*
      postLogin(body) {
        return api.post('/api/operation/user/login', body)
      }
      则传入 postLogin,会自动解析该函数的配置
      <div v-waiting="userApi.postLogin" @click="test1">兼容传入 axios 请求函数</div>
    */
    const targetUrlList = cmptFunStrToUrl(binding.value)
    targetUrlList.forEach(targetUrl => {
      window.waittingAjaxMap[targetUrl] = [targetDom, ...(window.waittingAjaxMap[targetUrl] || [])]
    })
  },

  // 参数变化
  update: (targetDom, binding) => {
    if (binding.oldValue !== binding.value) {
      const preTargetUrlList = cmptFunStrToUrl(binding.oldValue)
      preTargetUrlList.forEach(targetUrl => {
        const index = (window.waittingAjaxMap[targetUrl] || []).indexOf(targetDom)
        index > -1 && window.waittingAjaxMap[targetUrl].splice(index, 1)
      })

      // 添加需要监听的接口,注入对应的 dom
      const targetUrlList = cmptFunStrToUrl(binding.value)
      targetUrlList.forEach(targetUrl => {
        window.waittingAjaxMap[targetUrl] = [targetDom, ...(window.waittingAjaxMap[targetUrl] || [])]
      })
    }
  },

  // 指令被卸载,消除消息监听
  unbind: (targetDom, binding) => {
    const targetUrlList = typeof binding.value === 'object' ? binding.value : [binding.value]
    targetUrlList.forEach(targetUrl => {
      const index = window.waittingAjaxMap[targetUrl].indexOf(targetDom)
      index > -1 && window.waittingAjaxMap[targetUrl].splice(index, 1)
      if (window.waittingAjaxMap[targetUrl].length === 0) {
        delete window.waittingAjaxMap[targetUrl]
      }
    })
  }
})

Precautions

  • Since the bottom layer fills the elements through the pseudo-classes "::after" and "::before", it will overwrite the original "::after" "::before" of the dom element using the v-waiting custom instruction
  • This article only implements the overloading of "XMLHttpRequest", and does not monitor the "fetch" method. Students who have this need are welcome to mention "issues".

Universal version (agnostic to the framework, vue, react, jquery projects are all available)

  • The attribute name is changed to data-loading
  • react data-loading={'get::disable::user/ownCompany'}
  • Ordinary data-loading='get::disable::user/ownCompany'
  • To use, just load dom-loading.js file in advance in index.html

The end of the flower, thank you for reading.


momo707577045
2.4k 声望603 粉丝

[链接]