vue2组件化
组件分类
- 由 vue-router产生的每个页面。
- 不包含业务 独立 具体功能的基础组件
业务组件 业务组件只在当前项目中会用到,不具有通用性,而且会包含一些业务,比如数据请求,而独立组件不包含业务,在任何项目中都可以使用,功能单一。
Vue.js组件的三个API
prop
- event
-
组件的通信
ref 给元素或组件注册引用信息
- $parent / $children 访问父 / 子实例
- eventbus
- provide / inject app.vue理解为最外层的根组件,用来存储所有需要的全局数据和状态,甚至是计算属性 方法。 项目中所有的组件(包含路由) 它的父组件都是 app.vue。所以我们把整个app.vue实例通过 provide 对外提供。 也可以使用vue mixins的混入来减少 app.vue写过多的代码
- 使用 $attrs 与 $listeners实现多层嵌套传递。
现在需要在A中对C的props赋值,监听 C的emit事件
// A组件
<template>
<div>
<h2>组件A 数据项:{{myData}}</h2>
<B @changeMyData="changeMyData" :myData="myData"></B>
</div>
</template>
<script>
import B from "./B";
export default {
data() {
return {
myData: "100"
};
},
components: { B },
methods: {
changeMyData(val) {
this.myData = val;
}
}
};
</script>
// B组件
<template>
<div>
<h3>组件B</h3>
<C v-bind="$attrs" v-on="$listeners"></C>
</div>
</template>
<script>
import C from "./C";
export default {
components: { C },
};
</script>
// C组件
<template>
<div>
<h5>组件C</h5>
<input v-model="myc" @input="hInput" />
</div>
</template>
<script>
export default {
props: { myData: { String } },
created() {
this.myc = this.myData; // 在组件A中传递过来的属性
console.info(this.$attrs, this.$listeners);
},
methods: {
hInput() {
this.$emit("changeMyData", this.myc); // // 在组件A中传递过来的事件
}
}
};
</script>
- 自行实现 dispatch 和 broadcast 方法
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
const name = child.$options.name;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.name;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};
vm.$mount
如果Vue实例在实例化时没有收到el选项,则它处于 ‘未挂载’ 状态,没有关联的DOM元素。可以使用vm.$mount()手动挂载一个未挂载的实例。
如果没有提供 elementOrSelector参数,模版将被渲染为文档之外的元素,并且你必须使用原生dom api把它插入文档中。
var MyComponent = Vue.extend({
template: '<div>Hello!</div>'
})
// 创建并挂载到 #app (会替换 #app)
new MyComponent().$mount('#app')
// 同上
new MyComponent({ el: '#app' })
// 或者,在文档之外渲染并且随后挂载
var component = new MyComponent().$mount()
document.getElementById('app').appendChild(component.$el)
具有数据校验功能的表单组件—Form
Form 和 FormItem 两个组件主要做数据校验,用不到event。Form的slot 就是一系列的FormItem,FormItem的slot就是具体的表单控件,比如输入框
在Form组件中,定义两个props:
- model:表单控件绑定的数据对象,在校验或重置时会访问该数据对象下对应的表单数据,类型为 Object
- rules:表单验证规则,async-validator 所使用的校验规则,类型为 Object
在 FormItem 组件中 也定义两个 props
- label:单个表单组件的标签文本,类似原生的
prop:对应表单域Form组件 model里的字段,用于在校验或重置时访问表单组件绑定的数据,类型为String ```
<i-input v-model="formValidate.name"></i-input>
<i-input v-model="formValidate.mail"></i-input>
<a name="yGJAc"></a>
### 在Form中缓存FormItem实例
Form组件的核心功能是数据校验,一个Form中包含了多个FormItem,当提交按钮时,要逐一对每个FromItem内的表单组件校验,而校验是由使用者发起,并通过Form来调用每一个FormItem的验证方法,再将校验结果汇总后,通过Form返回出去。
1. 因为要在Form中逐一调用FormItem的验证方法,而Form和FormItem是独立的,需要预先将FormItem的每个实例缓存在Form中,当每个FormItem渲染时,将其自身(this)作为参数通过 dispatch 派发到Form组件中,然后通过一个数组缓存起来;同理当FormItem销毁时,将其从Form缓存的数组中移除。
// form-item.vue,部分代码省略
import Emitter from ‘../../mixins/emitter.js’;
export default { name: ‘iFormItem’, mixins: [ Emitter ], // 组件渲染时,将实例缓存在 Form 中 mounted () { // 如果没有传入 prop,则无需校验,也就无需缓存 if (this.prop) { this.dispatch(‘iForm’, ‘on-form-item-add’, this); } }, // 组件销毁前,将实例从 Form 的缓存中移除 beforeDestroy () { this.dispatch(‘iForm’, ‘on-form-item-remove’, this); } }
注意,Vue.js 的组件渲染顺序是由内而外的,所以 FormItem 要先于 Form 渲染,在 FormItem 的 mounted 触发时,我们向 Form 派发了事件 on-form-item-add,并将当前 FormItem 的实例(this)传递给了 Form,而此时,Form 的 mounted 尚未触发,因为 Form 在最外层,如果在 Form 的 mounted 里监听事件,是不可以的,所以要在其 created 内监听自定义事件,Form 的 created 要先于 FormItem 的 mounted
// form.vue,部分代码省略 export default { name: ‘iForm’, data () { return { fields: [] }; }, created () { this.$on(‘on-form-item-add’, (field) => { if (field) this.fields.push(field); }); this.$on(‘on-form-item-remove’, (field) => { if (field.prop) this.fields.splice(this.fields.indexOf(field), 1); }); } }
<a name="vtV1l"></a>
### 触发校验
Form 支持两种事件来触发校验:
- **blur**:失去焦点时触发,常见的有输入框失去焦点时触发校验;
- **change**:实时输入时触发或选择时触发,常见的有输入框实时输入时触发校验、下拉选择器选择项目时触发校验等。
Input 组件中,绑定在 <input> 元素上的原生事件 @input,每当输入一个字符,都会调用句柄 handleInput,并通过 dispatch 方法向上级的 FormItem 组件派发自定义事件 on-form-change;同理,绑定的原生事件 @blur 会在 input 失焦时触发,并传递事件 on-form-blur<br />基础组件有了,接下来要做的,是在 FormItem 中监听来自 Input 组件派发的自定义事件。这里可以在 mounted 中监听,因为你的手速远赶不上组件渲染的速度,不过在 created 中监听也是没任何问题的<br />通过调用 setRules 方法,监听表单组件的两个事件,并绑定了句柄函数 onFieldBlur 和 onFieldChange,分别对应 blur 和 change 两种事件类型。当 onFieldBlur 或 onFieldChange 函数触发时,就意味着 FormItem 要对**当前的数据**进行一次校验。当前的数据,指的就是通过表单域 Form 中定义的 props:model,结合当前 FormItem 定义的 props:prop 来确定的数据<br />在FormItem的 validate() 方法中,最终做了两件事
1. 设置了当前的校验状态 validateState 和校验不通过提示信息 validateMessage(通过值为空);
2. 将 validateMessage 通过回调 callback 传递给调用者,这里的调用者是 onFieldBlur 和 onFieldChange,它们只传入了第一个参数 trigger,callback 并未传入,因此也不会触发回调,而这个回调主要是给 Form 用的,因为 Form 中可以通过提交按钮一次性校验所有的 FormItem(后文会介绍)这里只是表单组件触发事件时,对当前 FormItem 做校验
在 Form 组件中,预先缓存了全部的 FormItem 实例,自然也能在 Form 中调用它们。通过点击提交按钮全部校验,或点击重置按钮全部重置数据,只需要在 Form 中,逐一调用缓存的 FormItem 实例中的 validate 或 resetField 方法
<a name="nLxqZ"></a>
### Form组件总结
1. 包含三类组件 form form-item input
2. from 组件
- 接受 model rules 并 provide this
- create 监听 form-item-add form-item-remove
- 提供 resetFields validate
3. form-item
- 接受 label prop
- mountend 阶段 dispatch on-form-item-add
- dispatch 后 setRules 获取当前 rules 并监听 on-form-blur on-form-change -> validate('change') -> newAsyncValidator(descriptor)展示当前校验结果
- beforeDestroy on-form-item-remove
4. input input change 触发 on-form-change on-form-blur
<a name="YJTlD"></a>
## 找到任意组件实例-findComponents系列方法
它适用于以下场景:
- 由一个组件,向上找到最近的指定组件;
- 由一个组件,向上找到所有的指定组件;
- 由一个组件,向下找到最近的指定组件;
- 由一个组件,向下找到所有指定的组件;
- 由一个组件,找到指定组件的兄弟组件。
<a name="NiGzZ"></a>
### 实现
5个函数的原理,都是通过递归 遍历,找到指定组件的 name 选项的组件实例并返回
<a name="L094v"></a>
### 向上找到最近的指定组件-- findComponentUpward
// assist.js // 由一个组件,向上找到最近的指定组件 function findComponentUpward (context, componentName) { let parent = context.$parent; let name = parent.$options.name;
while (parent && (!name || [componentName].indexOf(name) < 0)) { parent = parent.$parent; if (parent) name = parent.$options.name; } return parent; } export { findComponentUpward };
第一个参数当前上下文,比如你要基于哪个组件来向上寻找,一般都是基于当前的组件,也就是传入 this;第二个参数就是要找的组件的 name。<br />findComponentUpward 方法会在 while 语句里不断向上覆盖当前的 parent 对象,通过判断组件(即 parent)的 name 与传入的 componentName 是否一致,直到直到最近的一个组件为止。<br />findComponentUpward 是直接拿到组件的实例,而非通过事件通知组件
<a name="NIsYP"></a>
### 向上找到所有的指定组件-- findComponentsUpward
// assist.js // 由一个组件,向上找到所有的指定组件 function findComponentsUpward (context, componentName) { let parents = []; const parent = context.$parent;
if (parent) { if (parent.$options.name === componentName) parents.push(parent); return parents.concat(findComponentsUpward(parent, componentName)); } else { return []; } } export { findComponentsUpward };
<a name="QVBWx"></a>
### 向下找到最近的指定组件-- findComponentDownward
// assist.js // 由一个组件,向下找到最近的指定组件 function findComponentDownward (context, componentName) { const childrens = context.$children; let children = null;
if (childrens.length) { for (const child of childrens) { const name = child.$options.name;
if (name === componentName) {
children = child;
break;
} else {
children = findComponentDownward(child, componentName);
if (children) break;
}
}
} return children; } export { findComponentDownward };
context.$children 得到的是当前组件的全部子组件,所以需要遍历一遍,找到有没有匹配到的组件 name,如果没找到,继续递归找每个 $children 的 $children,直到找到最近的一个为止。
<a name="gYVAJ"></a>
### 向下找到所有指定的组件——findComponentsDownward
// assist.js // 由一个组件,向下找到所有指定的组件 function findComponentsDownward (context, componentName) { return context.$children.reduce((components, child) => { if (child.$options.name === componentName) components.push(child); const foundChilds = findComponentsDownward(child, componentName); return components.concat(foundChilds); }, []); } export { findComponentsDownward };
<a name="P4OPE"></a>
### 找到指定组件的兄弟组件——findBrothersComponents
// assist.js // 由一个组件,找到指定组件的兄弟组件 function findBrothersComponents (context, componentName, exceptMe = true) { let res = context.$parent.$children.filter(item => { return item.$options.name === componentName; }); let index = res.findIndex(item => item._uid === context._uid); if (exceptMe) res.splice(index, 1); return res; } export { findBrothersComponents };
<a name="f7ZDW"></a>
## 组合多选框组件——CheckboxGroup & Checkbox
<a name="i2I5R"></a>
### Checkbox
v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:
- text 和 textarea 元素使用 value property 和 input 事件;
- checkbox 和 radio 使用 checked property 和 change 事件;
- select 字段将 value 作为 prop 并将 change 作为事件。
- 多个复选框,绑定到同一个数组
Checked names: {{ checkedNames }}
new Vue({
el: ‘…’,
data: {
checkedNames: []
}
})
单独的Checkbox组件
- value change v-model
- value
- trueValue falseValue
- label
- props -> value emit change 完成 v-model 使用时通过 v-model ="single"
- 单独的Checkbox 组件就是自定义组件的v-model
CheckboxGroup组件
- props:value,与 Checkbox 的类似,用于 v-model 双向绑定数据,格式为数组;
- events:on-change,同 Checkbox;
- slots:默认,用于放置 Checkbox。
- CheckboxGroup组件其实就是 多个复选框,绑定到同一个数组。最后要把绑定数组对象以v-model形式传递到外部
<i-checkbox v-model="single">单独选项</i-checkbox>
<br>
数据:{{ single }}
<br><br>
<i-checkbox-group v-model="multiple">
<i-checkbox label="option1">选项 1</i-checkbox>
<i-checkbox label="option2">选项 2</i-checkbox>
<i-checkbox label="option3">选项 3</i-checkbox>
<i-checkbox label="option4">选项 4</i-checkbox>
</i-checkbox-group>
<br>
数据:{{ multiple }}
<a name="SwfJv"></a>
### 自定义组件 v-model
一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于[不同的目的](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#Value)。model 选项可以用来避免这样的冲突:
Vue.component(‘base-checkbox’, {
model: {
prop: ‘checked’,
event: ‘change’
},
props: {
checked: Boolean
},
template: <input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
})
<a name="l11di"></a>
## Vue的构造器 -- extend 与手动挂载 --$mount
创建一个Vue实例时,都会有一个选项 el,来指定实例的根节点,如果不写el选项,那组件就处于未挂载状态。<br />Vue.extend的作用 就是基于Vue构造器,创建一个 子类,它的参数和 new Vue的基本一致,但data要跟组件一样,是一个函数,再配合$mount,就可以让组件渲染,并且挂载到任意指定的节点上,比如 body。
<a name="Zk9wy"></a>
## 动态渲染.vue文件的组件 -Display
Display 是一个功能型的组件,没有交互和事件,只需要一个 prop:code 将 .vue 的内容传递过来,其余工作都是在组件内完成的,这对使用者很友好<br />将.vue文件进行分割 -> template js css
// display.vue,部分代码省略
export default {
methods: {
getSource (source, type) {
const regex = new RegExp(<${type}[^>]*>
);
let openingTag = source.match(regex);
if (!openingTag) return '';
else openingTag = openingTag[0];
return source.slice(source.indexOf(openingTag) + openingTag.length, source.lastIndexOf(`</${type}>`));
},
splitCode () {
const script = this.getSource(this.code, 'script').replace(/export default/, 'return ');
const style = this.getSource(this.code, 'style');
const template = '<div id="app">' + this.getSource(this.code, 'template') + '</div>';
this.js = script;
this.css = style;
this.html = template;
},
renderCode () {
this.splitCode();
if (this.html !== '' && this.js !== '') {
const parseStrToFunc = new Function(this.js)();
parseStrToFunc.template = this.html;
const Component = Vue.extend( parseStrToFunc );
this.component = new Component().$mount();
this.$refs.display.appendChild(this.component.$el);
}
if (this.css !== '') {
const style = document.createElement('style');
style.type = 'text/css';
style.id = this.id;
style.innerHTML = this.css;
document.getElementsByTagName('head')[0].appendChild(style);
}
}
} }
在 iView Run 里,默认是直接可以写 iView 组件库的全部组件,并没有额外引入,这是因为 Display 所在的工程,已经将 iView 安装在了全局,Vue.extend 在构造实例时,已经可以使用全局安装的插件了,如果你还全局安装了其它插件,比如 axios,都是可以直接使用的。
<a name="QNDAV"></a>
## 全局提示组件--$Alert
<a name="UlD60"></a>
### alert.vue
``` is 动态绑定的是一个组件对象(Object),它直接指向 a / b / c 三个组件中的一个。除了直接绑定一个 Object,还可以是一个 String,比如标签名、组件名。下面的这个组件,将原生的按钮 button 进行了封装,如果传入了 prop: to,那它会渲染为一个 标签,用于打开这个链接地址,如果没有传入 to,就当作普通 button 使用