<el-button type="primary" :loading="true">加載中</el-button>
。經過 loading 變量控制。常規方案存在的問題:css
兼容性不強,依賴 element 組件庫,使用方法不通用。html
本文章解決方案可解決以上問題,具備如下特色:vue
除點擊約束外,還可實現內容區的 loading 遮罩效果。jquery
註冊全局自定義指令(代碼量較少,且應對樣式進行定製性調整。故不提供 npm 包,直接拷貝代碼便可)ios
Vue.directive('waiting', { bind: (targetDom, binding) => { // 注入全局方法 (function() { if (window.hadResetAjaxForWaiting) { // 若是已經重置過,則再也不進入。解決開發時局部刷新致使從新加載問題 return } window.hadResetAjaxForWaiting = true window.waitingAjaxMap = {} // 接口映射 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.waitingAjaxMap).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) { targetDomList = [...window.waitingAjaxMap[key], ...targetDomList] window.waitingAjaxMap[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 const targetUrlList = Array.isArray(binding.value) ? binding.value : [binding.value] targetUrlList.forEach(targetUrl => { window.waitingAjaxMap[targetUrl] = [targetDom, ...(window.waitingAjaxMap[targetUrl] || [])] }) }, // 參數變化 update: (targetDom, binding) => { if (binding.oldValue !== binding.value) { const preTargetUrlList = Array.isArray(binding.oldValue) ? binding.oldValue : [binding.oldValue] preTargetUrlList.forEach(targetUrl => { const index = (window.waitingAjaxMap[targetUrl] || []).indexOf(targetDom) index > -1 && window.waitingAjaxMap[targetUrl].splice(index, 1) }) // 添加須要監聽的接口,注入對應的 dom const targetUrlList = Array.isArray(binding.value) ? binding.value : [binding.value] targetUrlList.forEach(targetUrl => { window.waitingAjaxMap[targetUrl] = [targetDom, ...(window.waitingAjaxMap[targetUrl] || [])] }) } }, // 指令被卸載,消除消息監聽 unbind: (targetDom, binding) => { const targetUrlList = typeof binding.value === 'object' ? binding.value : [binding.value] targetUrlList.forEach(targetUrl => { const index = window.waitingAjaxMap[targetUrl].indexOf(targetDom) index > -1 && window.waitingAjaxMap[targetUrl].splice(index, 1) if (window.waitingAjaxMap[targetUrl].length === 0) { delete window.waitingAjaxMap[targetUrl] } }) } })
在目標 dom 上,添加 v-waiting 屬性<div class="btn" v-waiting="'get::loading::http://yapi.luckly-mjw.cn/mock/50/test/users?pageIndex=1'" @click="ajaxSingleTest">發送單個請求 loading</div>
web
v-waiting 參數格式介紹ajax
v-waiting="'get::loading::/mock/50/test/users'"
url.indexOf(targetUrl)
,indexOf 來進行字符串匹配。監聽多個請求npm
<div v-waiting="['get::waiting::/test/users?pageIndex=2', 'get::/test/users?pageIndex=1']" @click="test"></div>
數組的第二條數據,沒有指定第二個參數的 loading 樣式,該參數是可選的,默認樣式爲「waiting」axios
重寫 「XMLHttpRequest」,實現 ajax 的底層通用性監聽,在接口發起時添加樣式,返回結果後消除。api
url.indexOf(targetUrl) > -1
。loading 內容的展現,經過僞類元素「::before」及「::after」來顯示。
Vue.directive('waiting', { bind: (targetDom, binding) => { // 注入全局方法 (function() { if (window.hadResetAjaxForWaiting) { // 若是已經重置過,則再也不進入。解決開發時局部刷新致使從新加載問題 return } window.hadResetAjaxForWaiting = true window.waitingAjaxMap = {} // 接口映射 '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.waitingAjaxMap).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) { targetDomList = [...window.waitingAjaxMap[key], ...targetDomList] window.waitingAjaxMap[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 const targetUrlList = Array.isArray(binding.value) ? binding.value : [binding.value] targetUrlList.forEach(targetUrl => { window.waitingAjaxMap[targetUrl] = [targetDom, ...(window.waitingAjaxMap[targetUrl] || [])] }) }, // 參數變化 update: (targetDom, binding) => { if (binding.oldValue !== binding.value) { const preTargetUrlList = Array.isArray(binding.oldValue) ? binding.oldValue : [binding.oldValue] preTargetUrlList.forEach(targetUrl => { const index = (window.waitingAjaxMap[targetUrl] || []).indexOf(targetDom) index > -1 && window.waitingAjaxMap[targetUrl].splice(index, 1) }) // 添加須要監聽的接口,注入對應的 dom const targetUrlList = Array.isArray(binding.value) ? binding.value : [binding.value] targetUrlList.forEach(targetUrl => { window.waitingAjaxMap[targetUrl] = [targetDom, ...(window.waitingAjaxMap[targetUrl] || [])] }) } }, // 指令被卸載,消除消息監聽 unbind: (targetDom, binding) => { const targetUrlList = typeof binding.value === 'object' ? binding.value : [binding.value] targetUrlList.forEach(targetUrl => { const index = window.waitingAjaxMap[targetUrl].indexOf(targetDom) index > -1 && window.waitingAjaxMap[targetUrl].splice(index, 1) if (window.waitingAjaxMap[targetUrl].length === 0) { delete window.waitingAjaxMap[targetUrl] } }) } })