一、前言
之前写文件将template(模板)、style(样式)、js(数据和行为)进行一定的整合,将其变成一个.vue文件形式的单文件组件,现在了解一下他的用法。
二、.vue文件
语法块: template:用来书写标签的层级结构,style:用于标签应用的css样式,export:用于标签的以JavaScript代码为基础的交互行为。
规定: 在语法块里面,不同的人存在使用不同的语言来进行页面的构建,在vue文件中,默认使用html书写标签,css书写样式,js书写逻辑,如果你需要使用ts来编写程序,那么就需要使用lang属性来告诉webpack打包工具应该使用什么loader来解析文件
<!-- 表示使用pug语法书写标签,pug语法主要靠缩进来表示 -->
<!-- 层级结构,优点是比较简洁,缺点就是结构不清晰 -->
<template lang="pug">
div.container
h1.head hello,world
<=> 写上面这样的代码等价于下面这种结构
<div class="container">
<h1 class="head">hello,world<h1>
</div>
</template>
<!-- 表示编写ts语言的程序 -->
<script lang="ts">
export default {
}
</script >
<!-- 表示书写less格式的css样式 -->
<style lang="less">
</style>
创建文件实例: 在组件基础中提到过创建vue实例有三种方法,new关键字,vue.component的方法,以及使用components的配置项。
// 假设不在根实例下面渲染文件
import Vue from "vue"
//先导入实例化的文件
import App from "./App.vue"
const vm = new Vue({
el: "#app",
// 这样就没有使用new关键字来实例化文件了
template: "<App />",
// 注册了一个APP的局部组件,表示我也可以在当前的vm
// 实例的模板中使用App标签
components: { App }
})
如果在
import App from "./App.vue"这个地方也想忽略掉.vue的后缀名,但是能保证页面的正常渲染,那么就可以在webpack.config.js中进行如下修改,
注意: 使用这个配置的时候就不能够书写同名文件,如果非要书写同名文件就不能忽略后缀名了
规范: 文件为JS的时候首字母小写,文件为组件的时候首字母大写
// webpack.config.js
module.exports = {
resolve: {
// 如果文件名后缀不写,它会自动在文件名后面自动添加extensions里面的
// 内容,从第一个开始添加去渲染,如果渲染不出来就下一个,到最后一个
// 如果还渲染不出来就会报错
extensions: ['.js', '.vue']
}
}
三、根组件、父组件和子组件
根组件: 没有父组件的组件就是根组件,所以根组件一定是通过new关键字产生,因为一个项目中new关键字只能使用一次。
父组件: 子组件的标签会出现在父组件的模板之中,如果存在这种嵌套关系,那么标签对应组件就是模板对应组件的子组件,模板对应的组件就是标签对应的父组件,同时,一个子组件只能有一个父组件,但是一个父组件可以拥有多个子组件
父组件的获取:vm.$paren子组件的获取:vm.$children根实例的获取:vm.$root
<template>
<div>
<!-- 组件在注册的时候首字母大写,这样的好处在于避免 -->
<!-- 使用同名标签以及让别人可以一眼看出这是自定义的 -->
<!-- 组件(开发者的约定)-->
<Button></Button>
</div>
</template>
<script>
import Button from "./Button"
export default {
el: "#app",
components: { Button }
mounted() {
// 查看根组件
console.log(this.$root)
// 查看子组件
console.log(this.$children)
// 查看父组件
console.log(this.$parent)
}
}
</script>
四、插槽
问题引入:
<!-- Button文件中 -->
<template>
<button class="el-button el-button--successful">
成功
</button>
<template>
<!-- 根据element-ui的文档,我每使用一次<Button></Button>, -->
<!-- 就会创建一个文字为成功,颜色为绿色的按钮,但是如果我 -->
<!-- 写成<Button>111</Button>的时候,文字111并不会展示出来, -->
<!-- 因为这个组件的内容我在Button组件文件中已经规定死了, -->
<!-- 那么我该如何Button组件的内容呢。 -->
残次解决: 在使用Button组件的时候传递一个attr属性
<!-- 使用Button组件 -->
<Button label="张三"></Button>
// Button组件文件中
// 在创建Button组件的时候获取这个attr属性,并将其作为组件
// 的内容传入
<template>
<button class="el-button el-button--successful">
{{$attr.label}}
</button>
<template>
缺点:
- 用法比较复杂
- 不符合button按钮使用规范
相对完美解决:当给组件传递内容后,内容会被传递到vm.$slots.default[0]中
// 使用Button组件
<Button slot="default">张三</Button>
// Button组件文件中
// slot起到占位符的作用,它可以将外界给这个标签传递
// 的所有的标签、内容都会渲染到这个位置(内置组件),
// 插槽的名称使用name属性指定
<template>
<button class="el-button el-button--successful">
<solt name="default"/>
<!-- 插槽默认值:当不传递内容的时候,会默认渲染aaaa的文本内容 -->
<solt> aaaa </solt>
</button>
<template>
注意: 只有组件的slot属性与插槽的name属性的名字相同,插槽才会和组件产生对应关系,一旦产生对应关系,向组件传递的内容才会被slot接住并渲染,也可以不写,那么其默认值是default,如果插槽不写名字,那么这个插槽就成为默认插槽
作用: 可以将组件标签的内容渲染在组件内部的指定位置(上面的文本内容张三就属于组件标签的内容),插槽如果多次使用,那就会进行多次渲染。
插槽的简易实现:
// 从上面的例子中可以得出,插槽的使用都是作为子组件,然后在父组件中的内容
// 都会默认被收集到父组件的$slots属性中,如果没有给插槽名字,那么就会收集
// 在vm.$slots.default里面,那么也就是说,不管我写不写官方文档上面的slot
// 标签,父组件的内容都会被收集到那个地方,那这样看来,插槽的作用主要是将
// 父组件的$slots属性收集到的内容渲染出来就可以
const Slot = {
name: 'co-slot',
// 插槽是有名字的,就是上面写的name的props的属性,它的默认值是default
props: {
name: {
type: String,
default: "default"
}
}
// 收集的内容它不是真实的节点,它是虚拟的节点,在jsx语法的那篇文章中
// 分享过,如果需要将虚拟节点渲染出来,需要使用render函数,对于这个
// 函数,它返回什么就会渲染什么。
render() {
// 拿到父组件的slots里面的内容进行渲染
return this.$parent.$slots[this.name]
// 在渲染的这个组件里面,如果将它打印出来,会发现里面有一个data的
// 选项,这个选项就是render函数的第二个参数,然后可以通过这个data
// 选项给渲染出来的组件添加事件、属性、指令等等
}
}
// 为了插槽方便使用use注册安装一个install的方法
export default {
install(v) {
v.component(Slot.name, Slot)
}
}
五、作用域插槽
作用: 借助slot标签,将组件内部的数据传递到组件外部。
<template>
<div>
<Foo>
<!-- 2.在组件的内部写一个template,在template上用v-slot -->
<!-- 绑定一个属性,这个属性就是插槽的名字,如果插槽没有 -->
<!-- 名字,就是默认插槽default,后面绑定的值可以拿到给 -->
<!-- slot标签绑定的属性(会拿到一个对象),如果需要使用 -->
<!-- 的话,点出来就好了 -->
<template v-slot:default="t">
{{t.msg}}
</template>
</Foo>
</div>
</template>
<script>
export default {
name: "library"
components: {
Foo: {
data() {
return {
msg: "我是foo组件内部的数据"
}
},
template: `
<div>
<!-- 1.当给slot绑定一个属性的时候,这个属性就可以 -->
<!-- 被外加访问到了 -->
<slot :msg="msg"></slot>
<h1>111</h1>
</div>
`
}
}
}
</script>
六、组件的event
// Button组件内部
// 组件外部传递之后,可以在组件内部使用
// vm.$listeners.事件名获取到这个函数体
// 如果传入的点击事件存在,便将点击事件执行一下
this.$listeners.click && this.$listeners.click()
// 上面的一行代码可以简化为下面这样,他们是等价关系
this.$emit('click')
// 使用Button组件
<template>
<div>
<!-- 在Button组件外部添加一个事件click, -->
<!-- 它制造了一种它绑定了一个事件的 -->
<!-- 假象,但是其核心是将log函数传递到组件内部。 -->
<Button @click.native="log"></Button>
</div>
</template>
<script>
import Button from "./Button"
export default {
el: "#app",
components: { Button },
methods: {
log() {
console.log(111)
}
}
}
</script>
注意: 在组件绑定事件的时候加上.native的修饰符会自动将事件继承给组件的根标签
七、$emit('事件名')的实现
// 既然是等价关系,那么至少$emit函数内部的this应该指向vm实例,
// 否则他无法调用$listeners里面的方法
var vm = {
$listeners: {
// 根据事件event里面的代码可以了解到,我在使用Button的这个组件的
// 时候在Button组件外部绑定了一个点击事件,事件的回调函数为log,当我
// 绑定成功的时候,我在Button组件内部就可以使用this.$listeners.事件名
// 拿到这个回调函数log,由父子组件的概念可以了解到,Button组件外部的
// 那个组件是父组件,Button组件便是子组件了,那么可以得到一下结论,这
// 个log函数属于父组件的数据内容。
click: function(arg) {
console.log('事件触发了')
console.log(arg)
}
},
// 在$emit函数中,它存在两个参数,type为事件类型,payload为传递参数
$emit(type, payload) {
// 如果它的$listeners有这个type的属性是一个函数,也就表示它是一个事件。
if(typeof this.$listeners[type] === 'function') {
// 那么就执行事件对应的回调函数并且将函数内部的this指向vm的实例,
// 在函数执行的同时,也可以向回调函数传递参数,因为$emit函数执行
// 在Button子组件里面,所以回调函数需要的参数来源于子组件,而子
// 组件可以将数据绑定的方式传递给回调函数,这样就 完成了子组件向
// 父组件传递的过程,也就是子向父的通信
this.$listeners[type].call(this,payload)
}
}
}
八、组件的双向数据绑定
// 父组件
// 1.双向数据绑定实现默认需要绑定一个value
// 的属性和一个input的事件,用v-model实现
<Input v-model="msg" />
// 2.但是对于组件来说,他没有事件对象,所以并不能用
// e.target.value,他只能接受子组件传递过来的数据,
// 然后将这个数据来修改绑定的value的属性对应的msg的值
<Input :value="msg" @input="(arg) => (msg = arg)" />
export default {
data() {
return {
// 6.此时父组件的input事件对应的回调函数
// 会接收到子组件传递的数据作为参数,
// 最后将拿到的参数将其赋值给绑定的msg数据,
// 从而完成数据的更新和绑定。
msg: ""
}
}
}
// 子组件
// 4.给子组件绑定一个跟父组件对应的事件用于触发来传递参数
<input @input="trigger"/>
export default {
// 3.上面在组件上面绑定了一个props的value的属性,
// 所以在子组件需要定义这个属性
props: {
value: {}
},
methods: {
// 5.在数据传递发生变化的时候通过子向父通信
// 传递的$emit的方法将改变后的数据传递给父组件
trigger(e) {
this.$emit('input', e.target.value)
}
}
}