vue的自定义
阅读原文时间:2023年07月15日阅读:2

自定义组件

组件是可以被复用的页面的零件,其实就是一个插件,只是在vue里叫组件

先看看别人的组件

vant

element

Mint

iView

去试试上面的组件,都是有脚手架版和直接引入使用的版本的

脚手架的使用需要先去main.js入口文件引入,还有css也是,查看官方文档都有的

这些组件让vue的开发容易得飞起,这也是为什么说是个人就能做前端,封装得太好了,以前自己写几百行几千行的功能,在组件里主要一个标签一句代码就行,如何封装一个组件,就能理解如何使用一个组件

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vant@2.2/lib/index.css">
<div id="app">
   <van-count-down :time="tt" />
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
<script src="https://cdn.jsdelivr.net/npm/vant@2.2/lib/vant.min.js"></script>
<script type="text/javascript">
var app = new Vue({
  el: '#app',
  data: {
      tt: 30 * 60 * 60 * 1000
  }
})
</script>

上面有个<van-count-down>标签不是vue的内置标签,而是自定义标签,就像路由的<route-view>标签一样,这个标签就是为了在所在位置生成自定义的内容,标签上还能看到一个:time这个自定义的参数

还有就是所有组件都有的弹窗提示功能,这类功能不需要在页面写自定义标签,是怎么实现的

Dialog.alert({
  title: '标题',
  message: '弹窗内容'
}).then(() => {
  // on close
});

除了自定义组件,还有自定义组件的v-model数据双向变化,自定义指令,自定义过滤器,自定义方法,下面会直接上代码,不解释,解释不清楚

标签组件myA

<template>
    <div>
       <div>{{newNum}}</div>
       <div @click="newAdd">点击添加</div>
    </div>
</template>
<script>
export default {
    name: 'myA',
    props: ["num","add"],
    data(){
        return {
            newNum: 0
        }
    },
    watch:{
      num(){
         this.newNum = this.num
      }
    },
    mounted(){
      this.newNum = this.num
      setInterval(()=>{
         this.newNum--;
      },1000)
    },
    methods:{
      newAdd(){
         // 因为子组件不能修改来自外来的数据
         // 所以应该用this.$emit去执行外来的方法
         // 用外来的方法修改外来的数据
         this.$emit("add",this.newNum)
      }
    }
}
</script>
<style scoped>
  div{
    color: red;
  }
</style>

双向绑定数据组件myB

<template>
    <div :value="newVal" @input="Input">{{newVal}}{{aa}}</div>
</template>
<script>
export default {
    name: 'myB',
    props: ["value","aa"],
    data(){
        return {
            newVal: this.value
        }
    },
    mounted(){
        setInterval(()=>{
            this.Input()
        },1000)
    },
    methods:{
        Input(){
            this.newVal--;
            this.$emit('input',this.newVal)
        }
    }
}
</script>
<style scoped>
  div{
    color: blue;
  }
</style>

生成型组件myC

<template>
    <div @click="bb">{{value}}{{aa}}666</div>
</template>

<script>
export default {
   name:'myC'
}
</script>
<style scoped>
  div{
    color: grey;
  }
</style>

封装

import myA from 'myA.vue'
import myB from 'myB.vue'
import myC from 'myC.vue'

export default {
    // 固定写法
    install: function (Vue, options) {
        // 添加的内容写在这个函数里面
        console.log(options);

        // 注册自定义组件
        Vue.component(myA.name,myA)
        // 注册自定义组件
        Vue.component(myB.name,myB)

        // 原型方法生成组件
        Vue.prototype.$myC = function(opt){
            var cc = Vue.extend(myC)
            var component = new cc({
               data:{
                  value:opt.value,
                  aa:opt.aa
               },
               methods:{
                  bb:opt.bb
               }
            }).$mount()
            // 添加到页面上去
            document.querySelector('body').appendChild(component.$el)
        }

        // 自定义过滤器
        Vue.filter('formatTime', function (value) {
            Date.prototype.Format = function (fmt) { //author: meizz
                var o = {
                    "M+": this.getMonth() + 1, //月份
                    "d+": this.getDate(), //日
                    "h+": this.getHours(), //小时
                    "m+": this.getMinutes(), //分
                    "s+": this.getSeconds(), //秒
                    "q+": Math.floor((this.getMonth() + 3) / 3), //季度
                    "S": this.getMilliseconds() //毫秒
                };
                if (/(y+)/.test(fmt))
                    fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
                for (var k in o)
                    if (new RegExp("(" + k + ")").test(fmt))
                        fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
                return fmt;
            }
            return new Date(value).Format("yyyy-MM-dd hh:mm:ss");
        })

        // 添加原型方法
        Vue.prototype.$service = {
            //电话号码合法性检查
            telNumberCheck: function (tel) {
                var pattern = /123/;
                return pattern.test(tel)
            }
        }
        // 自定义指令
        // 第一个参数:指令名称
        // 第二个参数:配置对象,指定指令的钩子函数
        Vue.directive('directiveName', {
        // bind中只能对元素自身进行DOM操作,而无法对父级元素操作
        // 只调用一次 指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
             bind( el,binding, vnode ) {
             // 参数详解
             // el:指令所绑定的元素,可以用来直接操作 DOM 。
             // binding:一个对象,包含以下属性:
             // name:指令名,不包括 v- 前缀。
             // value:指令的绑定值,等号后面的值 。
             // oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
            // expression:字符串形式的指令表达式 等号后面的字符串 形式
            // arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
            // modifiers:指令修饰符。例如:v-directive.foo.bar中,修饰符对象为 { foo: true, bar: true }。
            // vnode:Vue 编译生成的虚拟节点。。
            // oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
            },
            // inserted这个钩子函数调用的时候,当前元素已经插入页面中了,也就是说可以获取到父级节点了
            inserted (  el,binding, vnode ) {},
            //  DOM重新渲染前
            update(el,binding, vnode,oldVnode) {},
            // DOM重新渲染后
            componentUpdated ( el,binding, vnode,oldVnode ) {},
            // 只调用一次,指令与元素解绑时调用
            unbind ( el ) {
               // 指令所在的元素在页面中消失,触发
            }
        })
    }
}

插件引入

在main.js里

import my from 'my'
Vue.use(my)

使用

<template>
  <div>
    <my-a :num="num" @add="add"/>
    <my-b v-model="value"/>
    <div my-directive="{color:'white',text:'hello!'}"></div>
    <div>{{ time | formatTime }}</div>
  </div>
</template>
<script>
    // @ is an alias to /src
    export default {
        name: 'ceshi',
        data(){
          return {
              num:111,
              value:222,
              time:123153123
          }
        },
        mounted(){
            this.$myC({
                value:"1111",
                aa:"aa",
                bb(){
                    this.$service.telNumberCheck(123456789)
                }
            })
        }
    }
</script>

组件的递归,在组件调用自己就行,但是一定要防止死循环

递归组件最常见到就是tree结构

<template>
  <div>
    {{num}}
      <my-num :num="num-1" v-if="num>0"/>
  </div>
</template>
<script>
    export default {
        // name一定要
        name: 'myNum',
        props: ["num"],
    }
</script>

// 使用
<my-num num="3"/>
// 然后会显示
3
2
1
0

组件里的props上面都是用数组去接受,如果写成对象格式,可以做数据校验,能要求必传,还能添加默认值

props:{
    title:String,
    likes: Number,
    isPublished: Boolean,
    commentIds: Array,
    author: Object,
    callback: Function,
    contactsPromise: Promise,
    items: {  // 必须提供字段
       required: true
    },
    unit: {   // 可选字段,有默认值
       default: 3
    }
}

事件修饰符native

用来注册元素的原生事件而不是组件自定义事件的

<vButton @click="clickHandler" @vclick="vClickHandler">按钮</vButton>

export default {
  methods: {
    clickHandler () {
      alert('onclick') // 此处不会执行 因为组件中未定义 `click` 事件
    },
    vClickHandler () {
      alert('onvclick') // 触发 `vclick` 自定义事件
    }
  }
}

//如果将上面模版改成如下方式,那么两个事件都会执行。
<vButton @click.native="clickHandler" @vclick="vClickHandler">按钮</vButton>

有个问题

如果组件的渲染报错说运行时什么什么的

在vue.config.js里加上下面这个配置

module.exports = {
    runtimeCompiler: true
}

补充一个图片懒加载的自定义指令

 // 引入Vue构造函数
 import Vue from 'vue'

 var lazyload = {
     // Vue.use() 默认加载install,并且将Vue当做第一个参数传递过来
     install(vue,payload) {
         // 数组扩展移除元素
         if(!Array.prototype.remove){
             Array.prototype.remove = function(item){
                 if(!this.length) return
                 var index = this.indexOf(item);
                 if( index > -1){
                     this.splice(index,1);
                     return this
                 }
             }
         }

         // 默认值图片
         var defaultImage = payload.defaultImage || 'xxx.png';
         var errorImage = payload.errorImage || 'xxx.png';
         // 默认离可视区10px时加载图片
         var distanece = payload.scrollDistance || 10;
         // 收集未加载的图片元素和资源
         var listenList = [];
         // 收集已加载的图片元素和资源
         var imageCacheList = [];

         // 是否已经加载完成的图片
         const isAlredyLoad = (imageSrc) => {
             if(imageCacheList.indexOf(imageSrc) > -1){
                 return true;
             }else{
                 return false;
             }
         }

         //检测图片是否可以加载,如果可以则进行加载
         const isCanShow = (item) =>{
             var ele = item.ele;
             var src = item.src;
             //图片距离页面顶部的距离
             var top = ele.getBoundingClientRect().top;
             //页面可视区域的高度
             var windowHeight = window.innerHight;
             // top - distance 距离可视区域还有distance像素
             if(top - distanece < window.innerHeight){
                 var image = new Image();
                 image.src = src;
                 image.onload = function() {
                     ele.src = src;
                     imageCacheList.push(src);
                     listenList.remove(item);
                 }
                 image.onerror = function() {
                     ele.src = errorImage;
                     imageCacheList.push(src);
                     listenList.remove(item);
                 }
                 return true;
             }else{
                 return false;
             }
         };

         const onListenScroll = () => {
             window.addEventListener('scroll',function(){
                 var length = listenList.length;
                 for(let i = 0;i<length;i++ ){
                     isCanShow(listenList[i]);
                 }
             })

         }

         //Vue 指令最终的方法
         const addListener = (ele,binding) =>{
             //绑定的图片地址
             var imageSrc = binding.value;

             // 防止重复加载。如果已经加载过,则无需重新加载,直接将src赋值
             if(isAlredyLoad(imageSrc)){
                 ele.src = imageSrc;
                 return false;
             }

             var item = {
                 ele: ele,
                 src: imageSrc
             }

             //图片显示默认的图片
             ele.src = defaultImage;

             //再看看是否可以显示此图片
             if(isCanShow(item)){
                 return
             }

             //否则将图片地址和元素均放入监听的lisenList里
             listenList.push(item);

             //然后开始监听页面scroll事件
             onListenScroll();
         }

         Vue.directive('lazyload',{
             inserted: addListener,
             updated: addListener
         })
     }
 }

 export default lazyload;

// 使用
// 参数均为可选
Vue.use(Lazyload,{
 scrollDistance: 15, // 距离可视区还有15px时开发加载资源
 defaultImage: '', // 资源图片未加载前的默认图片(绝对路径)
 errorImage:'' // 资源图片加载失败时要加载的资源(绝对路径)
})