Vue 学习笔记 - 深入了解组件

Vue 学习笔记 - 深入了解组件

  • 组件注册
    • 组件名
      • 组件名大小写
    • 全局注册
    • 局部注册
    • 模块系统
      • 在模块系统中局部注册
      • 基础组件的自动化全局注册
  • Prop
    • Prop 的大小写 (camelCase vs kebab-case)
    • Prop 类型
    • 传递静态或动态 Prop
    • 单向数据流
      • 两个想编辑`prop` 的特殊场景:
    • Prop 验证
      • 类型检查
    • 非 Prop 的 Attribute
      • 替换/合并已有的 Attribute
      • 禁用 Attribute 继承
  • 自定义事件
    • 事件名
    • 自定义组件的 v-model
    • 将原生事件绑定到组件
    • .sync 修饰符 `(2.3.0+ 新增)`
  • 插槽
    • 插槽内容
    • 编译作用域
    • 后备内容
    • 具名插槽
    • 作用域插槽
      • 独占默认插槽的缩写语法
      • 解构插槽 Prop
    • 动态插槽名 `2.6.0 新增`
    • 具名插槽的缩写 `2.6.0 新增`
    • 其它示例
    • 废弃了的语法
      • 带有 `slot` attribute 的具名插槽
      • 带有 `slot-scope` attribute 的作用域插槽
  • 动态组件 & 异步组件
    • 在动态组件上使用 `keep-alive`
    • 异步组件
      • 处理加载状态 `2.3.0+ 新增`
  • 处理边界情况
    • 直接看[**官方教程**](https://cn.vuejs.org/v2/guide/components-edge-cases.html)吧
  • 参考资料

组件注册

组件名

Vue.component('my-component-name', { /* ... */ })

该组件名就是 Vue.component 的第一个参数。
当直接在 DOM 中使用一个组件 (而不是在字符串模板单文件组件) 的时候,官方强烈推荐遵循W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突。
你可以在风格指南中查阅到关于组件名的其它建议。

组件名大小写

命名方式 定义 使用
kebab-case 短横线分隔 Vue.component(’my-component-name’, { /* … */ }) <my-component-name>
PascalCase首字母大写 Vue.component(’MyComponentName’, { /* … */ }) <my-component-name> 和 <MyComponentName>

注意直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的

全局注册

注册之后新创建的任何根实例都可以用。

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

局部注册

var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }

new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})
  1. 实例初始化时创建,只在实例内有效。
  2. components 中每个propertykey是组件名value是组件对象
  3. 局部注册的组件在其子组件中不可用。如果想 ComponentAComponentB 中可用,则需要这样写:
var ComponentA = { /* ... */ }

var ComponentB = {
  components: {
    'ComponentA': ComponentA // 可以直接简写为:ComponentA。组件名字和值都是 ComponentA
  },
  // ...
}

模块系统

在模块系统中局部注册

  • src\components下为每个组件创建一个目录(同组件名)
  • 然后你需要在局部注册之前导入每个你想使用的组件。例如,在一个假设的ComponentB.jsComponentB.vue 文件中:
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

export default {
  components: {
    ComponentA,
    ComponentC
  },
  // ...
}

现在 ComponentAComponentC 都可以在 ComponentB 的模板中使用了。

基础组件的自动化全局注册

如果有很多基本的通用组件,如上的一个个注册,太复杂。
如果你恰好使用了webpackVue CLI 3+(在内部使用了 webpack),那么就可以使用 require.context 对想要的组件进行全局注册。

  • 全局注册要在new Vue 实例之前才有意义。
  • 基本流程就是:
    1. 通过require.context获取指定文件夹下所有文件,可以正则过滤文件名,可以控制是否查询其子目录
    2. 遍历文件列表。拼出组件名字,逐个调用Vue.component进行注册。
    3. 例子代码如下:
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'

const requireComponent = require.context(
  // 其组件目录的相对路径
  './components',
  // 是否查询其子目录
  false,
  // 匹配基础组件文件名的正则表达式
  /Base[A-Z]\w+\.(vue|js)$/
)

requireComponent.keys().forEach(fileName => {
  // 获取组件配置
  const componentConfig = requireComponent(fileName)

  // 获取组件的 PascalCase 命名
  const componentName = upperFirst( //首字大写
    camelCase( // 转为驼峰
      fileName
        .split('/') //把文件路径拆分后最后一个就是文件名
        .pop() // 弹出最后一个
        .replace(/\.\w+$/, '') // 将后缀名去掉
    )
  )

  // 全局注册组件
  Vue.component(
    componentName,
    // 如果这个组件选项是通过 `export default` 导出的,
    // 那么就会优先使用 `.default`,
    // 否则回退到使用模块的根。
    componentConfig.default || componentConfig
  )
})

Prop

Prop 的大小写 (camelCase vs kebab-case)

prop中用驼峰,标签属性还是得写成分隔线

Vue.component('blog-post', {
  // 在 JavaScript 中是 camelCase 的
  props: ['postTitle'],
  template: '

{{ postTitle }}

'
})
<blog-post post-title="hello!"></blog-post>

重申一次,如果你使用字符串模板,那么这个限制就不存在了。(这名我还没理解)

Prop 类型

数组形式默认值为字符串

props: ['title', 'likes', 'isPublished', 'commentIds', 'author']

想定义类型用对象:

props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // or any other constructor
}

你会在这个页面接下来的部分看到类型检查和其它 prop 验证。

传递静态或动态 Prop

前面已经学过:

传值方式 写法 备注
静态字符串 title="三国演义"> 直接使用 title属性,默认字符串
变量 v-bind:title="book.title"> 使用了v-bind:指令
此时值就跟着变量走了
表达式 v-bind:title="'《' + book.title + '》'"> 同上
对象 v-bind:mybook="bookOjb"> 同上
数字 v-bind:page="500"> 这里 500没有引号,是数字
布尔值 v-bind:hardback> 不赋值,存在就默认为true
赋值时当变量看就好了
数组 v-bind:characters="['大哥','二第','三弟]"> 支持字面量,也支持变量
对象 v-bind:character="{name:'刘备'}"> 支持字面量,也支持变量

单向数据流

  1. 所有的 prop 只从父组件 =》子组件。父组件中的值更新,子也更新。
  2. 子组件不应该修改prop(逻辑上它表明你想这样 子组件=》父组件但这是禁止的,你会收到错误提示)
  3. 数组对象是引用传递的,对它们内容的修父子之间是同步的。
  4. 子组件想父级数据,就触发事件,让父级监听来处理吧。详见:sync修饰符

两个想编辑prop 的特殊场景:

  1. 初始值,这个应该在子组件中用一个变量来接收,再使用,不应该直接编辑prop
  2. 父传进来的值需要格式化显示,此时推荐使用计算属性computed

Prop 验证

自己编写的组件给别人用时,需要明确prop的一些验证规则,可以用对象形式定义:

Vue.component('my-component', {
  props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    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 ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }
})

注意顺序:验证实例化。 (如 data、computed 等 property ) 在 default 或 validator 函数中是不可用的。

类型检查

type 可以是下列原生构造函数中的一个:
String、 Number、 Boolean、 Array、 Object、 Date、 Function、 Symbol
另外还支持自定义的构造函数,如下验证author是否通过new Person创建的

function Person (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}
Vue.component('blog-post', {
  props: {
    author: Person
  }
})

非 Prop 的 Attribute

  • 组件可以接受任意的attribute
  • 如果组件显示的定义对应的prop,那就它就有着落了。
  • 如果如果组件没有显示的定义对应prop,那么此attribute就会被添加到组件的根元素上。

替换/合并已有的 Attribute

绝大多数 attribute 会用外部提供给组件的值替换掉组件内部设置好的值。
所以如果传入 type="text" 就会替换掉 type="date" 并把它破坏!
幸好classstyle稍微智能一些,会把两部分值合并起来。

禁用 Attribute 继承

设置 inheritAttrs: false 可以在组件中禁用 Attribute 继承(不会影响 styleclass

Vue.component('my-component', {
  inheritAttrs: false,
  // ...
})

inheritAttrs: false 和 $attrs 配合的例子没看懂

自定义事件

事件名

触发:this.$emit('my-event')
监听:v-on:my-event="clickHandler"
事件名会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 v-on:myEvent 将会变成 v-on:myevent
因此,官方推荐始终使用 kebab-case 的事件名,如:my-event

自定义组件的 v-model

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的。model 选项可以用来避免这样的冲突:
2.2.0+ 新增的model

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    
  `
})
<base-checkbox v-model="lovingVue">base-checkbox>

我们定义了一个叫checkedprop用来接收v-modellovingVue值。当 触发 change事件并附带一个新的值的时候,这个 lovingVue将会更新。

将原生事件绑定到组件

直接在自定义组件上监听事件要在事件后.native,此事子组件中的input元素的focus事件能触发

<div id="app">
	<base-input v-on:focus.native="onFocus"></base-input>
</div>

<script>
	Vue.component('base-input', {
		data: function(){
			return {label : '标签:',value: '值123'}
		},
		template: ``
	})
	new Vue({
		el: "#app",
		methods:{
			onFocus: function($event){console.info("获得焦点");}
		}
	});
</script>

但如果子组件中根元素不是input元素。那么.native也没用了。
为了解决这个问题,Vue 提供了一个 $listeners property,它是一个对象,包含了作用在这个组件上的所有监听器。

<div id="app">
	<base-input v-on:focus="onFocus" v-model="myValue"></base-input> {{myValue}}
</div>

<script>
	Vue.component('base-input', {
	  inheritAttrs: false,
	  props: ['label', 'value'],
	  computed: {
	    inputListeners: function () {
	      var vm = this
	      // `Object.assign` 将所有的对象合并为一个新对象
	      return Object.assign({},
	        // 我们从父级添加所有的监听器
	        this.$listeners,
	        // 然后我们添加自定义监听器,
			// 或者覆写一些监听器的行为
	        {
	          // 这里确保组件配合 `v-model` 的工作
	          input: function (event) {
	            vm.$emit('input', event.target.value)
	          }
	        }
	      )
	    }
	  },
	  template: `
	    
	  `
	})
	
	new Vue({
		el: "#app",
		data: {myValue : "123"},
		methods:{
			onFocus: function($event){console.info("获得焦点");}
		}
	});
</script>

现在 组件完全透明了,组件内部所有事件都传出来了。
并且重写了input事件,配合v-model="myValue"使用。

.sync 修饰符 (2.3.0+ 新增)

子组件想父级数据,就触发事件,让父级监听来处理。 .sync 修饰符就是这种操作的缩写形式。
注意:

  1. 带有 .sync 修饰符的v-bind 不能和表达式一起使用 (例如 v-bind:title.sync="doc.title + '!'" 是无效的)。取而代之的是,你只能提供你想要绑定的 property 名,类似 v-model
  2. 用一个对象同时设置多个propv-bind.sync="doc"会把 doc 对象中的每个property (如 title) 都作为一个独立的prop传进去,然后各自添加用于更新的v-on监听器。
  3. v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync="{ title: doc.title }"无法正常工作。

插槽

插槽内容

2.6.0中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slotslot-scope 这两个目前已被废弃但未被移除且仍在文档中的 attribute。新语法的由来可查阅这份 RFC

它允许你像这样合成组件:

<navigation-link url="/profile">
  Your Profile
</navigation-link>

然后你在 的模板中可能会写为:

<a> <slot></slot> </a>
  • 当组件渲染的时候将会被替换为“Your Profile”
  • 插槽内可以包含任何模板代码,包括 HTML,甚至其它的组件
  • 如果没有包含一个元素,则该组件两个标签之间的内容会被抛弃。

编译作用域

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

<navigation-link url="/profile">
  {{ url }}  // 这里其实是读取不取 navigation-link 属性 url的。因为这句会先编译好,再传进去替换插槽。
navigation-link>

后备内容

就是默认内容 如果没有传值进来就会使用它。

具名插槽

2.6.0起有所更新。已废弃的使用 slot attribute 的语法在这里