×
广告

vue成长之路+实战+Vue2+VueRouter2+webpack(二)vue组件入门第一节

96
言墨儿
2017.05.25 18:52* 字数 2798

推荐我的vue教程:VUE系列教程目录

上篇讲解了vue-router路由入门,现在讲讲关于vue组件的内容。
如果你们有使用过element组件的话,他就是以vue组件的形式进行封装的,在讲解组件之前我们需要知道vue是数据驱动的,它的一切依赖于数据,我们应该根据数据的不同来进行相关的处理,在这一前提下才能形成vue框架的思考模式。
在了解这一模式的前提下我们来看看vue组件是个什么东西。

什么是VUE组件?

在github上,各位请使用git拉一下项目:vuetemplate。不会使用git拉文件的请去GitHub上下载压缩包。

/src/page/components下是有关组件的代码

我们打开vue官网的组件API,可以简单浏览,对于新手来说这个API的阅读有时很晦涩,或者跟实际应用有些许差别。于是我的讲解是建立在对这个API的补充说明与简单化的,所以你们最好还是看看这个。

VUE组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,VUE组件是自定义元素, Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展。

注册组件

在注册组件时我们有两种方式,第一种注册全局组件,第二种是局部使用组件,对于那些应用多的组件来说全局无疑是最好的选择,对于变数太大,应用不多且在统一目录下的局部使用是我们想要的。

全局注册:

要注册一个全局组件,你可以使用 Vue.component(tagName, options)。

// 模板
Vue.component('my-component', {
  // 选项
})

一个模板并不能说明什么,实例才能让人看的更明白:

// html
<my-component></my-component>

// 注册
Vue.component('my-component', {
  template: '<div>A custom component!</div>'
})

组件的简单使用就是这个样子,即<my-component></my-component>最后变成了<div>A custom component!</div>。可是,实际操作中我们并不会这么弱智的使用,组件的复杂度远远不是这个样子的。

局部注册

要注册一个局部组件,你可以使用 Vue的components属性。

<script>
export default {
  components: {},
  data () {
    return {
    }
  }
}
</script>

例子才是真理:

// html
<my-component></my-component>

<script>
var vuecomponent = {
  template: '<div>A custom component!</div>'
}
export default {
  components: {
    'my-component': vuecomponent
  },
  data () {
    return {
    }
  }
}
</script>

DOM渲染的局限

在html中我们知道有些标签的孩子是固定的比如<ul> ,<ol>,<table> ,<select>限制了能被它包裹的元素,例如ul里面只能包裹li。同时,一些像 <option> 这样的元素只能出现在某些其它元素内部。

在自定义组件中使用这些受限制的元素时会导致一些问题,例如:

<table>
  <my-row>...</my-row>
</table>

自定义组件 <my-row> 被认为是无效的内容,因此在渲染的时候会导致错误。那我们怎么解决呢?变通的方案是使用特殊的 is 属性:

<table>
  <tr is="my-row"></tr>
</table>

data 必须是函数

组件中必须是函数,通过Vue构造器传入的各种选项大多数都可以在组件里用。 data 是一个例外,它必须是函数。

// html
<simple-counter></simple-counter>
<script>
export default {
  components: {
    'simple-counter': {
      template: '<el-button size="small" v-on:click="counter += 1">{{ counter }}</el-button>',
      // 技术上 data 的确是一个函数了,因此 Vue 不会警告,
      // 但是我们返回给每个组件的实例的却引用了同一个data对象
      data: function () {
        return {
            counter: 0
        }
      }
    }
  },
  data () {
    return {
    }
  }
}
</script>

这里有很多人看蒙了,问起初不是这样写吗???

data: {
    counter: 0
}

这里别问,我起初也没看懂,你可以这样想:把所有关于data的数据引入变为data () {}就可以了。
为何我会这样说?我曾经在上一篇文章里说:vue路由的本质是根据url的不同来进行组件的各种切换罢了。而组件的data必须是数据,所以你看我写的代码里关于.vue文件的都使用的是这种结构:

<template>
  <div>
  </div>
</template>
<script>
export default {
  data () {
    return {
    }
  }
}
</script>

如果不明白你可以看上面的<simple-counter>组件的例子,其最终的本质变成了:

<template>
  <div>
        <el-button size="small" v-on:click="counter += 1">{{ counter }}</el-button>
  </div>
</template>
<script>
export default {
  data () {
    return {
        counter: 0
    }
  }
}
</script>

其实你只要使用vue-router路由你的.vue文件的结构只能变成这样:

<template>
  <div>
  </div>
</template>
<script>
export default {
  data () {
    return {
        //  这里写基础数据
    }
  }
}
</script>

构成组件

组件意味着协同工作,引用组件的地方叫父组件,如<simple-counter></simple-counter>,组件的内容则被成为子组件。

通常父子组件会是这样的关系:组件 A 在它的模版中使用了组件 B 。它们之间必然需要相互通信:父组件要给子组件传递数据,子组件需要将它内部发生的事情告知给父组件。然而,在一个良好定义的接口中尽可能将父子组件解耦是很重要的。这保证了每个组件可以在相对隔离的环境中书写和理解,也大幅提高了组件的可维护性和可重用性。

在 Vue.js 中,父子组件的关系可以总结为 props down, events up 。父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息。看看它们是怎么工作的?如图:

父子组件数据交互

父子组件的交互的原理这个具体呢?

父子组件的交互

props的单向流

组件实例的作用域是孤立的。这意味着不能(也不应该)在子组件的模板内直接引用父组件的数据。要让子组件使用父组件的数据,我们需要通过子组件的props选项。

我们在开发中,一个单文件组件的.vue文件的基本构成是这样的:

<template>
  <div>
  </div>
</template>

<script>
export default {
  // el: '',
  props: {}, // 父到子传参
  components: {}, // 组件接收
  mixins: [], // 混合
  data () { // 基础数据
    return {
      //
    }
  },
  created () {}, // 创建周期
  watch: {}, // 状态过渡
  methods: {}, // 方法存放的地方
  computed: {}, // 计算属性存放的地方
  filters: {}, // 过滤
  directives: {} // 指令
}
</script>

可是常用的很少,父组件给子组件传值使用的就是props选项

props选项可以接受两种模式的参数:

第一种固定的属性:如这样message="hello!传一个普通的字符;
第二种动态属性:如这样v-bind:myMessage="this.message"传一个变量,其可以简化为:myMessage="this.message"

例子如下:(父子组件在同一目录下,子组件-child.vue)

// 父组件
// HTML
<child message="HELLO!" :my-message="this.message"></child>

// script

<script>
import child from './child.vue'
export default {
  components: {
    child: child
    }
  },
  data () {
    return {
      message: '你猜'
    }
  }
}
</script>

// 子组件
<template>
  <div>
    <div>{{message}}</div>
    <div v-text="myMessage"></div>
  </div>
</template>

<script>
export default {
  props: {
    message: null,
    myMessage: null
  }, // 父到子传参
  data () { // 基础数据
    return {
      //
    }
  },
  created () {}, // 创建周期
  watch: {}, // 状态过渡
  methods: {} // 方法存放的地方
}
</script>

prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态。

另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop 。如果你这么做了,Vue 会在控制台给出警告。

但是有时我们就是需要修改,怎么办?(老子需求)

一般情况下我们修改通常是这两种原因:

  1. prop 作为初始值传入后,子组件想把它当作局部数据来用;
  2. prop 作为初始值传入,由子组件处理成其它数据输出。

对于第一种情况我们可以李代桃僵,即用另外一个变量替代它,把它的值赋给那个变量:

data () { // 基础数据
    return {
      //
      counter: this.message
    }
  }

第二种情况可以定义一个计算属性,处理 prop 的值并返回:

computed: {
    messagetoLowerCase: function () {
      return this.message.trim().toLowerCase()
    }
}
注意:使用字面量语法传递数值时,必须使用动态props,即如这样`v-bind:number="1"`

props验证

我们可以为组件的 props 指定验证规格。如果传入的数据不符合规格,Vue 会发出警告。当组件给其他人使用时,这很有用。

要指定验证规格,需要用对象的形式,而不能用字符串数组:(修改上面的例子)

// html
<child message="HELLO!" :my-message="this.message" :number="11"></child>

// 子组件props
props: {
    message: String,
    myMessage: {
      type: String,
      required: true
    },
    number: {
      validator: function (value) {
        return value > 10
      }
    }
  }

验证规格模板:

props: {
    // 基础类型检测 (`null` 意思是任何类型都可以)
    propA: Number,
    // 多种类型
    propB: [String, Number],
    // 必传且是字符串
    propC: {
      type: String,
      required: true
    },
    // 数字,有默认值,如果你没有传则以默认为准
    propD: {
      type: Number,
      default: 100
    },
    // 数组或对象的默认值应当由一个工厂函数返回
    propE: {
      type: Object,
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }

自定义事件向父组件传值

事件$on与$emit

我们知道,父组件是使用 props 传递数据给子组件,但如果子组件要把数据传递回去,应该怎样做?那就是自定义事件!

每个 Vue 实例都实现了事件接口(Events interface),即:

使用 $on(eventName) 监听事件
使用 $emit(eventName) 触发事件

注意:Vue的事件系统分离自浏览器的EventTarget API。尽管它们的运行类似,但是$on 和 $emit 不是addEventListener 和 dispatchEvent 的别名。

你们一定很奇怪怎么用事件监听来向父元素传递?

其实原理很简单就是我们在父组件上通过v-on监听子组件的事件,而子组件通过$emit(eventName) 触发事件。例子如下:

// 父组件
<template>
  <div>
<child  v-on:onchild="inparent"></child>
</div>
</template>
<script>
import child from './child.vue'
export default {
  components: {
    child: child
  },
  data () {
    return {
    }
  },
  methods: {
    inparent () {
      alert('父组件响应了')
    }
  }
}
</script>
// 子组件
<template>
  <div>
    <el-button size="small" v-on:click="onparent">父组件响应吧!!!</el-button>
  </div>
</template>

<script>
export default {
  props: {},
  data () { // 基础数据
    return {
    }
  },
  methods: {
    onparent () {
      this.$emit('onchild')
    }
  }
}
</script>

这个例子中,子组件给父组件传值通过$emit('onchild'),触发父组件的v-on:onchildv-on:onchild响应后执行inparent函数。但是就达到我们的目的了???

传值,传值,传值,值呢?这个API里可没说,那怎么办呢?

很简单按照程序工程师的思路来想,值肯定是这种模式:

this.$emit('onchild', 需要的值)
// 多个呢?
this.$emit('onchild', 需要的值1,需要的值2)

那接值呢?

onparent (需要的值1, 需要的值2) {
}

所以完整的模式应该是这样的:

// 父组件
<template>
  <div>
<child  v-on:onchild="inparent"></child>
</div>
</template>
<script>
import child from './child.vue'
export default {
  components: {
    child: child
  },
  data () {
    return {
    }
  },
  methods: {
   inparent (childs, childrens) {
      alert('父组件响应了')
      console.log(childs)
      console.log(childrens)
    }
  }
}
</script>
// 子组件
<template>
  <div>
    <el-button size="small" v-on:click="onparent">父组件响应吧!!!</el-button>
  </div>
</template>

<script>
export default {
  props: {},
  data () { // 基础数据
    return {
        childs: '我是孩子的值',
        childrens: '我是孩子的另一个值'
    }
  },
  methods: {
    onparent () {
      this.$emit('onchild', this.childs, this.childrens)
    }
  }
}
</script>
sync-修饰符

在一些情况下,我们可能会需要对一个 prop 进行『双向绑定』。当一个子组件改变了一个 prop 的值时,这个变化也会同步到父组件中所绑定的值。这很方便,但也会导致问题,因为它破坏了『单向数据流』的假设。由于子组件改变 prop 的代码和普通的状态改动代码毫无区别,当光看子组件的代码时,你完全不知道它何时悄悄地改变了父组件的状态。这在 debug 复杂结构的应用时会带来很高的维护成本。

事实上,这正是 Vue 1.x 中的 .sync修饰符所提供的功能。但是VUE在 2.0 中移除了 .sync。后来在 2.3 VUE又重新引入了 .sync 修饰符。

// 父组件
<template>
  <div>
<child  :foo.sync="bar"></child>
</div>
</template>
<script>
import child from './child.vue'
export default {
  components: {
    child: child
  },
  data () {
    return {
          bar: 1
    }
  },
  watch: {
    bar: function () {
      console.log(this.bar)
    }
  }
}
</script>
// 子组件
<template>
  <div>
    <div>{{foo}}</div>
    <el-button size="small" v-on:click="onsync">改变foo</el-button>
  </div>
</template>

<script>
export default {
  props: {
      foo: null
  },
  data () { // 基础数据
    return {}
  },
  methods: {
      onsync () {
         this.$emit('update:foo', this.foo + 1)
    }
  }
}
</script>

其实本质上VUE做到的只是:需要做的只是让子组件改变父组件状态的代码更容易被区分。

即把<comp :foo="bar" @update:foo="val => bar = val"></comp>简写为<child :foo.sync="bar"></child>,不让使用者在父元素上进行事件监听了而已其他都是一样的,它通过子组件传值改变父组件,依赖props传值把修改的父组件元素再传回子组件而已。

小结:

父组件向子组件传值通过props;子组件向父组件传值,我们在父组件上通过v-on监听子组件的事件,而子组件通过$emit(eventName) 触发事件。

至此组件的基本知识就结束了,高深的组件有关的,下一节再说。

提示:在最近几天我会慢慢附上VUE系列教程的其他后续篇幅,后面还有精彩敬请期待,请大家关注我的专题:web前端。如有意见可以进行评论,每一条评论我都会认真对待。

日记本
Web note ad 1