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

前段时间在用框架开发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一天,希望与你共同成长~
作者:罡风小天原文地址:https://segmentfault.com/a/1190000042574651

%s 个评论

要回复文章请先登录注册