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>
以上是對組件化小小的總結,后期也會不斷的更新,其中可能有紕漏或者有寫錯單詞的情況,大家也可以幫忙指出來,共同學習共同進步~
蘇大強上線
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容