前端(vue)入門到精通課程:進(jìn)入學(xué)習(xí)
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API調(diào)試工具:點(diǎn)擊使用
準(zhǔn)備:自定義指令介紹
除了核心功能默認(rèn)內(nèi)置的指令 (
v-model
和v-show
等),Vue 也允許注冊自定義指令。注意,在 Vue2.0 中,代碼復(fù)用和抽象的主要形式是組件。然而,有的情況下,你仍然需要對普通 DOM 元素進(jìn)行底層操作,這時候就會用到自定義指令?!緦W(xué)習(xí)視頻分享:vue視頻教程、web前端視頻】
作為使用Vue
的開發(fā)者,我們對Vue
指令一定不陌生,諸如v-model
、v-on
、v-for
、v-if
等,同時Vue也為開發(fā)者提供了自定義指令的api,熟練的使用自定義指令可以極大的提高了我們編寫代碼的效率,讓我們可以節(jié)省時間開心的摸魚~
對于Vue的自定義指令相信很多同學(xué)已經(jīng)有所了解,自定義指令的具體寫法這里就不細(xì)講了,官方文檔很詳細(xì)。 但是不知道各位同學(xué)有沒有這種感覺,就是這個技術(shù)感覺很方便,也不難,我也感覺學(xué)會了,就是不知道如何去應(yīng)用。這篇文檔就是為了解決一些同學(xué)的這些問題才寫出來的。
PS:這次要講的自定義指令我們主要使用的是vue2.x
的寫法,不過vue3.x
不過是幾個鉤子函數(shù)有所改變,只要理解每個鉤子函數(shù)的含義,兩者的用法差別并不大。
試煉:實(shí)現(xiàn)v-mymodel
我的上篇文章說到要自己實(shí)現(xiàn)一個v-model
指令,這里使用v-myodel
模擬一個簡易版的,順便再領(lǐng)不熟悉的同學(xué)熟悉一下自定義指令的步驟和注意事項(xiàng)。
定義指令
首先梳理思路:原生input
控件與組件的實(shí)現(xiàn)方式需要區(qū)分,input
的實(shí)現(xiàn)較為簡單,我們先實(shí)現(xiàn)一下input
的處理。 首先我們先定義一個不做任何操作的指令
Vue.directive('mymodel', { //只調(diào)用一次,指令第一次綁定到元素時調(diào)用。在這里可以進(jìn)行一次性的初始化設(shè)置。 bind(el, binding, vnode, oldVnode) { }, //被綁定元素插入父節(jié)點(diǎn)時調(diào)用 (僅保證父節(jié)點(diǎn)存在,但不一定已被插入文檔中),需要父節(jié)點(diǎn)dom時使用這個鉤子 inserted(el, binding, vnode, oldVnode) { }, //所在組件的 VNode 更新時調(diào)用,**但是可能發(fā)生在其子 VNode 更新之前**。指令的值可能發(fā)生了改變,也可能沒有。但是你可以通過比較更新前后的值來忽略不必要的模板更新 (詳細(xì)的鉤子函數(shù)參數(shù)見下)。 update(el, binding, vnode, oldVnode) { }, //指令所在組件的 VNode **及其子 VNode** 全部更新后調(diào)用。 componentUpdated(el, binding, vnode, oldVnode) { }, 只調(diào)用一次,指令與元素解綁時調(diào)用。 unbind(el, binding, vnode, oldVnode) { }, })
上面的注釋中詳細(xì)的說明了各個鉤子函數(shù)的調(diào)用時機(jī),因?yàn)槲覀兪墙o組件上添加input
事件和value
綁定,因此我們在bind
這個鉤子函數(shù)中定義即可。所以我們把其他的先去掉,代碼變成這樣。
Vue.directive('mymodel', { //只調(diào)用一次,指令第一次綁定到元素時調(diào)用。在這里可以進(jìn)行一次性的初始化設(shè)置。 bind(el, binding, vnode, oldVnode) { } })
簡單說一下bind
函數(shù)的幾個回調(diào)參數(shù),el
是指令綁定組件對應(yīng)的dom
,binding
是我們的指令本身,包含name
、value
、expression
、arg
等,vnode
就是當(dāng)前綁定組件對應(yīng)的vnode
結(jié)點(diǎn),oldVnode
就是vnode
更新前的狀態(tài)。
接下來我們要做兩件事:
- 綁定
input
事件,同步input
的value
值到外部 value
值綁定,監(jiān)聽value
的變化,更新到input
的value
這對于input
原生組件比較容易實(shí)現(xiàn):
//第一步,添加inout事件監(jiān)聽 el.addEventListener('input', (e) => { //context是input所在的父組件,這一步是同步數(shù)據(jù) vnode.context[binding.expression] = e.target.value; }) //監(jiān)聽綁定的變量 vnode.context.$watch(binding.expression, (v) => { el.value = v; })
這里解釋一下上面的代碼,vnode.context
是什么呢,他就是我們指令所在組件的上下文環(huán)境,可以理解就是指令綁定的值所在的組件實(shí)例。不熟悉vnode
結(jié)構(gòu)的同學(xué)建議先看一下官方的文檔,不過文檔描述的比較簡單,不是很全面,所以最好在控制臺log
一下vnode
的對象看一下它具體的結(jié)構(gòu),這很有助于我們封裝自定義指令,對理解Vue
原理也很有幫助。
我們可以通過context[binding.expression]
獲取v-model上到綁定的值,同樣可以修改它。上面的代碼中我們首先通過在添加的input事件中操作vnode.context[binding.expression] = e.target.value
同步input
的value
值到外部(context
),與使用@input
添加事件監(jiān)聽效果是一樣的;然后我們需要做第二件事,做value
值的綁定,監(jiān)聽value
的變化,同步值的變更到input
的value
上,我們想到我們可以使用Vue實(shí)例上的額$watch
方法監(jiān)聽值的變化,而context
就是那個Vue
實(shí)例,binding.expression
就是我們想要監(jiān)聽的屬性,如果我們這樣寫
參考vue實(shí)戰(zhàn)視頻講解:進(jìn)入學(xué)習(xí)
<input v-mymodel='message'/>
那么binding.expression
就是字符串'message'
。所以我們想下面的代碼這樣監(jiān)聽綁定的響應(yīng)式數(shù)據(jù)。
//監(jiān)聽綁定的變量 vnode.context.$watch(binding.expression, (v) => { el.value = v; })
至此,input
的v-mymodel
的處理就完成了(當(dāng)然input
組件還有type
為checkbox
,radio
,select
等類型都需要去特別處理,這里就不再一一處理了,感興趣的同學(xué)可以自己嘗試去完善一下),但是對于非原生控件的組件,我們要特殊處理。 因此我們完善代碼如下:
Vue.directive('mymodel', { //只調(diào)用一次,指令第一次綁定到元素時調(diào)用。在這里可以進(jìn)行一次性的初始化設(shè)置。 bind(el, binding, vnode, oldVnode) { //原生input組件的處理 if(vnode.tag==='input'){ //第一步,添加inout事件監(jiān)聽 el.addEventListener('input', (e) => { //context是input所在的父組件,這一步是同步數(shù)據(jù) vnode.context[binding.expression] = e.target.value; }) //監(jiān)聽綁定的變量 vnode.context.$watch(binding.expression, (v) => { el.value = v; }) }else{//組件 } } })
接下來我們要處理的是自定義組件的邏輯,
//vnode的結(jié)構(gòu)可以參見文檔。不過我覺得最直觀的方法就是直接在控制臺打印處理 let { componentInstance, componentOptions, context } = vnode; const { _props } = componentInstance; //處理model選項(xiàng) if (!componentOptions.Ctor.extendOptions.model) { componentOptions.Ctor.extendOptions.model = { value: 'value', event: 'input' } } let modelValue = componentOptions.Ctor.extendOptions.model.value; let modelEvent = componentOptions.Ctor.extendOptions.model.event; //屬性綁定,這里直接修改了屬性,沒有想到更好的辦法,友好的意見希望可以提出 _props[modelValue] = binding.value; context.$watch(binding.expression, (v) => { _props[modelValue] = v; }) //添加事件處理函數(shù),做數(shù)據(jù)同步 componentInstance.$on(modelEvent, (v) => { context[binding.expression] = v; })
聲明一下,上面的實(shí)現(xiàn)不是vue
源碼的實(shí)現(xiàn)方式,vue
源碼中實(shí)現(xiàn)v-model
更加復(fù)雜一點(diǎn),是結(jié)合自定義指令、模板編譯等去實(shí)現(xiàn)的,因?yàn)槲覀兪菓?yīng)用級別的封裝,所以采用了上述的方式實(shí)現(xiàn)。
實(shí)現(xiàn)此v-mymodel
需要同學(xué)去多了解一下Vnode
和Component
的API
,就像之前說的,最簡單的方法就是直接在控制臺中直接打印出vnode
對象,組件的vnode
上有Component
的實(shí)例componentInstance
。
接下來簡單說一下上面的代碼,首先我們可以在componentOptions.Ctor.extendOptions
上找到model
的定義,如果沒有的話需要設(shè)置默認(rèn)值value
和input
,然后分別對想原生input
的處理一樣,分別監(jiān)聽binding.expression
的變化和modelEvent
事件即可。
需要注意的是,我們上面的代碼直接給_prop
做了賦值操作,這實(shí)際上是不符合規(guī)范的,但是我目前沒有找到更好的方法去實(shí)現(xiàn),有好思路的同學(xué)可以在評論區(qū)留言指教。
下面?是完整的源碼:
應(yīng)用實(shí)踐:4個實(shí)用的自定義指令
上文我們通過封裝v-mymodel
為各位同學(xué)展示了如何封裝和使用自定義指令,接下來我把自己在生產(chǎn)實(shí)踐中使用自定義指令的一些經(jīng)驗(yàn)分享給大家,通過實(shí)例,我相信各位同學(xué)能夠更深刻的理解如何在在應(yīng)用中封裝自己的指令,提高效率。
權(quán)限控制
下面我們定義一個v-permission
指令用于全平臺的權(quán)限控制
- role:角色控制;
- currentUser:當(dāng)前登錄人判斷;當(dāng)前用戶是否是業(yè)務(wù)數(shù)據(jù)中的創(chuàng)建人或者負(fù)責(zé)人
- bussinessStatus:業(yè)務(wù)狀態(tài)判斷;
- every:與操作;
- some:或操作;
示例代碼
//定義權(quán)限類型 const permissionType = { ROLE: 'role', CURRENTUSER:'currentUser', BUSSINESSSTATUS: 'bussinessStatus', MIX_EVERY: 'every', MIX_SOME: 'some' } export default { //只調(diào)用一次,指令第一次綁定到元素時調(diào)用 bind: function () { }, //當(dāng)前vdom插入到真實(shí)dom時,因?yàn)槭菍om的樣式操作,在這里操作 inserted: function (el, binding) { let show = false; show=processingType(binding.arg,binding.value); el.style.display = `${show ? 'inline-block' : 'none'}` }, //所在組件的VNode更新時調(diào)用,狀態(tài)更新后需要更新顯示狀態(tài) update: function (el, binding) { //避免無效的模板更新 if(binding.value===binding.oldValue) return; let show = false; show=processingType(binding.arg,binding.value); el.style.display = `${show ? 'inline-block' : 'none'}` }, //指令所在組件的 VNode 及其子 VNode 全部更新后 componentUpdated: function (el, binding) { }, unbind: function () { }, } //處理不同類型的權(quán)限控制 function processingType(type,value){ let values=[]; switch (type) { case permissionType.ROLE: return permissionByRole(value); case permissionType.CURRENTUSER: return permissionCreater(value); case permissionType.BUSSINESSSTATUS: return permissionBusinessStatus(value); case permissionType.MIX_EVERY: for(let type in value){ values.push(processingType(type,value[type])) } return values.every(v=>{ return v; }) case permissionType.MIX_SOME: for(let type in value){ values.push(processingType(type,value[type])) } return values.some(v=>{ return v; }) default: return false; } } //業(yè)務(wù)狀態(tài)判斷 function permissionBusinessStatus(bindingValue){ return bindingValue.status==bindingValue.value; } //當(dāng)前用戶? function permissionCreater(bindingValue){ const userInfo = JSON.parse(sessionStorage.CDTPcookie); // console.log(userInfo.userInfo.id,bindingValue) if(bindingValue instanceof Array){ return bindingValue.some(v=>{ return userInfo.userInfo.id==v; }) } return userInfo.userInfo.id==bindingValue; } //角色控制 export function permissionByRole(bindingValue) { //這里也可以是store里的用戶信息 const userInfo = JSON.parse(sessionStorage.userInfo); let roles = [] if (userInfo) { roles = userInfo.roleList } let show = false; if (bindingValue instanceof Array) { return roles.some(role => {//多角色處理 return bindingValue.some(item => { return role.roleCode === item }) }) } else if (typeof bindingValue == 'string') { show = roles.some(role => { return role.roleCode === bindingValue; }) } return show; }
簡單說一下上面?指令的定義思路和使用方法。整體思路就是通過processingType處理權(quán)限邏輯,使用el.style.display控制組件顯示或隱藏。我在這里從日常應(yīng)用中提取了一些通用的processingType中的權(quán)限處理方式,方便大家理解也供大家參考。
下面逐一說一下權(quán)限指令各個類型的使用方法:
//角色權(quán)限 <component v-permission:role='leader'></component> //判斷當(dāng)前登錄人 <component v-permission:currentUser='orderInfo.createUser'></component> //判斷業(yè)務(wù)狀態(tài) <component v-permission:bussinessStatus='{status:orderStatus.RUNNING,value:orderInfo.status}'></component> //角色是leader或者是當(dāng)前訂單的創(chuàng)建者,有權(quán)限 <component v-permission:some="{role:'leader',currentUser:'orderInfo.createUser'}"></component> //角色是leader并且是當(dāng)前訂單的創(chuàng)建者,有權(quán)限 <component v-permission:every="{role:'leader',currentUser:'orderInfo.createUser'}"></component>
輸入限制
v-input 輸入框限制,限制數(shù)字、保留n位小數(shù)點(diǎn)等。
export default { inserted: function (el, binding, vnode) { el.addEventListener('input', function (e) { if (binding.arg == 'toFixed') { //限制輸入n位小數(shù)點(diǎn) toFiexd(e.target, vnode, binding.value) } else { //限制數(shù)字輸入 Integer(e.target, vnode) } }) }, } function toFiexd(target, vnode, v) { console.log(v); let ln = 2; if (v) { ln = v; } var regStrs = [ ['^0(\d+)$', '$1'], //禁止錄入整數(shù)部分兩位以上,但首位為0 ['[^\d\.]+$', ''], //禁止錄入任何非數(shù)字和點(diǎn) ['\.(\d?)\.+', '.$1'], //禁止錄入兩個以上的點(diǎn) ['^(\d+\.\d{' + ln + '}).+', '$1'] //禁止錄入小數(shù)點(diǎn)后兩位以上 ]; for (var i = 0; i < regStrs.length; i++) { var reg = new RegExp(regStrs[i][0]); target.value = target.value.replace(reg, regStrs[i][1]); } //對于封裝的像el-input組件,因?yàn)槠湫枰ㄟ^input事件同步狀態(tài) if(vnode.componentInstance){ vnode.componentInstance.$listeners.input(target.value) } } function Integer(target, vnode) { let valueStr = target.value if (valueStr.length == 1) { //第一個數(shù)字不為0 valueStr = valueStr.replace(/[^0-9]/g, ""); } else { //只能輸入正整數(shù) valueStr = valueStr.replace(/D/g, ""); } target.value = valueStr; if(vnode.componentInstance){ vnode.componentInstance.$listeners.input(target.value) } }
這里需要特別注意的是下面這行代碼
vnode.componentInstance.$listeners.input(target.value)
我們?yōu)槭裁葱枰砑舆@一句呢,我們明明已經(jīng)為target.value做了賦值。
實(shí)際上這一句代碼相當(dāng)于指令作用組件內(nèi)部的$emit('input',target.value)
,這是因?yàn)槿绻覀兪窃赼ntd或者elementui中的輸入框組件上添加我們定義的v-input指令,直接為target.value賦值是不能生效的,修改的只是原生input控件value值,并沒有修改自定義組件的value,還需要通過觸發(fā)input事件去同步組件狀態(tài),修改value值。(這里不了解為什么需要觸發(fā)input事件區(qū)同步狀態(tài)的同學(xué)了解一下v-model的語法糖原理即可理解, 使用方法:
<!-- 限制輸入兩位小數(shù)數(shù)字 --> <input v-input:toFixed="2"/> <!-- 限制輸入正整數(shù) --> <el-input v-input:integer/>
內(nèi)容處理
我們也可以通過自定義指令做對內(nèi)容到處理,比如
-
空值處理
-
數(shù)字千分?jǐn)?shù)逗號分割
export default { bind:function(){ }, inserted:function(el,binding){ dealContent(el,binding) }, update:function(el,binding){ dealContent(el,binding) }, componentUpdated:function(){ }, unbind:function(){ }, } function dealContent(el,binding){ const {arg}=binding; if(arg=='empty'){ if(!el.textContent){//空值顯示 el.textContent=binding.value||'暫無數(shù)據(jù)'; } }else if(arg=='money'){//金額千分位逗號分割,如10000000顯示為100,000,00 if (binding.value) { el.textContent = dealMoney(binding.value); }else { el.textContent = dealMoney(el.textContent); } } }
千分位分割代碼:
//金額處理 export function dealMoney(money, places = 2) { const zero = `0.00`; if (isNaN(money) || money === '') return zero; if (money && money != null) { money = `${money}`; let left = money.split('.')[0]; // 小數(shù)點(diǎn)左邊部分 let right = money.split('.')[1]; // 小數(shù)點(diǎn)右邊 // 保留places位小數(shù)點(diǎn),當(dāng)長度沒有到places時,用0補(bǔ)足。 right = right ? (right.length >= places ? '.' + right.substr(0, places) : '.' + right + '0'.repeat(places - right.length)) : ('.' + '0'.repeat(places)); var temp = left.split('').reverse().join('').match(/(d{1,3})/g); // 分割反向轉(zhuǎn)為字符串然后最多3個,最少1個,將匹配的值放進(jìn)數(shù)組返回 return (Number(money) < 0 ? '-' : '') + temp.join(',').split('').reverse().join('') + right; // 補(bǔ)齊正負(fù)號和貨幣符號,數(shù)組轉(zhuǎn)為字符串,通過逗號分隔,再分割(包含逗號也分割)反向轉(zhuǎn)為字符串變回原來的順序 } else if (money === 0) { return zero; } else { return zero; } }
使用方法:
<span v-content:empty="'無'">{{message}}</span> <!-- 金額千分位逗號分割 --> <span v-content:money>100000</span>
文件預(yù)覽
v-preview方便的實(shí)現(xiàn)文件預(yù)覽功能
-
預(yù)覽圖片;
-
預(yù)覽文件;
-
其他預(yù)覽類業(yè)務(wù)功能
import {isOffic,isPdf,isImage} from '@/utils/base' import {previewWithOffice} from '@/utils/fileUtils.js' export default { inserted:function(el,binding){ el.onclick=function(e){ let params = binding.value if(isOffic(params.name)){ e.preventDefault() e.stopPropagation() previewWithOffice(params.url)//使用office在線預(yù)覽打開 }else if(isPdf(params.name) || isImage(params.name)){ e.preventDefault() e.stopPropagation() if(params.url){//直接打開url previewFile(params) } } } }, //指令所在組件的 VNode 及其子 VNode 全部更新后 componentUpdated: function (el, binding) { el.onclick=function(e){ let params = binding.value if(isOffic(params.name)){ //使用插件預(yù)覽Office文件 e.preventDefault() e.stopPropagation() previewWithOffice(params.url) }else if(isPdf(params.name) || isImage(params.name)){ //預(yù)覽圖片和pdf等能直接打開的文件 e.preventDefault() e.stopPropagation() previewFile(params) } } }, unbind(el){ el.onclick=null; } } //預(yù)覽圖片和pdf等能直接打開的文件 function previewFile(params) { let a = document.createElement("a"); a.download = params.name a.href = params.url; a.target = "_blank"; a.click(); a = null; }
使用方法:
<!-- 預(yù)覽圖片 --> <image :src='url' v-preview="{name:file.name,url:file.url}"></image> <!-- 預(yù)覽文件 --> <span v-preview="{name:file.name,url:file.url}">{{file.name}}</span>
試著自己實(shí)現(xiàn)
各位同學(xué)可以試著自己實(shí)現(xiàn)一個v-loading
的加載中的指令,通過設(shè)置一個bool
值來設(shè)置容器的加載狀態(tài)。 如有疑問可以在評論區(qū)留言。
總結(jié)
本文主要講了如下幾件事:
vue
自定義指令介紹- 實(shí)現(xiàn)一個
v-model
- 通用的自定義指令使用技巧
(學(xué)習(xí)視頻分享:web前端開發(fā)、編程基礎(chǔ)視頻)