01Vue组件化实战

01 组件化实战

组件化

vue组件系统提供了⼀种抽象,让我们可以使用独立可复用的组件来构建大型应用,任意类型的应用界面都可以抽象为⼀个组件树。组件化能提高开发效率,方便重复使用,简化调试步骤,提升项目可维护性,便于多人协同开发。

image.png

组件通信常用方式

  • props

  • event

  • vuex

自定义事件

  1. 边界情况

    • $parent

    • $children

    • $root

    • $refs

    • provide/inject

  2. 非prop特性

    • $attrs

    • $listeners

组件通信

props

父子传值

// child
props: { msg: String }

// parent
<HelloWord msg="测试父子传值" />

自定义事件

子父传值

// child
this.$emit('sonToFather', 'son-->Father')

// parent
<Cart @sonToFather="testSonToFather($event)"></Cart>

事件总线

任意两个组件之间传值常用事件总线或 vuex 的方式。

// Bus: 事件派发、监听和回调管理
class Bus {
  constructor() {
    this.event = {}
  }
  // 订阅事件
  $on (eventName, callback) {
    if (!this.event[eventName]) {
      this.event[eventName] = []
    }
    this.event[eventName].push(callback)
  }
  // 触发事件(发布事件)
  $emit (eventName, params) {
    let eventArr = this.event[eventName]
    if (eventArr) {
      eventArr.map(item => {
        item(params)
      })
    }
  }
  // 删除订阅事件
  $off (eventName, callback) {
    let arr = this.event[eventName]
    if (arr) {
      if (callback) {
        let index = arr.indexOf(callback)
        arr.splice(index, 1)
      } else {
        arr.length = 0
      }
    }
  }
}

// main.js
Vue.prototype.$bus = new Bus()

// child1
this.$bus.$on('testBus',handle)

// child2
this.$bus.$emit('testBus')

实践中通常用 Vue 代替 Bus,因为 Vue 已经实现了相应接口

vuex

组件通信最佳实践

创建唯⼀的全局数据管理者 store,通过它管理数据并通知组件状态变更。

parent/root

兄弟组件之间通信可通过共同祖辈搭桥,parent 或root。

// brother1
this.$parent.$on('testParent',handle)

// brother2
this.$parent.$emit('testParent')

$children

父组件可以通过 $children 访问子组件实现父子通信。

// parent
this.$children[0].xx = 'xxx'

注意:$children 不能保证子元素顺序

和 $refs 有什么区别?

attrs/listeners

包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 ( class 和 style 除外)。当⼀个组件没有
声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

// child:并未在props中声明foo
<p>{{$attrs.foo}}</p>

// parent
<HelloWorld foo="foo"/>

文档

refs

获取子节点引用

// parent
<HelloWorld ref="testRef">

mounted() {
  this.$refs.testRef.xx='xxx'
}

provide/inject

能够实现祖先和后代之间传值

// ancestor
provide() {
  return {foo: 'foo'}
}

// descendant
inject: ['foo']

范例:组件通信

插槽

插槽语法是 Vue 实现的内容分发 API,用于复合组件开发。该技术在通用组件库开发中有大量应用。

匿名插槽

// comp1
<div>
  <slot></slot>
</div>

// parent
<Comp>testSlot</Comp>

具名插槽

将内容分发到子组件指定位置

// 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>
</Comp3>

范例:插槽

组件化实战

通用表单组件

收集数据、校验数据并提交。

需求分析

  1. 实现 KForm

    • 指定数据、校验规则
  2. KformItem

    • label 标签添加

    • 执行校验

    • 显示错误信息

  3. KInput

    • 维护数据

最终效果:Element 表单

范例代码

KInput

创建 components/form/KInput.vue

<template>
  <div>
    <input :value="value" @input="onInput" v-bind="$attrs">
  </div>
</template>

<script>
  export default {
    inheritAttrs:false,
    props:{
      value:{
        type:String,
        default:''
      }
    },
    methods:{
      onInput(e){
        this.$emit('input',e.target.value)
      }
    }
  }
</script>

使用 KInput

创建 components/form/index.vue,添加如下代码:

<template>
  <div>
    <h3>Form表单</h3>
    <hr>
    <k-input v-model="model.username"></k-input>
    <k-input type="password" v-model="model.password"></k-input>>
  </div>
</template>

<script>
import KInput from './KInput'

export default {
  components:{
    KInput
  },
  data(){
    return {
      model:{
        username:'tom',
        password:''
      }
    }
  }
}
</script>

实现 KFormItem

创建components/form/KFormItem.vue

<template>
  <div>
    <label v-if="label">{{label}}</label>
    <slot></slot>
    <p v-if="error">{{error}}</p>
  </div>
</template>

<script>
export default {
  props: {
    label:{ // 输入项标签
      type: String,
      default:''
    },
    prop:{ // 字段名
      type: String,
      default: ''
    }
  },
  data() {
    return {
      error: '' // 校验错误
    }
  }
}
</script>

使用 KFormItem

components/form/index.vue,添加基础代码:

<template>
  <div>
    <h3>Form表单</h3>
    <hr>
    <k-form-item label="用户名" prop="username">
      <k-input v-model="model.username"></k-input>
    </k-form-item>
    <k-form-item label="确认密码" prop="password">
      <k-input type="password" v-model="model.password"></k-input>
    </k-form-item>
  </div>
</template>

实现 KForm

<template>
  <form>
    <slot></slot>
  </form>
</template>

<script>
export default {
  provide() {
    return {
      form: this // 将组件实例作为提供者,子代组件可方便获取
    }
  },
  props:{
    model:{
      type: Object,
      required: true
    },
    rules:{
      type: Object
    }
  }
}
</script>

使用 KForm

components/form/index.vue,添加基础代码:

<template>
  <div>
    <h3>Form表单</h3>
    <hr/>
    <k-form :model="model" :rules="rules" ref="loginForm">
      ...
    </k-form>
  </div>
</template>

<script>
import KForm from './KForm'

export default {
  components: {
    KForm
  },
  data() {
    return {
      rules: {
        username: [{
          required: true,
          message: '请输入用户名'
        }],
        password: [{
          required: true,
          message: '请输入密码'
        }]
      }
    }
  },
  methods: {
    submitForm() {
      this.$refs['loginForm'].validate(valid => {
        if (valid) {
          alert('请求登录')
        } else {
          alert('校验失败')
        }
      })
    }
  }
}
</script>

数据校验

Input 通知校验

onInput(e) {
  // ...
  // $parent指FormItem
  this.$parent.$emit('validate')
}

FormItem 监听校验通知,获取规则并执行校验

inject: ['form'], // 注入
mounted() { // 监听校验事件
  this.$on('validate',() => {this.validate()})
},
methods:{
  validate() {
    // 获取对应 FormItem 校验规则
    console.log(this.form.rules[this.prop])
    // 获取校验值
    console.log(this.form.model[this.prop])
  }
}

安装 async-validator:

npm i async-validator -S
import Schema from 'async-validator'

validate() {
  // 获取对应 FormItem 校验规则
  const rules = this.form.rules[this.prop]
  // 获取校验值
  const value = this.form.model[this.prop]
  // 校验描述对象
  const descriptor = {[this.prop]:rules}
  // 创建校验器
  const schema = new Schema(descriptor)
  // 返回 Promise,没有触发 catch 就说明验证通过
  return schema.validate({[this.prop]:value},errors=>{
    if (errors) {
      // 将错误信息显示
      this.error = errors[0].message
    } else {
      // 校验通过
      this.error = ''
    }
  })
}

表单全局验证,为 Form 提供 validate 方法

validate(cb){
  // 调用所有含有 prop 属性的子组件的 validate 方法并得到 Promise 的值
  const tasks = this.$children
      .filter(item => item.prop)
      .map(item => item.validate())
  // 所有任务必须全部成功才算校验通过,任一失败则校验失败
  Promise.all(tasks)
      .then(() => cb(true))
      .catch(() => cb(false))
}

实现弹窗组件

弹窗这类组件的特点是它们在当前 vue 实例之外独立存在,通常挂载于 body;它们是通过 JS 动态创建
的,不需要在任何组件中声明。常见使用姿势:

this.$create(Notice, {
  title: '林慕-弹窗组件'
  message: '提示信息',
  duration: 1000
}).show()

create 函数

import Vue from 'vue'

// 创建函数接收要创建组件定义
function create(Component, props) {
  // 创建一个 Vue 实例
  const vm = new Vue({
    render(h) {
      // render 函数将传入组件配置对象转换为虚拟 dom
      console.log(h(Component,{props}))
      return h(Component, {props})
    }
  }).$mount() // 执行挂载函数,但未指定挂载目标,表示只执行初始化工作

  // 将生成 dom 元素追加至 body
  document.body.appendChild(vm.$el)
  // 给组件实例添加销毁方法
  const comp = vm.$children[0]
  comp.remove = () => {
    document.body.removeChild(vm.$el)
    vm.$destroy()
  }
  return comp
}

// 暴露调用接口
export default create

另一种创建组件实例的方式: Vue.extend(Component)

通知组件

新建通知组件,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 = truw
      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 api

测试,components/form/index.vue

<script>
import create from "@/utils/create"
import Notice from "@/components/Notice"

export default {
  methods: {
    submitForm(form) {
      this.$refs[form].validate(valid => {
        const notice = create(Notice, {
          title: '林慕-create',
          message: valid ? '请求登录' : '校验失败',
          duration: 1000
        })
        notice.show()
      })
    }
  }
}
</script>

递归组件

// TODO

拓展

  1. 使用 Vue.extend 方式实现 create 方法
  • 方法一:和第一个 create 方法类似
export function create2 (Component, props) {
  let VueMessage = Vue.extend({
    render(h) {
      return h(Component, {props})
    }
  })
  let newMessage = new VueMessage()
  let vm = newMessage.$mount()
  let el = vm.$el
  document.body.appendChild(el) 
  const comp = vm.$children[0]
  comp.remove = () => {
    document.body.removeChild(vm.$el)
    vm.$destroy()
  }
  return comp
}
  • 方法二:利用 propsData 属性
export function create3 (Component, props) {
  // 组件构造函数如何获取?
  // 1. Vue.extend()
  const Ctor = Vue.extend(Component)
  // 创建组件实例
  const comp = new Ctor({ propsData: props })
  comp.$mount()
  document.body.appendChild(comp.$el)
  comp.remove = function () {
    document.body.removeChild(comp.$el)
    comp.$destroy()
  }
  return comp
}

方法三:使用插件进一步封装便于使用,create.js

import Notice from '@/components/Notice.vue'
// ...
export default {
  install(Vue) {
    Vue.prototype.$notice = function (options) {
      return create(Notice, options)
    }
  }
}
// 使用
this.$notice({title: 'xxx'})
  1. 修正 input 中 $parent 写法的问题
  • mixin emitter

  • 声明 componentName

  • dispatch()

  1. 学习 Element 源码
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,063评论 6 510
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,805评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,403评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,110评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,130评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,877评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,533评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,429评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,947评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,078评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,204评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,894评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,546评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,086评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,195评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,519评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,198评论 2 357