[TOC]

vue2组件化

组件分类

  1. 由 vue-router产生的每个页面。
  2. 不包含业务 独立 具体功能的基础组件
  3. 业务组件 业务组件只在当前项目中会用到,不具有通用性,而且会包含一些业务,比如数据请求,而独立组件不包含业务,在任何项目中都可以使用,功能单一。

    Vue.js组件的三个API

  4. prop

  5. event
  6. slot

    组件的通信

  7. ref 给元素或组件注册引用信息

  8. $parent / $children 访问父 / 子实例
  9. eventbus
  10. provide / inject app.vue理解为最外层的根组件,用来存储所有需要的全局数据和状态,甚至是计算属性 方法。 项目中所有的组件(包含路由) 它的父组件都是 app.vue。所以我们把整个app.vue实例通过 provide 对外提供。 也可以使用vue mixins的混入来减少 app.vue写过多的代码
  11. 使用 $attrs 与 $listeners实现多层嵌套传递。

image.png
现在需要在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>
  1. 自行实现 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:

  1. model:表单控件绑定的数据对象,在校验或重置时会访问该数据对象下对应的表单数据,类型为 Object
  2. rules:表单验证规则,async-validator 所使用的校验规则,类型为 Object

在 FormItem 组件中 也定义两个 props

  1. label:单个表单组件的标签文本,类似原生的
  2. prop:对应表单域Form组件 model里的字段,用于在校验或重置时访问表单组件绑定的数据,类型为String ```

<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

``` - 渲染notices数组数据 - 提供 add remove方法 - .alert fixed .alert-content 根据数据在容器内展示 ### notification.js ``` import Alert from './alert.vue'; import Vue from 'vue'; console.log('notification:',Alert) Alert.newInstance = properties => { const props = properties || {}; const Instance = new Vue({ data: props, render (h) { return h(Alert, { props: props }); } }); const component = Instance.$mount(); document.body.appendChild(component.$el); // 就是 Render 的 Alert 组件实例 const alert = Instance.$children[0]; return { add (noticeProps) { alert.add(noticeProps); }, remove (name) { alert.remove(name); } } }; export default Alert; ``` - Alert 实例添加 newInstance 并在其中 生成 Alert 实例 挂载到body 并 返回 可以操作实例 notices数组的方法 ### alert.js 入口 ``` // alert.js import Notification from './notification.js'; let messageInstance; function getMessageInstance () { messageInstance = messageInstance || Notification.newInstance(); return messageInstance; } function notice({ duration = 1.5, content = '' }) { let instance = getMessageInstance(); instance.add({ content: content, duration: duration }); } export default { info (options) { return notice(options); } } ``` 通过单例模式 从 notification.js 获取到实例, 然后提供 instance.add 添加数据 ### 将方法添加到 Vue.prototype上 ``` // src/main.js import Vue from 'vue' import App from './App.vue' import router from './router' import Alert from '../src/components/alert/alert.js' Vue.config.productionTip = false Vue.prototype.$Alert = Alert new Vue({ router, render: h => h(App) }).$mount('#app') ``` ## Table ## Tree 实现一个递归组件的必要条件是: - 要给组件设置 **name**; - 要有一个明确的结束条件 动态组件 ```

``` is 动态绑定的是一个组件对象(Object),它直接指向 a / b / c 三个组件中的一个。除了直接绑定一个 Object,还可以是一个 String,比如标签名、组件名。下面的这个组件,将原生的按钮 button 进行了封装,如果传入了 prop: to,那它会渲染为一个 标签,用于打开这个链接地址,如果没有传入 to,就当作普通 button 使用