三个自定义指令demo,带你上手开发vue自定义指令

为了方便操作,我们都会封装一些全局指令为项目服务,这也会作为一个项目团队的基建。具体自定义指令请看这里

想要开发一个全局自定义指令,我们需要了解其生命周期钩子。

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 图片懒加载

我们对于懒加载指令做了一下处理

  • 对图片做占位操作。
  • 对图片做未成功加载占位操作。
  • 只有当前元素可见时,才会去加载。

如果想了解更多图片懒加载请看这里

大致的实现过程

  • IntersectionObserverapi去判断当前元素是否可见。
  • 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钩子。

往期文章

专栏文章

相关推荐
Pedantic1 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘1 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆2 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师3 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆3 小时前
VSCode自动格式化三要素
前端
爱勇宝3 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen4 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
user20585561518136 小时前
Windows 项目安装时报 `node-sass` 错误,如何快速处理
前端
LiaCode6 小时前
Redis 在生产项目的使用
前端·后端