Vue组件

1,174 阅读8分钟

前言

为什么要有组件化、模块化

现在的网站在公司业务发展的过程中体积越来越庞大,其中堆叠了大量的业务逻辑代码,不同业务模块的代码相互调用,相互嵌套,代码之间的耦合性越来越高,调用逻辑会越来越混乱。

当某个功能需要升级的时候,往往牵一发而动全身,不管是在 DOM 上面,还是 js逻辑 层面,都会让我们需要大量的精力去兼顾旧的代码的修改以及调整,带来工作量的增加。

总的来说,大量的代码堆叠使得可读性、可维护性、不同工程师的协同非常的差,再加之业务越来越复杂,我们需要一种方式让其变得简单。

化繁为简

一个页面的繁琐,化为每个块独立的单独功能,而每个单独的块组合、嵌套,协同工作,就是组件化的思想。

我们可以把一个页面想象成一辆车,组件就是每个独立的零件,他们自身发挥自己的作用,组合起来就是一辆车。

我们可以很大程度的降低系统各个功能的耦合性,并且提高了功能内部的聚合性。这使得我们的可读性、可维护性大大的增加,耦合性的降低,也降低了我们开发的复杂性,提升了开发效率。我们就可以多点时间做自己喜欢的事情辣!

设计组件要遵循一个原则:一个组件只专注做一件事,且把这件事做好。(用钢炼的话来说:一就是全,全就是一)

下面我们就来学习 Vue 的组件化开发吧~

组件是什么

组件 跟我们平时写的 js 函数一样,具有

  1. 封装性
  2. 复用性
  3. 单一职责

他可以扩展HTML元素,封装可重用的HTML代码,我们可以将组件看作自定义的HTML元素。

组件注册

全局注册

  • 通过 component 指令直接注册一个组件
  • 第一个参数为 组件名,第二个参数就是我们在new Vue内写的 options,包括 data 等
  • component 内暴漏一个字段 template 用于写 html 结构
  • 我们可以把组件当成一个 标签 引入
    • 值得注意的是,我们习惯性用大驼峰命名法命名组件
    • 而在 标签 的书写上,我们需要用烤肉串的命名规则(html对大小写不敏感)
<div id="app">
    <component-a></component-a>
</div>
Vue.component("ComponentA", {
    template: `<div>ComponentA</div>`
});
const app = new Vue({
    el: '#app'
});
  • 我们也可以通过字段 template 引入
    • 这个时候我们可以直接用大驼峰命名,没有命名限制
    • 使用 template 亦会使得目标内的东西被 template 的内容取代
<div id="app">
    我要被干掉了~
</div>
Vue.component("ComponentA", {
    template: `<div>ComponentA</div>`
})

const app = new Vue({
    el: '#app',
    template: `<div>
        {{msg}}
        <ComponentA></ComponentA>
    </div>`,
    data: {
        msg: 'hello world'
    }
});

局部注册

  • 我们可以声明一个对象,通过字段 components 引入组件使用
<div id="app"></div>
const ComponentB = {
    template: `<div>
        ComponentB
        {{msg}}
        <button @click="handleClick">add</button>
    </div>`,
    data() {
        return {
            msg: 0
        }
    },
    methods: {
        handleClick() {
            this.msg++
        }
    },
}

Vue.component("ComponentA", {
    components: {
        ComponentB
    },
    template: `<div>
        ComponentA
        <ComponentB></ComponentB>
    </div>`
})

const app = new Vue({
    el: '#app',
    template: `<div>
        {{msg}}
        <ComponentA></ComponentA>
        <ComponentA></ComponentA>
    </div>`,
    data: {
        msg: 'hello world'
    }
});

注意点

  • 组件必须有根节点(Vue3可不需要)
  • data 必须是个函数(因为组件可复用,使用函数把他们解耦开)

组件生命周期

什么是生命周期

组件的生命周期就是组件从 挂载前 到 卸载后 的一些钩子,也可以称为 hooks,就是普通的函数

为什么要用生命周期

我们常常有需求,挂载这个组件前要请求数据,又或者添加组件后需要启用全局(window)下的函数,例如定时器,也需要知道组件什么时候卸载,这个时候,我们需要用到组件的生命周期,我们可以把它想象为内置的 组件监听事件

生命周期是怎样的

  • 主要由四套,create、mount、updata、destroy,每套有之前(before)、之后(ed) 组成
  • 通过下面的小案例,我们可以清晰的看到生命周期的执行顺序是怎样的,如何工作的
  • 组件会在 mounted 即 挂载后 添加到视图里面 => 我们能看到他了
Vue.component("ComponentA", {
    template: `<div>
        ComponentA
        {{count}}
        <button @click="handleAdd">add</button>
    </div>`,
    data() {
        return { count: 0 }
    },
    methods: {
        handleAdd() {
            this.count++
        }
    },
    beforeCreate() {
        console.log('创建前 - a');
    },
    created() {
        console.log('创建后 - a');
    },
    beforeMount() {
        console.log('挂载前 - a');
    },
    mounted() {
        console.log('挂载后 - a');
    },
    beforeUpdate() {
        console.log('更新前 - a');
    },
    updated() {
        console.log('更新后 - a');
    },
    beforeDestroy() {
        console.log('卸载前 - a');
    },
    destroyed() {
        console.log('卸载后 - a');
    }
})

const app = new Vue({
    el: '#app',
    template: `<div>
        <ComponentA v-if="handleShow"></ComponentA>
        <button @click="handleShow=!handleShow">挂载/卸载</button>
    </div>`,
    data: {
        handleShow: true
    },
    beforeCreate() {
        console.log('创建前');
    },
    created() {
        console.log('创建后');
    },
    beforeMount() {
        console.log('挂载前');
    },
    mounted() {
        console.log('挂载后');
    }
});

组件间的通信

上面我们说了,组件相当于 js函数一样,那么在组件里面,我们是如何进行传参的?又如何返回参数的呢?换句话说,组件之间,如何协同工作呢?下面我们来说说组件间的通信

props

  • 我们需要把参数传入组件时,用props进行传参
  • 通过字段 props 接收参数,引用组件时通过 attribute 传入参数
  • props 可定义为 数组 或者 对象 的方式
  • 类比 typeScriptprops 也可以通过 对象的关键字段 限制类型,还能自定义规则
  • 限制类型和规则,目的是为了多人协同工作时,防止乱传参数,减少对接成本
Vue.component("ComponentA", {
    // 方式1
    props: ['title'],
    // 方式2
    props: {
        title: {
            // 定义类型
            type: String,
            // 定义默认值
            default: 'title-default',
            // 定义是否必须传值
            required: true
        },
        student: {
            // 定义类型
            type: Object,
            // 自定义规则
            validator(val) {
                console.log(val);
                // return true 通过, return false 不通过
                return val.name === '小明'
            }
        }
    },
    template: `<div>
        ComponentA
        <div>{{title}}</div>
    </div>`
})

const app = new Vue({
    el: '#app',
    template: `<div>
    	<ComponentA title="123" :student="{name:'小明'}"></ComponentA>
    </div>`
});

$emit

  • emit 可以理解为 js函数 的 output,是用于组件发出值
  • 调用 this.$emit ,第一个参数即是外层接收事件的 名字,用事件方式接收
Vue.component("ComponentA", {
    template: `<div>
    	<button @click="handleClick">ComponentA</button>
    </div>`,
    methods: {
        handleClick() {
            this.$emit('change', '点击了')
        }
    }
});

const app = new Vue({
    el: '#app',
    template: `<div>
    	<ComponentA @change="handleChange"></ComponentA>
    </div>`,
    methods: {
        handleChange(val) {	
            console.log(val);
        }
    }
});

v-model

  • 使用 v-model 在组件传值,需要在组件内生命一个 props
  • 一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件
  • 为了避免像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同目的,可用 model 修改默认名
Vue.component("ComponentA", {
    // model 用于修改默认关键字
    model: {
        prop: 'milk',
        event: 'change'
    },
    // 默认为value
    // props: ['value'],
    props: ['milk'],
    template: `<div>
        <!-- {{value}} -->
        {{milk}}
        <button @click="handleClick">ComponentA</button>
    </div>`,
    data() {
        return {
            countA: 0
        }
    },
    methods: {
        handleClick() {
            // 默认为 input
            // this.$emit('input', ++this.countA)
            this.$emit('change', ++this.countA)
        }
    }
});

const app = new Vue({
    el: '#app',
    template: `<div>
        {{count}}
        <!-- 这里的 count 将会传入 props 的 milk 内,当组件内 $emit change 触发并带回新值时,count 将会被更新 -->
        <ComponentA v-model="count"></ComponentA>
    </div>`,
    data: {
        count: 0
    }
});

.sync

  • 上面的 v-model 可以使得传入的 count 与组件进行绑定,通过 $emit 更改 count,但是组件内需要一个中间值 countA 进行传输,那么如果我们需要绑定的值很多,我们可不可以在 组件内 直接修改 props 使得外部的 count 发生改变呢?
  • 直接修改 props 是不可以的,因为 Vue单向数据流 ,即数据只能单向传导,这么做是为了数据不乱套,维护数据的统一性
  • 我们应该怎么做呢?
  1. 我们可以直接使用 $emit 的方法,发送新的值出去,在外部改变
    • 这样写起来很麻烦,需要绕一个圈:先传值 => 改变值 => 传输出去 => 外部再改变值
    • 会让我们的代码变得繁琐,业务多会出现这种逻辑,大量的堆叠起来,维护性和阅读性也会变差,而且组件之间不灵活,耦合性很强
Vue.component("ComponentA", {
    props: ['count'],
    template: `<div>
        {{count}}
        <button @click="handleClick">ComponentA</button>
    </div>`,
    methods: {
        handleClick() {
            this.$emit("upDateCount", this.count + 1)
        }
    }
});

const app = new Vue({
    el: '#app',
    template: `<div>
        {{count}}
        <ComponentA :count="count" @upDateCount="upDateCount"></ComponentA>
    </div>`,
    data: {
        count: 0
    },
    methods: {
        upDateCount(val) {
            console.log(val);
            this.count = val;
        }
    }
});
  1. 外部定义传值的时候,也定义一个修改这个值的方法(跟React逻辑一样)
    • 这样看起来很不错,耦合性降低,一个值对应了读,写
    • 但是这样我还是觉得不方便,我看着还是不爽
Vue.component("ComponentA", {
    props: ['count', 'upDateCount'],
    template: `<div>
        {{count}}
        <button @click="handleClick">ComponentA</button>
    </div>`,
    methods: {
        handleClick() {
            this.upDateCount(this.count + 1);
        }
    }
});

const app = new Vue({
    el: '#app',
    template: `<div>
        {{count}}
        <ComponentA :count="count" :upDateCount="upDateCount"></ComponentA>
    </div>`,
    data: {
        count: 0
    },
    methods: {
        upDateCount(val) {
            this.count = val;
        }
    }
});
  1. sync
    • 这是 Vue 内置的语法糖
    • 它可以大量的缩减我们的代码,使得我们心情愉悦(划掉),减少代码量,增加阅读性
Vue.component("ComponentA", {
    props: ['count'],
    template: `<div>
        {{count}}
        <button @click="handleClick">ComponentA</button>
    </div>`,
    methods: {
        handleClick() {
            // 发出:update:propsName(接收的attribute)
            this.$emit('update:childCount', this.count + 1)
        }
    }
});

const app = new Vue({
    el: '#app',
    template: `<div>
        {{parentCount}}
        <!-- 接收:propsName(接收的attribute).sync:data -->
        <ComponentA :count="parentCount" :childCount.sync="parentCount"></ComponentA>
    </div>`,
    data: {
        parentCount: 0
    }
});

slot插槽

  • 组件的传输,除了 props 还有 slot 插槽
  • slot 内可写默认的值
  • 插槽就像打孔,在组件内打孔,用外部传进去的东西填充
  • 插槽可有多个,单个的 name 默认为 default ,当你想定义多个时 你得给他起个名字
  • 插槽有自己的作用域,如果想在调用组件时可以调用组件内的参数,可以通过 slotattribute 以对象的形式发出
Vue.component("ComponentA", {
    template: `<div>
        <!-- name 为 header 的 插槽 -->
        <p><slot name="header">headerDefault</slot></p>
        <!-- 默认插槽 的 两种写法 -->
        <slot>default</slot>
        <slot name="default">default</slot>
        <!-- name 为 footer 的 插槽 -->
        <!-- 插槽的作用域,把 childMsg 和 123 通过 attribute 传出去 -->
        <p><slot name="footer" :msg="childMsg" num="123">footerDefault</slot></p>
    </div>`,
    data() {
        return {
            childMsg: 'childMsg'
        }
    }
})

const app = new Vue({
    el: '#app',
    template: `<div>
        <ComponentA>
            <!-- name 为 header 的 插槽 -->
            <template v-slot:header>{{parentMsg}}</template>
            <!-- 默认插槽 的 两种写法 -->
            slot1
            <template v-slot:default>slot2</template>
            <!-- name 为 footer 的 插槽 -->
            <!-- 作用域插槽传出的 data 为 对象 -->
            <template v-slot:footer="data">{{data.msg}} -- {{data.num}}</template>
        </ComponentA>
    </div>`,
    data: {
        parentMsg: 'parentMsg'
    }
});
  • 附加示例:作用域插槽的使用,内置 for 循环,外部调用
Vue.component("ComponentA", {
    template: `<div>
        <ul>
            <li v-for="item in list"><slot :item="item"></slot></li>
        </ul>
    </div>`,
    data() {
        return {
            list: [
                { name: '橘子', num: 12 },
                { name: '苹果', num: 0 },
                { name: '香蕉', num: 3 },
            ]
        }
    }
})

const app = new Vue({
    el: '#app',
    template: `<div>
        <ComponentA>
            <-- 这里 item 是对象,通过{}解构 -->
            <template v-slot="{item}">
                {{item.name}} - {{item.num}}
                <span v-if="!item.num">卖的真好,么得了</span>
            </template>
        </ComponentA>
    </div>`
});

mixin混入

  • mixin 是为了 组件的 复用而出现的
  • 他就是一个普通对象,在组件内暴露的 mixins 引入后将会被使用
  • mixin 对象内所有的参数 跟组件完全一样(包括mixins)**
  • mixin 有缺陷
    1. 依赖关系模糊,找不到 对应的 来源
      • 因为 mixin 里面可以套用 mixin ,我们很难在嵌套的关系中找到对应我们需要的 fn
    2. 命名冲突 A、B methods命名 可能一样,被顶替掉
  • mixinVue3compositionAPI 取缔
const mixinB = {
    mounted() {
        console.log('hei~ I\'m mixinB');
    }
}

const mixinA = {
    mixins: [mixinB],
    template: `<div>ComponentA</div>`,
    mounted() {
        console.log('hei~ I\'m mixinA');
    }
}

Vue.component("ComponentA", {
    mixins: [mixinA]
});

Vue.component("ComponentB", {
    mixins: [mixinA],
    template: `<div>ComponentB</div>`
});

const app = new Vue({
    el: '#app',
    template: `<div>
        <ComponentA></ComponentA>
        <ComponentB></ComponentB>
    </div>`
});

ref

  • 我们有的时候有可能需要到真实的 DOM 元素进行一些操作,例如 input 框获取焦点之类的
  • 这个时候我们可以通过使用 特殊的 attribute 找到 DOM
Vue.component("ComponentA", {
    template: `<div>
    ComponentA
        <!-- attribute为ref -->
        <input ref="ref"/>
    </div>`,
    mounted() {
        // 通过 this.$refs 找到标记的 ref
        console.log(this.$refs.ref);
        this.$refs.ref.focus();
    }
})

const app = new Vue({
    el: '#app',
    template: `<div>
        <ComponentA></ComponentA>
    </div>`
});

自定义指令

  • 除了上面的 ref 之外,我们还可以通过 自定义指令 拿到真实的 DOM ,还有更加丰富的 配置项,可以让我们自定义我们需要的 Vue指令
  • Vue3 更新了 directive 内的生命周期指令,使得指令跟组件生命周期一样方便我们记,以后有空写自定义指令的时候再详细解读
Vue.directive('focus', {
    // el 是 真实的 DOM
    // binding 是 配置项,包括了参数,修饰语句等
    // vnode 是 Vue 的虚拟 DOM
    // bind 会在指令第一次绑定到元素时调用
    bind(el, binding, vnode) {
        console.log(el);
        console.log(binding);
        console.log(vnode);
        console.log('bind');
    },
    // inserted 会在被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
    inserted(el, binding) {
        console.log('inserted');
        el.focus();
    },
    // 还有其他的生命周期 update、componentUpdated、unbind 等可在官网了解
});

Vue.component("ComponentA", {
    template: `<div>
        ComponentA
        <!-- .a.b 及 132 + 123 可在 binding 内获取 -->
    	<input v-focus.a.b="132 + 123" />
    </div>`
});

const app = new Vue({
    el: '#app',
    template: `<div>
    	<ComponentA></ComponentA>
    </div>`
});