写点什么

利用 Vue 自定义指令让你的开发变得更优雅

  • 2022 年 10 月 05 日
    广东
  • 本文字数:3855 字

    阅读完需:约 13 分钟

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



经过一番思索,我发现 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,为今后开发项目减少一个第三方库的使用吧。


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



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


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;
复制代码


以上就是文章的全部内容,希望对你有所帮助!如果觉得文章写的不错,可以点赞收藏,也欢迎关注,我会持续更新更多前端有用的知识与实用技巧,我是茶无味de一天,希望与你共同成长~

发布于: 刚刚阅读数: 3
用户头像

公众号:品味前端 2022.09.22 加入

一介前端,卖码为生。很惭愧,只希望在学习和分享的道路上能做一点微小的贡献。

评论

发布
暂无评论
利用Vue自定义指令让你的开发变得更优雅_Vue_茶无味的一天_InfoQ写作社区