Vue组件化

Vue.png

前言

在工作中经常会用到Vue,包括也会用到很多重要的点例如组件化等等,现在也想对于之前的应用和学习做一个小小的总结~后期也会不定期的更新

组件化

  • 概念:
    Vue组件系统提供了一种抽象,让我们可以使用独立可复用的组件来构建大型应用,任何类型的应用界面都可以抽象为一棵组件树。
  • 思想:
    高内聚低耦合(功能性越单一可复用性就越强)
  • 优点:
    1. 提高开发效率
    2. 方便重复使用
    3. 简化调试步骤
    4. 提升项目可维护性
    5. 便于多人协助开发
      ....

一、组件通信

组件化的重中之重就是组件之间的通信,怎么进行传值可以高效方便的完成功能开发

  • 常用通信方式:
    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被识别(且获取)的特性绑定(classstyle除外)。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定(classstyle除外),并且可以通过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表单分析我们需要实现哪些组件:

  1. KForm (指定数据、校验规则->便于管理,统一传参)
  2. KFormItem (执行校验、显示错误信息)
  3. KInput (维护数据)
  • KInput
    1. 创建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>
    
    1. 使用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
    1. 创建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>
    
    1. 使用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 设置数据模型和校验规则
    1. 创建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>
    
    1. 使用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>
    
    1. 在KFormItem添加
    export default {
      inject: ['form'] // 通过form.rules[prop]可以访问当前表单的校验规则
    }
    
  • 数据校验
    1. 在KInput里面的onIput事件中触发校验
    onInput(e) {
      // $parent指向KFormItem
      this.$parent.$emit('validate')
    }
    
    1. 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()
                }
              })
            })
          }
        }
      }
    
    1. 在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>
    
    1. 在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
    }
    

四、弹窗组件

  • 弹窗这一类组件的特点:
    1. 在当前vue实例之外是独立存在的,通常挂载在body上
    2. 通过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>
以上是对组件化小小的总结,后期也会不断的更新,其中可能有纰漏或者有写错单词的情况,大家也可以帮忙指出来,共同学习共同进步~
苏大强上线
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容