• vue组件精讲


    一、组件分类

    1. 由 vue-router
      产生的每个页面,它本质上也是一个组件(.vue)。一般不会有prop选项和自定义事件,不会被复用也不会对外提供接口 。
    2. 不包含业务,独立、具体功能的基础组件。这类组件作为项目的基础控件会被大量使用,组件的API进行过高强度的抽象,可以通过不同配置实现不同的功能。独立组件的开发难度要高于第一类组件,因为它的侧重点是API的设计、兼容性、性能、以及复杂的功能。
    3. 业务组件。不像第二类独立组件只包含某个功能,这类组件在业务中被多个页面复用,但只在当前项目会用到,不具有通用性,而且会包含一些业务。业务组件更像是介于第一类和第二类之间,在开发上也与独立组件类似,但寄托于项目,有必要考虑组件的可维护性和复用性。

    二、prop、event、slot

    组件都是由prop、event、slot三部分组成,它们构成了 Vue.js 组件的 API

    1. 属性prop

    prop 定义了这个组件有哪些可配置的属性,组件的核心功能也都是它来确定的。写通用组件时,props最好用对象的写法,这样可以针对每个属性设置类型、默认值或自定义校验属性的值

    比如封装一个按钮组件 :

    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    使用组件

    
    
    
    • 1
    • 2

    组件里定义的 props,都是修改定义在 data 里单向数据流,也就是只能通过父级修改,组件自己不能修改 props 的值,只能的数据,非要修改,也是通过后面介绍的自定义事件通知父级,由父级来修改。

    2.插槽 slot

    如果要给上面的按钮组件 添加一些文字内容,就要用到组件的第二个 API:插槽 slot,它可以分发组件的内容,比如在上面的按钮组件中定义一个插槽:

    子组件:

    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这里的 节点就是指定的一个插槽的位置,这样在组件内部就可以扩展内容了:

    父组件(是调用的子组件):

    按钮 1
    
    	按钮 2
    
    
    • 1
    • 2
    • 3
    • 4

    再举一个例子:

    //父组件
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    //子组件slotOne1
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    显示为:

    我是父组件我是slotOne1组件我是父组件插槽内容
    
    • 1

    当需要多个插槽时,会用到具名 slot,比如上面的组件我们再增加一个 slot,用于设置另一个图标组件:

    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    
    
    按钮 3
    
    
    • 1
    • 2
    • 3
    • 4

    这样,父级内定义的内容,就会出现在组件对应的 slot 里,没有写名字的,就是默认的 slot。

    在组件的 里也可以写一些默认的内容,这样在父级没有写任何 slot 时,它们就会出现,比如:

    提交
    
    • 1

    自定义事件 event

    给组件 加一个点击事件,目前有两种写法,先看自定义事件 event(部分代码省略):

    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    通过 $emit,就可以触发自定义的事件 on-click ,在父级通过 @on-click 来监听:

    
    
    • 1

    上面的 click 事件,是在组件内部的 元素上声明的,这里还有另一种方法,直接在父级声明,但为了区分原生事件和自定义事件,要用到事件修饰符 .native,所以上面的示例也可以这样写:

    
    
    • 1

    如果不写 .native 修饰符,那上面的 @click 就是自定义事件 click,而非原生事件 click,但我们在组件内只触发了 on-click 事件,而不是 click,所以直接写 @click 会监听不到。

    三、组件的通信

    组件一般有以下几种关系: A.vue–>B.vue–>C.vue ;B.vue–>D.vue A 和 B、B 和 C、B 和 D 都是父子关系,C 和 D 是兄弟关系,A 和 C 是隔代关系(可能隔多代)。组件间经常会通信,Vue.js 内置的通信手段一般有两种:

    • ref:给元素或组件注册引用信息;
    • $parent / $children:访问父 / 子实例。
      这两种都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据,

    1. ref

    比如下面的示例中,用 ref 来访问组件(部分代码省略):

    // component-aexport default {
    	data () {
    		return {
    			title: 'Vue.js'
    		}
    	},
    	methods: {
    		sayHello () {
    			window.alert('Hello');
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2. $parent / $children

    $parent 和 $children 类似,也是基于当前上下文访问父组件或全部子组件的。

    $children : 当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用 $children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件,并且使用 Array 作为真正的来源。

    $parent: 当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。

    这两种方法的弊端是,无法在跨级或兄弟间通信

    3.provide / inject

    provide / inject可以解决上述弊端

    A.vue–>B.vue,用法:

    // A.vue
    export default {
    	provide: {
    		name: 'Aresn'
    	}
    }
    
    // B.vue
    export default {
    	inject: ['name'],
    		mounted () {
    			console.log(this.name); // Aresn
    		}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    provide / inject替代 Vuex
    Vuex 做状态管理,它是一个专为 Vue.js 开发的状态管理模式,用于集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

    使用 Vuex,最主要的目的是跨组件通信、全局数据维护、多人协同开发。需求比如有:用户的登录信息维护、通知信息维护等全局的状态和数据。

    一般在 webpack 中使用 Vue.js,都会有一个入口文件 main.js,里面通常导入了 Vue、VueRouter、iView 等库,通常也会导入一个入口组件 app.vue 作为根组件。一个简单的 app.vue 可能只有以下代码:

    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    使用 provide / inject 替代 Vuex,就是在这个 app.vue 文件上做文章。把app.vue 理解为一个最外层的根组件,用来存储所有需要的全局数据和状态,甚至是计算属性(computed)、方法(methods)等。因为你的项目中所有的组件(包含路由),它的父组件(或根组件)都是 app.vue,所以我们把整个 app.vue 实例通过 provide 对外提供。

    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    把整个 app.vue 的实例 this 对外提供,命名为 app(这个名字可以自定义,推荐使用 app,使用这个名字后,子组件不能再使用它作为局部属性)。接下来,任何组件(或路由)只要通过 inject 注入 app.vue 的 app 的话,都可以直接通过 this.app.xxx 来访问 app.vue 的 data、computed、methods 等内容。

    app.vue 是整个项目第一个被渲染的组件,而且只会渲染一次(即使切换路由,app.vue 也不会被再次渲染),利用这个特性,很适合做一次性全局的状态数据管理。

    //app.vue
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    其它任何界面或组件通过inject注入app 后就可以访问 userInfo 的数据了:

    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在其他页面导致userInfo更新,需要重新获取时:

    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    mixins
    例如上面的例子中将用户信息放到混合中:

    //user.js
    export default {
    	data () {
    		return {
    			userInfo: null
    		}
    	},
    	methods: {
    		getUserInfo () {
    		// 这里通过 ajax 获取用户信息后,赋值给 this.userInfo,以下为伪代码
    			$.ajax('/user/info', (data) => {
    				this.userInfo = data;
    			});
    		}
    	},
    	mounted () {
    	this.getUserInfo();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在app.vue中混合:

    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    app.vue可以直接使用user.js的数据和方法,这样比较容易维护

    只要一个组件使用了 provide 向下提供数据,那其下所有的子组件都可以通过 inject 来注入,不管中间隔了多少代,而且可以注入多个来自不同父级提供的数据。需要注意的是,一旦注入了某个数据,比如上面示例中的 app,那这个组件中就不能再声明 app 这个数据了,因为它已经被父级占有。

    4. on/emit实现父子通信

    $emit 会在当前组件实例上触发自定义事件,并传递一些参数给监听器的回调,一般来说,都是在父级调用这个组件时,使用 @on 的方式来监听自定义事件的,比如在子组件中触发事件:

    // child.vue,部分代码省略
    export default {
    	methods: {
    		handleEmitEvent () {
    			this.$emit('test', 'Hello Vue.js');
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在父组件中监听由 child.vue 触发的自定义事件 test:

    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这里看似是在父组件 parent.vue 中绑定的自定义事件 test 的处理句柄,然而事件 test 并不是在父组件上触发的,而是在子组件 child.vue 里触发的,只是通过 v-on 在父组件中监听。既然是子组件自己触发的,那它自己也可以监听到,这就要使用 $on 来监听实例上的事件,换言之,组件使用 $emit 在自己实例上触发事件,并用 $on 监听它。

    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    **$on 监听了自己触发的自定义事件 test,**因为有时不确定何时会触发事件,一般会在 mounted 或 created 钩子中来监听。

    四、Vue 的构造器——extend 与手动挂载——$mount

    1.使用场景

    在写vue.js时不论是用 CDN 的方式还是在 Webpack 里用 npm 引入的 Vue.js,都会有一个根节点,并且创建一个根实例:

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Webpack一般在入口文件 main.js 里,最后会创建一个实例:

    import Vue from 'vue';
    import App from './app.vue';
    new Vue({
    	el: '#app',
    	render: h => h(App)
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    因为用 Webpack 基本都是前端路由的,它的 html 里一般都只有一个根节点
    。组件只需注册即可使用,有一下几个特点:

    • 所有的内容,都是在 #app 节点内渲染的;
    • 组件的模板,是事先定义好的;
    • 由于组件的特性,注册的组件只能在当前位置渲染。

    常规的组件无法解决以下问题:

    • 组件的模板是通过调用接口从服务端获取的,需要动态渲染组件;
    • 实现类似原生 window.alert() 的提示框组件,它的位置是在 下,而非 ,并且不会通过常规的组件自定义标签的形式使用,而是像 JS 调用函数一样使用

    对于这两种场景可以使用Vue.extend 和 vm.$mount语法解决。

    2.用法

    创建一个 Vue 实例时,都会有一个选项 el,来指定实例的根节点,如果不写 el 选项,那组件就处于未挂载状态。Vue.extend 的作用,就是基于 Vue 构造器,创建一个“子类”,它的参数跟 new Vue 的基本一样,但 data 要跟组件一样,是个函数,再配合 $mount ,就可以让组件渲染,并且挂载到任意指定的节点上,比如 body。

    import Vue from 'vue';
    
    const AlertComponent = Vue.extend({
    	template: '
    {{ message }}
    ', data () { return { message: 'Hello, Aresn' }; }, });
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这一步创建了一个构造器,这个过程就可以解决异步获取 template 模板的问题,下面要手动渲染组件,并把它挂载到 body 下:

    const component = new AlertComponent().$mount();
    
    • 1

    这一步调用了 $mount 方法对组件进行了手动渲染,但它仅仅是被渲染好了,并没有挂载到节点上,也就显示不了组件。此时的 component 已经是一个标准的 Vue 组件实例,因此它的 $el 属性也可以被访问:

    document.body.appendChild(component.$el);
    
    • 1

    除了 body还可以挂载到其它节点上。

    $mount 也有一些快捷的挂载方式,以下两种都是可以的:

    // 在 $mount 里写参数来指定挂载的节点new AlertComponent().$mount('#app');
    // 不用 $mount,直接在创建实例时指定 el 选项new AlertComponent({ el: '#app' });
    
    • 1
    • 2

    实现同样的效果,除了用 extend 外,也可以直接创建 Vue 实例,并且用一个 Render 函数来渲染一个 .vue 文件:

    import Vue from 'vue';
    import Notification from './notification.vue';
    
    const props = {}; // 这里可以传入一些组件的 props 选项
    
    const Instance = new Vue({
    	render (h) {
    		return h(Notification, {
    		props: props
    		});
    	}
    });
    
    const component = Instance.$mount();
    document.body.appendChild(component.$el);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    渲染后,如果想操作 Render 的 Notification 实例:

    const notification = Instance.$children[0];
    
    • 1

    因为 Instance 下只 Render 了 Notification 一个子组件,所以可以用 $children[0] 访问到。
    需要注意的是,采用 $mount 手动渲染的组件,如果要销毁,也要用 $destroy 来手动销毁实例,必要时,也可以用 removeChild 把节点从 DOM 中移除。

    四、动态渲染 .vue 文件的组件—— Display

    动态渲染的核心技术就是extend 和 $mount。

    1.接口设计

    一个常规的 .vue 文件一般都会包含 3 个部分:

    -