为了方便操作,我们都会封装一些全局指令为项目服务,这也会作为一个项目团队的基建。具体自定义指令请看这里
想要开发一个全局自定义指令,我们需要了解其生命周期钩子。
js
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
js
<div v-example:foo.bar="baz">
binding
参数会是一个这样的对象:
js
{
arg: 'foo',
modifiers: { bar: true },
value: /* `baz` 的值 */,
oldValue: /* 上一次更新时 `baz` 的值 */
}
注意,一般自定义指令不要使用在自定义组件上,如果该组件有多个根节点,那么vue将抛出一个警告。可能导致该指令不能达到预期效果。如果只有一个根节点那就无所谓了。
全局指令大体开发过程
我们一般在src
目录下创建一个directives
目录存放全局自定义指令。并提供一个index.js
作为全部指令的出口文件,在main.js中统一注册。
js
// 单独指令.js
function customDirective(app) {
// TODO: 实现逻辑
}
export function setCustomDirective(app) {
customDirective(app)
}
js
// index.js
import { setCustomDirective } from './单独指令'
export default function setDirectives(app) {
setCustomDirective(app)
}
js
// main.js
import { createApp } from 'vue'
import App from './App.vue';
import setDirectives from "./directives";
const app = createApp(App);
(function(app){
setDirectives(app)
app.mount('#app')
})(app)
auth 按钮级权限校验指令
这个功能对于后台管理系统是十分常见的,我记得当时我在实习的时候还搞不懂为啥需要按钮权限,因为当时在学校做项目的时候根本没有那么多权限控制。
对于不同的角色,他会具有不同的操作权限。为了方便控制这些操作权限,我们就可以开发一个指令去控制按钮级别的元素的显隐。
大致的实现过程
- 我们只需要使用
mounted
钩子即可。 - 获取当前用户的操作权限列表,一般放在全局store中。
- 支持指令的值是字符串或者数组,做适配处理。
- 如果在当前用户的操作列表中未查到指定的权限,我们将直接移除当前元素。
el.parentNode?.removeChild(el)
js
function authDirective(app) {
app.directive('auth', {
mounted(el, bindings) {
// TODO: 全局状态中获取当前用户的权限
const permissions = ['a', 'b', 'c']
const authArr = Array.isArray(bindings.value)
? bindings.value
: [bindings.value]
let isExist = false
for (let item of authArr) {
if (permissions.includes(item)) {
isExist = true
break
} else {
continue
}
}
if (!isExist) el.parentNode?.removeChild(el)
}
})
}
export function setAuthDirective(app) {
authDirective(app)
}
format-time 格式化时间
一般后端给我们返回的时间都是时间戳,或者返回的是完整时间,我们需要对齐格式化成我们想要的,就需要用到一些时间格式化库,例如dayjs。为了方便操作,不需要在setup中写太多逻辑,我们就可以直接封装成一个自定义指令。
大致步骤如下
- 在
created
钩子中设置默认的格式。 mounted
中处理逻辑,一般后台返回的都是字符串时间戳,我们需要转为数字才可以传入dayjs做格式化。
js
import dayjs from 'dayjs'
function formatTimeDirective(app) {
app.directive('format-time', {
created(el, bindings) {
bindings.formatString = 'YYYY-MM-DD'
// 当用户传入格式化字符串,我们将使用用户传入的格式
if (bindings.value) {
bindings.formatString = bindings.value
}
},
// 挂载时
mounted(el, bindings) {
let time = el.textContent
// 如果是时间戳,转化为数字
if (!Object.is(Number(time), NaN)) {
time = Number(time)
}
el.textContent = dayjs(time).format(bindings.value)
}
})
}
export function setFormatTimeDirective(app) {
formatTimeDirective(app)
}
lazy 图片懒加载
我们对于懒加载指令做了一下处理
- 对图片做占位操作。
- 对图片做未成功加载占位操作。
- 只有当前元素可见时,才会去加载。
大致的实现过程
IntersectionObserver
api去判断当前元素是否可见。new Image()
的onload,onerror去判断指定的图片资源是否加载完毕或者成功。做统一的异步处理。
js
// 先指定img元素占位图
el.setAttribute( 'src', 'https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/e08da34488b114bd4c665ba2fa520a31.svg' )
// 加载真实图片地址
// 图片加载完毕后,赋值真实地址
el.setAttribute('src', bindings.value)
// TODO: 错误图 图片加载失败时,赋值错误占位符
el.setAttribute(
'src',
'https://p26-passport.byteacctimg.com/img/user-avatar/cba9b94c077e932fb1ef7f9d5abd447b~50x50.awebp'
)
js
function isExistContainer(el, bindings) {
const observer = new IntersectionObserver(async (entires) => {
// 当前元素可见
if (entires[0].isIntersecting) {
// 判断当前元素是否加载完毕
const isLoaded = await isImgLoaded(el, bindings)
if (isLoaded) {
// 图片加载完毕后,赋值真实地址
el.setAttribute('src', bindings.value)
} else {
// TODO: 错误图
el.setAttribute(
'src',
'https://p26-passport.byteacctimg.com/img/user-avatar/cba9b94c077e932fb1ef7f9d5abd447b~50x50.awebp'
)
}
observer.unobserve(el)
}
})
// 监听
observer.observe(el)
return observer
}
async function isImgLoaded(el, bindings) {
return new Promise((resolve) => {
// TODO: 给当前元素设置占位图
el.setAttribute(
'src',
'https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/e08da34488b114bd4c665ba2fa520a31.svg'
)
const img = new Image()
// 赋值真实的路径
img.src = bindings.value
img.onload = () => {
resolve(true)
}
img.onerror = () => {
resolve(false)
}
})
}
function LazyDirective(app) {
app.directive('lazy', {
// 挂载时
mounted(el, bindings) {
// 判断是否在可视区域,并经行加载
const observer = isExistContainer(el, bindings)
bindings.observer = observer
},
// 取消监听
unmounted(el, bindings) {
bindings.observer.unobserve(el)
}
})
}
export function setLazyDirective(app) {
LazyDirective(app)
}
通过这三个例子可以看出,我们开发简单的自定义指令来说都是使用mounted
钩子即可。对于复杂一点的,我们还需要监听指令值的变化,需要用到updated
钩子。
往期文章
- 因为原生,选择一家公司(前端如何防笔试作弊)
- 结合开发,带你熟悉package.json与tsconfig.json配置
- 如何优雅的在项目中使用echarts
- 如何优雅的做项目国际化
- 近三个月的排错,原来的憧憬消失喽
- 带你从0开始了解vue3核心(运行时)
- 带你从0开始了解vue3核心(computed, watch)
- 带你从0开始了解vue3核心(响应式)
- 3w+字的后台管理通用功能解决方案送给你
- 入职之前,狂补技术,4w字的前端技术解决方案送给你(vue3 + vite )