利用Vue自定义指令 - 让你的开发变得更简单

前段时间在用框架开发H5页面时,碰到框架中的组件内置了一个属性用于适配异形屏,虽然是组件内部实现的,但这个方式让我萌生一个想法:能不能自己写一个属性来实现这样的功能?

image.png

经过一番思索,我发现Vue的指令模式就很像属性的写法,在Vue中,我们利用模板指令诸如v-if v-for等完成了许多工作,而Vue同样也支持自定义属性:

const app = Vue.createApp({})
// 注册一个全局自定义指令 `v-focus`
app.directive('focus', {
  // 当被绑定的元素挂载到 DOM 中时……
  mounted(el) {
    // 聚焦元素
    el.focus()
  }
})

然后你可以在模板中任何元素上使用新的 v-focus attribute,如下

<input v-focus />

注:这里除了全局注册,也可以采用局部注册的方式,实际开发中可以使用vue另一项方便的功能mixin来将对应的指令混入你想使用的文件中,以达到代码的复用,那么开始进入正题吧。

底部安全区适配

首先页面必须在 head 标签中添加 meta 标签,并设置 viewport-fit=cover 值

directives: {
    safeAreaBottom: {
      bind(el, binding) {
        const addHigh = binding.value || 0
        el.setAttribute('style', el.style.cssText + `padding-bottom: calc(${addHigh} +  constant(safe-area-inset-bottom));padding-bottom: calc(${addHigh} +  env(safe-area-inset-bottom));`);
      }
    }
}

使用:

<div v-safe-area-bottom></div>

如果设计图本身存在一个边距,则可以动态适配:

<div v-safe-area-bottom="'1rem'"></div>
<div v-safe-area-bottom="'10px'"></div>

是不是很方便?我们再来看看另一个移动端H5会遇到的问题,并且还是用Vue指令来解决它。

弹窗背景页不滚动

在移动端开发中,页面弹出滚动窗口时,需要将背景页固定住不动,否则会出现"滚动穿透"的现象。

touchScroll: {
  inserted() {
    const scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
    document.body.style.cssText += 'position:fixed;width:100%;top:-' + scrollTop + 'px;';
  },
  unbind() {
    const body = document.body || document.documentElement;
    body.style.position = '';
    const top = body.style.top;
    document.body.scrollTop = document.documentElement.scrollTop = -parseInt(top, 10);
    body.style.top = '';
  }
}
<div v-touch-scroll>是的,我是一个弹窗,当我出现时我的背景会吓得不敢动</div>

实现一个copy工具

有时我们需要页面点击可以"一键复制"的功能,可能大家都有用到一个叫vue-clipboard的库,知道了指令的使用,实现一个copy自然也不在话下,那么就自己动手写一个vueCopy,为今后开发项目减少一个第三方库的使用吧。

首先我们看看这个工具是怎么使用的:

image.png

可以看出作者也是利用了指令,就照他这个思路,动手撸了一个,这里就直接上代码了,具体思路点见注释:

clipboard: {
  bind(el, binding, { context }) {
    const _this = context
    // 利用arg用来注入回调函数
    if (binding.arg === 'success') {
      _this.__clipboardSuccess = _this[binding.expression]
    } else if (binding.arg === 'error') {
      _this.__clipboardError = _this[binding.expression]
    } else { // 正常情况下就将文字缓存起来
      _this.__clipboardValue = binding.value
    }
    el.handler = () => {
      if (!_this.__clipboardValue) {
        this.__clipboardError && this.__clipboardError('无内容')
        return
      }
      if (binding.arg) { // 这里是因为属性被我们用了多次会多次执行,所以限制了执行次数
        return
      }
      try {
        const textarea = document.createElement('textarea')
        textarea.readOnly = 'readonly' // 禁止输入, readonly 防止手机端错误聚焦自动唤起键盘
        textarea.setAttribute('style', 'position:fixed;top:-9999px;left:-9999px;') // 它是可见的,但它又是不可见的
        textarea.value = binding.value
        document.body.appendChild(textarea)
        textarea.select()
        const result = document.execCommand('Copy')
        if (result) {
          _this.__clipboardSuccess && _this.__clipboardSuccess(binding.value) // 这里可以定义成功回调返回的数据
        }
        document.body.removeChild(textarea)
      } catch (e) {
        this.__clipboardError && this.__clipboardError(e)
      }
    }
    el.addEventListener('click', el.handler)
  },
  componentUpdated(el, { arg, value }, { context }) { // 更新值时候触发
    const _this = context
    if (!arg) { // 注册回调的部分不要赋值
      _this.__clipboardValue = value
    }
  },
  unbind(el) {
    el.removeEventListener('click', el.handler)
  },
}

简单使用:

<div v-clipboard="'copy copy Text'">点击直接复制到剪贴板</div>

带回调的使用:

<template>
    <div v-clipboard="text" v-clipboard:success="success" v-clipboard:error="error">copy copy Text</div>
</template>

<script>
export default {
  data() {
    return {
      text: 123
    }
  },
  methods: {
    success(e) {
      console.log(e); // 复制成功回调
    },
    error(e) {
      console.log(e); // 复制失败回调
    }
  }
}
</script>

表单防止重复提交

// 设置 v-throttle 自定义指令
Vue.directive('throttle', {
  bind: (el, binding) => {
    let throttleTime = binding.value; // 节流时间
    if (!throttleTime) { // 用户若不设置节流时间,则默认2s
      throttleTime = 2000;
    }
    let cbFun;
    el.addEventListener('click', event => {
      if (!cbFun) { // 第一次执行
        cbFun = setTimeout(() => {
          cbFun = null;
        }, throttleTime);
      } else {
        event && event.stopImmediatePropagation();
      }
    }, true);
  },
});

使用:

<button @click="sayHello" v-throttle>提交</button>

图片懒加载

const LazyLoad = {
    // install方法
    install(Vue,options){
    	  // 代替图片的loading图
        let defaultSrc = options.default;
        Vue.directive('lazy',{
            bind(el,binding){
                LazyLoad.init(el,binding.value,defaultSrc);
            },
            inserted(el){
                // 兼容处理
                if('IntersectionObserver' in window){
                    LazyLoad.observe(el);
                }else{
                    LazyLoad.listenerScroll(el);
                }
                
            },
        })
    },
    // 初始化
    init(el,val,def){
        // data-src 储存真实src
        el.setAttribute('data-src',val);
        // 设置src为loading图
        el.setAttribute('src',def);
    },
    // 利用IntersectionObserver监听el
    observe(el){
        let io = new IntersectionObserver(entries => {
            let realSrc = el.dataset.src;
            if(entries[0].isIntersecting){
                if(realSrc){
                    el.src = realSrc;
                    el.removeAttribute('data-src');
                }
            }
        });
        io.observe(el);
    },
    // 监听scroll事件
    listenerScroll(el){
        let handler = LazyLoad.throttle(LazyLoad.load,300);
        LazyLoad.load(el);
        window.addEventListener('scroll',() => {
            handler(el);
        });
    },
    // 加载真实图片
    load(el){
        let windowHeight = document.documentElement.clientHeight
        let elTop = el.getBoundingClientRect().top;
        let elBtm = el.getBoundingClientRect().bottom;
        let realSrc = el.dataset.src;
        if(elTop - windowHeight<0&&elBtm > 0){
            if(realSrc){
                el.src = realSrc;
                el.removeAttribute('data-src');
            }
        }
    },
    // 节流
    throttle(fn,delay){
        let timer; 
        let prevTime;
        return function(...args){
            let currTime = Date.now();
            let context = this;
            if(!prevTime) prevTime = currTime;
            clearTimeout(timer);
            
            if(currTime - prevTime > delay){
                prevTime = currTime;
                fn.apply(context,args);
                clearTimeout(timer);
                return;
            }

            timer = setTimeout(function(){
                prevTime = Date.now();
                timer = null;
                fn.apply(context,args);
            },delay);
        }
    }

}
export default LazyLoad;
posted @ 2021-06-15 09:57  茶无味的一天  阅读(6)  评论(0编辑  收藏  举报  来源