前言
在工作中经常会用到Vue,包括也会用到很多重要的点例如组件化等等,现在也想对于之前的应用和学习做一个小小的总结~后期也会不定期的更新
组件化
- 概念:
Vue组件系统提供了一种抽象,让我们可以使用独立可复用的组件来构建大型应用,任何类型的应用界面都可以抽象为一棵组件树。 - 思想:
高内聚低耦合(功能性越单一可复用性就越强) - 优点:
- 提高开发效率
- 方便重复使用
- 简化调试步骤
- 提升项目可维护性
- 便于多人协助开发
....
一、组件通信
组件化的重中之重就是组件之间的通信,怎么进行传值可以高效方便的完成功能开发
-
常用通信方式:
1. props (parent -> children)
// child props: { msg: String } // parent <HelloWorld msg="Welcome to Vue.js" />
2. event (children -> parent)
// 派发自定义事件 谁派发谁监听 // child this.$event('add',good) // parent <Cart @add="cartAdd($event)" />
3. 事件总线 (任意两个组件)
// 发布订阅模式 // Bus: 事件派发、监听和回调管理 class Bus { constructor() { this.callbacks = {} } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || [] this.callbacks[name].push(fn) } $emit(name, args) { if(this.callbacks[name]) { this.callbacks[name].forEach(cb => cb(args)) } } } // main.js // 工作中通常用Vue代替Bus,因为Vue已经实现了相应接口 Vue.proptotype.$bus = new Bus() // child1 this.$bus.$on('foo',handle) // child2 this.$bus.$emit('foo')
4. vuex (任意两个组件)
创建唯一的全局数据管理者store,通过它管理数据并通知组件状态变更,具体使用大家可以去vux了解学习。
-
自定义事件:
-
边界情况:
注:parent、root、children由于高耦合、强依赖的原因在项目里可根据实际情况使用
1. parent / root
// 兄弟组件之间通信可以通过公共祖辈搭桥,$parent或$root // brother1 this.$parent.$on('foo',handle) // brother2 this.$parent.$emit('foo')
2. $children(自定义组件不包含原始标签)
// 父组件可以通过$children访问子组件实现父子通信 // parent this.$children[0].xx = 'xxxxxx' // 注:$children不能保证子元素的顺序(异步组件)
3. $refs
// 获取子节点引用 // parent <HelloWorld ref="hw" /> mounted() { this.$refs.hw.xx = 'xxxxxx' }
4. provide/inject
// 能够实现祖先和后代之间传值 // 并不是响应式的,但是可以传入响应式的数据 // 后代组件内部声明的变量名称和inject传入的名称冲突,后代组件会覆盖传入的值 // ancestor provide() { // 隔代传参,用法类似于data return { foo: 'foo', app: this // 指的是当前组件实例本身 } } // descendant <p>{{ app.$options.name }}</p> // 当命名冲突还想使用传入的值的时候,给传入的数据起别名 // inject: ['foo','app'], inject: { foo2: 'foo', app: 'app' }, data() { return { foo: 'my-foo' } }
-
非prop特性
注:包含父作用域中不作为prop被识别(且获取)的特性绑定(class和style除外)。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定(class和style除外),并且可以通过v-bind="$attrs"传入内部组件1. $attrs(属性并未在props中声明)
// child:并未在props中声明foo <p>{{ $attrs.foo }}</p> // parent <HelloWorld foo="foo" />
2. $listeners (在子组件中只负责触发回调函数但是在父组件中处理回调函数的逻辑)
// parent <HelloWorld @click="onClick"/> // child 使用v-on指令将$listeners展开(如果有多个事件,是都会展开的) // $listeners (本身是一个键值对格式的对象 ) // key->所有事件监听器的名称 value->回调函数 // 展开后 <p @click="onClick"></p> <p v-on="$listeners"></p>
-
二、插槽
插槽语法是Vue实现的内容分发API,用于复合组件开发。内容分发简单来说就是内容要在子组件中使用,但是要通过父组件将内容传递进来。
-
匿名插槽
// comp1
<div>
<slot></slot>
</div>
// parent
<Comp1>Hello</Comp1>
-
具名插槽
// 将内容分发到子组件指定位置
// comp2
<div>
<slot></slot>
<slot name="content"></slot>
</div>
// parent
<Comp2>
// 默认插槽用default做参数
<template v-slot:default>匿名插槽</template>
// 具名插槽用插槽名做参数
<template v-slot:content>内容...</template>
</Comp2>
-
作用域插槽
数据在子组件中,但是要在插槽中使用
// comp3
<div>
<slot :foo="foo"></slot>
</div>
// parent
<Comp3>
// 把v-slot的值指定为作用域上下文对象
<template v-slot:default="slotProps">
来自子组件中的数据{{ slotProps.foo }}
</template>
// 解构赋值写法
<template v-slot:default="{foo}">
来自子组件中的数据{{ foo }}
</template>
</Comp3>
上面是对Vue组件化包括组件通信以及插槽的一些介绍,接下来要通过几个在工作中常用的实例来实践一下
一、表单组件
通用表单组件,参考element表单分析我们需要实现哪些组件:
- KForm (指定数据、校验规则->便于管理,统一传参)
- KFormItem (执行校验、显示错误信息)
- KInput (维护数据)
-
KInput
- 创建components/form/KInput.vue
<template> <div> // 实现双向数据绑定 @input,:value // 通过v-bind展开$attrs,显示没有在props里面传入的值,例如(placeholder、type) <input :value="value" @input="onInput" v-bind="$attrs" /> </div> </template> <script> export default { inheritAttrs: false, // 将属性继承关闭 props: { value: { type: String, defaule: '' } }, methods: { onInput(e) { // 派发事件,将最新的值传出去 this.$emit('input',e.target.value) } } } </script>
- 使用KInput,创建components/form/index.vue
<template> <div> <h3>KForm表单</h3> <hr /> <KInput v-model="model.username"></KInput> <KInput type="password" v-model="model.password"></KInput> </div> </template> <script> import KInput from './KInput' export default { components: { KInput }, data() { return { model: { username: 'Cherry', password: '' } } } } <script>
-
KFormItem
- 创建components/form/KFormItem.vue
<template> <div> // label标签 <label v-if="label">{{ label }}</label> // 插槽 input <slot></slot> // 错误信息 <p v-if="error">{{ error }}</p> </div> <script> export default { props: { label: { type: String, default: '' }, prop: String // 校验的字段名称 }, // 这个值是否涉及当前组件的状态,如果是就放在data里面 data() { return { error: '' } } } </script> </template>
- 使用KFormItem 在components/form/index.vue添加
<template> <div> <h3>KForm表单</h3> <KFormItem label="用户名" prop="username"> <KInput v-model="model.username"></KInput> </KFormItem> <KFormItem label="密码" prop="password"> <KInput v-model="model.password" type="password"></KInput> </KFormItem> <KFormItem> <button @click="onLogin">登录</button> </KFormItem> </div> </template>
-
KForm 设置数据模型和校验规则
- 创建components/form/KForm.vue
<template> <div> <form> <slot></slot> </form> </div> </template> <script> export default { // 隔层传递数据 provide() { return { // 将表单实例直接传递给后代 form: this } }, props: { model: { type: Object, required: true }, rules: Object } } </script>
- 使用KForm.vue 在components/form/index.vue添加
<template> <div> <KForm :model="model" :rules="rules"> ... </KForm> </div> </template> <script> import KForm from './KForm' export default { components: { KForm }, data() { return { rules: { username: [{ required: true, message: "请输入用户名" }], password: [{required: true, message: "请输入密码" }] }, model: { username: "Cherry", password: "" }, } } } </script>
- 在KFormItem添加
export default { inject: ['form'] // 通过form.rules[prop]可以访问当前表单的校验规则 }
-
数据校验
- 在KInput里面的onIput事件中触发校验
onInput(e) { // $parent指向KFormItem this.$parent.$emit('validate') }
- KFormItem监听校验通知,获取规则并执行校验
// 引入校验库:npm i -S async-validator import Schema from 'async-validator' export default { inject: ['form'], //注入 mounted() { this.$on('validate',() => { this.validate() }) }, methods: { validate() { // 获取校验规则 const rule = this.form.rules[this.prop] // 获取校验值 const val = this.form.model[this.prop] // 获取校验器 Schema参数,key: 校验字段名 value: 校验规则 const validator = new Schema({ [this.prop] : rule }) // 执行校验,参数1校验目标:校验值,参数2回调函数 // 返回Promise对象 return new Promise((resolve,reject) => { validator.validate({ [this.prop] : val },(errors) => { if(errors) { // 校验失败 this.error = errors[0].message reject() } else { // 校验通过 清空error this.error = '' resolve() } }) }) } } }
- 在index.vue添加
<template> <div> <KForm :model="model" :rules="rules" ref="loginForm"> ... <KFormItem> <button @click="onLogin">登录</button> </KFormItem> </KForm> </div> <script> export default { methods: { onLogin() { // 全局校验 this.$refs.loginForm.validate(isValid => { if(isValid) { console.log('success') } else { alert('校验失败!') } }) } } } </script> </template>
- 在KForm.vue添加
// 添加全局校验方法 validate() { // 遍历所有FormItem,执行他们的validate方法 // tasks是返回的Promise数组 const tasks = this.$children .filter(item => item.prop) // 过滤一下没有prop的FormItem .map(item => {}) Promise.all(tasks) .then(() => cb(true)) // 校验通过 返回true .catch(() => cb(false)) // 校验失败 返回false }
四、弹窗组件
-
弹窗这一类组件的特点:
- 在当前vue实例之外是独立存在的,通常挂载在body上
- 通过js动态创建,不需要在任何组件中声明
-
创建utils文件夹,并创建create.js
import Vue from 'vue' // 创建create函数,可以动态生成组件实例,并且挂载至body上 // Component:组件配置对象 function create(Component,props) { // 第一种实现方式:通过Vue实例实现 // 借助Vue的构造函数来动态生成组件实例 const vm = new Vue({ render(h) { // h createElement别名,可以返回一个虚拟dom,VNode return h(Component,{props}) } }) vm.$mount() // 不指定宿主元素,则会创建真实dom,但是不会追加操作 // 通过$el属性获取真实dom,并在body后面做追加操作 document.body.appendChild(vm.$el) // 返回组件实例 const comp = vm.children[0] // 销毁方法 comp.remove = () => { document.body.removeChild(vm.$el) vm.destroy() } // 第二种实现方式:通过Vue.extend()实现 const Ctor = Vue.extend(Component) // 构造函数 // 创建组件实例 const comp = new Ctor({propsData:props}) // 挂载 comp.$mount() document.body.appendChild(comp.$el) comp.remove = () => { document.body.removeChild(comp.$el) comp.$destroy() } return comp } // 暴露调用接口 export default create
-
创建通知组件:Notice.vue
<template> <div class="box" v-if="isShow"> <h3>{{ title }}</h3> <p class="box-content">{{ message} }</p> </div> </template> <script> export default { props: { title: { // 标题 type: String, default: "" }, message: { // 信息 type: String, default: "" }, duration: { // 时间 type: Number, default: 1000 } }, data() { return { isShow: false } }, methods: { show() { // 显示 this.isShow = true // 自动隐藏 setTimeout(this.hide,this.duration) }, hide() { // 隐藏 this.isShow = false // 销毁 this.remove() } } } </script> <style> .box { position: fixed; width: 100%; top: 16px; left: 0; text-align: center; pointer-events: none; background-color: #fff; border: grey 3px solid; box-sizing: border-box; } .box-content { width: 200px; margin: 10px auto; font-size: 14px; padding: 8px 16px; background: #fff; border-radius: 3px; margin-bottom: 8px; } </style>
-
-
使用create.js在index.vue中
// 引入 import create from "@/utils/create" import Notice from "@/components/Notice" export default { onLogin() { // 全局校验 this.$refs.loginForm.validate(isValid => { if(isValid) { console.log('success') } else { // 传入值第一个参数组件,第二个参数是配置项 this.$create(Notice,{ title: '校验失败', message: '校验错误,请重试', duration: 3000 }).show() } }) } }
五、递归组件
递归组件是可以在它们自己模板中调用自身的组件,主要是针对树形结构的数据进行展示,在工作中的应用场景也是很多的
-
创建Node.vue
<template>
<div>
<div @click="toggle" :style="{ paddingLeft: (level-1)+'em' }">
<label>
{{ model.name }}
</label>
<span v-if="isFolder">[{{ open ? '-' : '+' }}]</span>
</div>
<div v-show="open" v-if="isFolder">
<Node
class="item"
v-for="model in model.children"
:model="model"
:key="model.name"
:level="level + 1"
></Node>
</div>
</div>
</template>
<script>
export default {
name: 'Node',
props: {
model: Object,
level: {
type: Number,
default: 0
}
},
data() {
return {
open: false
}
},
computed: {
isFolder: function() {
return this.model.children && this.model.children.length
}
},
methods: {
toggle: function() {
if(this.isFolder) {
this.open = !this.open
}
}
}
}
</script>
-
创建Tree.vue
<template>
<div class="tree">
<Node v-for="item in date" :key="item.name" :model="item"></Node>
</div>
</template>
<script>
import Node from './Node'
export default {
name: 'Tree',
props: {
data: {
type: Array,
required: true
}
},
components: {
Node
}
}
</script>
<style>
.tree {
text-align: left;
}
</style>
-
使用Tree.vue在Index.vue
<template>
<div>
<Node :data="treeData"></Node>
</div>
</template>
<script>
import Node from '@/components/Tree'
export default {
components: {
Node
},
data() {
return {
treeData: [
{
name: '水果',
children:[
{
name: '南方水果',
children: [
...
]
},
{
name: '北方水果'
},
]
},
{
name: '蔬菜'
}
]
}
}
}
</script>