「 Vue 」在 Vue 中使用组件

1,112 阅读6分钟

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

组件注册

组件是网页的一部分,一个复杂的应用可以拆分表示为组件树的形式。

image.png

const app = Vue.createApp({
    // ...
});

创建一个 Vue 实例 / 应用,接收一个参数决定根节点如何渲染。

注册组件有两种方式,全局注册局部注册

全局注册是指我们在 Vue 的应用实例上调用 component 方法注册,该组件可以在 Vue 应用中的所有组件的模板中使用,但是性能不高:

app.component('my-counter',{
    data() {
        return { count: 1 }
    },
    template:` <div @click='count += 1'>{{counter}}</div> `
})

app.component('my-counter-parent',{
    template:`
    	<counter />
    	<counter />
    `
})

组件可以被复用,组件中的数据被当前组件独享。

局部注册,定义注册之后才能使用,性能较高:

// 在 JavaScript 的对象中定义组件
const Counter = {
    // ...
}

// 使用局部组件,在 component 选项中定义想用的组件
const app = Vue.createApp({
    components: {
		'my-counter': Counter
    },
    template: `<my-counter />`
});

在字符串模板template和单文件组件中定义组件时,我们可以使用帕斯卡命名法PascalCase和短横线命名法kebab-case来命名;

由于 DOM 是大小写不敏感的,在 DOM 中只能使用短横线命名法;

由于 JavaScript 的-会被识别为减号,在 JavaScript 中只能使用帕斯卡命名法。

注意局部注册的组件在其子组件中不可用。例如,如果你希望 ComponentAComponentB 中可用,则你需要这样写:

const ComponentA = {
  /* ... */
}

const ComponentB = {
  components: {
    'component-a': ComponentA
  }
  // ...
}

组件传参

静态传参和动态传参

我们在调用组件的时候通过组件的attribute来进行传参,通过是否使用v-bind指令来区分参数是静态参数还是动态参数:

const app = Vue.createApp({
    data() {
        return { age: 100 }
    }
	template:`
		<my-component name='Evan' :age='age'>
	`
})

在组件中通过props选项来接收参数。数组形式的props

app.component('my-component',{
    props: ['name', 'age']
})

还可以使用对象形式的props来对参数进行约束:

props: {
    name: String,
    age: {
        type: Number,
        required: true,
        default: 18
    }
}

动态属性可以是数字、布尔值、数组和对象。

传入一个对象的所有 property:将一个对象的所有 property 都作为 prop 传入,使用不带参数的v-bind

post: {
  id: 1,
  title: 'My Journey with Vue'
}
<blog-post v-bind="post"></blog-post>
// 等价于
<blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>

单向数据流

父组件到子组件的参数传递是单向的,即父组件的 prop 更新会影响到子组件,反之则不行,这种现象叫做单向下行绑定。我们不希望子组件能够变更影响到父组件的状态。

所以当子组件想要使用变更后的参数时,我们可以使用 data 属性或者计算属性。

Prop 验证

我们可以对子组件接收的 prop 提出验证要求,当接收到的 prop 不满足要求时,Vue 会发出警告。

Prop 验证是在组件实例创建之前,所以此时的 data 属性不可用。

Prop 的大小写命名

由于 HTML 大小写不敏感, JavaScript 不能使用短横线命名(前文有提及)。所以 prop 在 HTML 中使用短横线命名法,在 JavaScript 中要使用驼峰命名法camelCase与之对应。

<my-component :post-title='post.title' />
props: ['postTitle']

同样的,字符串模板中不受此限制。

Non-Prop 的 Attribute

Non-Prop 就是说父组件给子组件传递属性时,子组件不通过 prop 接收的属性。这些属性将会被直接添加到子组件最外层标签的属性中。

const app = Vue.createApp({
	template:`<my-counter status='active' />`
})

app.component('my-counter',{
        // props: ['status'], 这里不通过 props 接收
	template:`<div />`
})
<!-- 最终渲染的结果 -->
<div status='active' />

一般用于styleclassid属性。

禁用 Attribute 继承

使用inheritAttrs: false禁用 attribute 继承,这适用你不希望子组件的最外层继承 attribute 的情况。

同时可以访问$attrs property ,它包括 propsemitsproperty 未包含的所有属性。

app.component('date-picker', {
  inheritAttrs: false,
  template: `
    <div class="date-picker">
      <input type="datetime-local" v-bind="$attrs" />
      <input :msg='$attrs.msg'>
    </div>
  `
})

多个根节点的组件不具有自动 attribute fallthrough (隐式贯穿) 行为。(上一篇有提及)

父子组件之间的通信

前面提到过,子组件不能直接修改父组件传递过来的 props。

我们可以通过$emit方法来向父组件传入一个事件名,父组件从而可以监听这个事件。

const app = Vue.createApp({
    data() { 
        return {count: 1 }
    },
    // template:`<counter :count='count' />`
    methods: {
        handleAddOne() {
            this.count += 1
        }
    },
    template:`<counter :count='count' @add-one='handleAddOne'/>`
})

app.component('counter',{
    props: ['count'],
    methods: {
        handleClick() { 
            // this.count += 1;
            this.$emit('addOne');
        }
    },
	template:`<div @click='handleClick'>{{count}}</div>`
        // 或者直接 template:`<div @click='$emit('addOne')'>{{count}}</div>`
})

父组件模板中的 add-one 事件不同于click等原生事件,它是自定义事件。

$emit函数可以接收更多参数:

methods: {
    handleAddOne(param1, param2) {
        this.count = this.count + param1 + param2;
    }
}
handleClick() { 
	this.$emit('add', 2, 3);
}

验证抛出的事件

与 prop 的验证类似,我们可以通过emits选项在组件中定义发出的事件。

emits: ['add', 'minus']
emits: {
    // 检验向外传递的参数的值
    add: (count1, count2) => {
        if(count1 > 0 && count2 > 0) {
            return true;
        }
        return false;
    },
    
    // 没有检验
    minus: null
}

使用v-model更新父组件参数

父子组件之间的参数传递类似v-model,我们可以通过该指令进行数据的修改。

默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。我们可以通过向 v-model 传递参数来修改这些名称:

const app = Vue.createApp({
    data() { 
        return { bookTitle: 'HowToLearnVue' }
    },
    template:`<my-component v-model:title="bookTitle"></my-component>`
})
app.component('my-component', {
    props: {
        title: String
    },
    emits: ['update:title'],
    template: `
        <input
            type="text"
            :value="title"
            @input="$emit('update:title', $event.target.value)"
        />
      `
})

可以同时使用多个v-model

<my-component 
	v-model:title="bookTitle"
	v-model:author='bookAuthor'
/>

处理 v-model 修饰符

当我们学习表单输入绑定时,我们看到 v-model 有内置修饰——.trim.number.lazy。但是,在某些情况下,我们可能还需要添加自己的自定义修饰符。这些修饰符以 modelModifiers prop 传入到子组件。

<my-component v-model.capitalize="myText"></my-component>
app.component('my-component', {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  methods: {
      emitValue(e){
          this.$emit('update:modelValue', $event.target.value)
      }
  },
  template: `
    <input type="text"
      :value="modelValue"
      @input="emitValue">
  `,
  created() {
    console.log(this.modelModifiers) // { capitalize: true }
  }
})

当组件的 created 生命周期钩子触发时,modelModifiers prop 会包含 capitalize,且其值为 true——因为 capitalize 被设置在了写为 v-model.capitalize="myText"v-model 绑定上。

现在我们已经设置了 prop,我们可以检查 modelModifiers 对象键并编写一个处理器来更改发出的值。在下面的代码中,每当 <input/> 元素触发 input 事件时,我们都将字符串大写。

methods: {
    emitValue(e){
        let value = e.target.value;
        if(this.modelModifiers.capitalize){
            value = value.charAt(0).toUpperCase() + value.slice(1)
        }
        this.$emit('update:modelValue', value)
    }
},

对于带参数的 v-model 绑定,生成的 prop 名称将为 arg + "Modifiers"

<my-component v-model:desciption.capitalize="myText"></my-component>
app.component('my-component', {
  props: ['description', 'descriptionModifiers'],
  emits: ['update:description'],
  template: `
    <input type="text"
      :value="description"
      @input="$emit('update:description', $event.target.value)">
  `,
  created() {
    console.log(this.descriptionModifiers) // { capitalize: true }
  }
})

插槽

在调用组件时,组件的起始标签和结束标签的内容可以替换组件模板中的<slot>

<todo-button>
  <i class="fas fa-plus"></i>
  Add todo
</todo-button>
<!-- todo-button 组件模板 -->
<button class="btn-primary">
  <slot></slot>
</button>
<!-- 渲染 HTML -->
<button class="btn-primary">
  <i class="fas fa-plus"></i>
  Add todo
</button>

如果组件模板中没有<slot>元素,则内容会被抛弃。

渲染作用域

父模板调用的数据属性使用的都是父模板里的数据,子模板调用的数据属性使用的都是子模板里的数据。

const app = Vue.createApp({
    data() {
        return { message: 'component1' }
    },
    template:`<my-component>{{message}}</ my-component>`
})

app.component('my-component',{
  	data() {
        return { message: 'component2' }
    },
    template:`
    <div>
    	<slot></slot>
    </div>`
})
<!-- 渲染结果 -->
<div>componet1</div>

备用内容

如果调用子组件时起始标签和结束标签之间没有内容,则可以使用<slot>的备用内容。备用内容放在<slot>标签内。

<todo-button></todo-button>
<!-- todo-button 组件模板 -->
<div>
    <slot>submit</slot>
</div>
<!-- 渲染 HTML -->
<div>submit</div>

具名插槽

有时我们需要使用多个插槽,这时候我们需要为每个<slot>添加一个name attribute 为其命名。

<div>
    <slot name='header'></slot>
    - body -
    <slot name='footer'></slot>
</div>

同时,在父组件上将提供的内容写在<template>元素上,并用v-slot指令的参数形式提供其名称。

<layout>
    <template v-slot:header>header</template>
    <template v-slot:footer>footer</template>
</layout>
<!-- 渲染 HTML -->
<div>
    header - body - footer
</div>

具名插槽有简写形式,v-slot:可以简写为#,例如上面的代码可以简写为:

<layout>
    <template #header>header</template>
    <template #footer>footer</template>
</layout>

作用域插槽

父组件可以通过作用域插槽来访问子组件中的数据。

对于子组件,我们需要把向父组件传递的数据作为一个 attribute 写到<slot>元素中。可以绑定多个属性,这些属性被称作插槽 prop。

<ul>
  <li v-for="( item, index ) in items">
    <slot :item="item" :index="index" :another-attribute="anotherAttribute"></slot>
  </li>
</ul>

父组件通过带值的v-slot来定义插槽 prop的名字。

<todo-list>
  <template v-slot="slotProps">
    <i class="fas fa-check"></i>
    <span class="green">{{ slotProps.item }}</span>
  </template>
</todo-list>

独占默认插槽的缩写语法

同时使用具名插槽和作用域插槽时,可以这样写:

<todo-list>
  <template v-slot:default="slotProps">
    <span class="green">{{ slotProps.item }}</span>
  </template>

  <template v-slot:other="otherSlotProps">
    ...
  </template>
</todo-list>

解构插槽 Prop

插槽 Prop 适用 ES6 的结构语法。可以重命名或者定义备用内容。

动态插槽名

动态指令参数也可以用在 v-slot 上,来定义动态的插槽名:

<template v-slot:[dynamicSlotName] />

Provide / Inject

provideinject解决了父组件到它的子孙组件数据的传输需要层层传递的问题。父组件使用provide选项提供数据,子组件使用inject选项接收数据即可。

const app = Vue.createApp({})

app.component('todo-list', {
  provide: {
    user: 'John Doe'
  }
})

app.component('todo-list-statistics', {
  inject: ['user']
})

要访问组件实例 property,我们需要将 provide 转换为返回对象的函数,这样更加安全。

app.component('todo-list', {
  data() {
    return {
      todos: ['Feed a cat', 'Buy tickets']
    }
  },
  provide() {
    return {
      todoLength: this.todos.length
    }
  }
})

处理响应性

provideinject传递的数据并非响应式的。

我们可以通过传递一个 ref property 或 reactive 对象给 provide 来改变这种行为。在我们的例子中,如果我们想对祖先组件中的更改做出响应,我们需要为 provide 的 todoLength 分配一个组合式 API computed property:

app.component('todo-list', {
  // ...
  provide() {
    return {
      todoLength: Vue.computed(() => this.todos.length)
    }
  }
})

app.component('todo-list-statistics', {
  inject: ['todoLength'],
  created() {
    console.log(`Injected property: ${this.todoLength.value}`)
  }
})

动态组件 & 异步组件

动态组件

我们可以在<component>元素中通过is attribute 来切换不同的组件。

const app = Vue.createApp({
    data() {
		return { currentComponent: 'input'}
    },
    methods: {
		handleClick() {
			handleClick() {
             	this.currentComponent === 'input' ?
                    this.currentComponent = 'hello' :
                    this.currentComponent = 'input'
                }
        }
    },
    template:`
    	<component :is='currentComponent' />
    	<button @click='handleClick'>change</button>
    `
})

app.component('input',{
	template:`<input />`
})

app.component('hello',{
	template:`<div>hello</div>`
})

如果我们想在切换组件时保持这些组件的状态,以避免来回切换重复渲染造成的性能问题,我们可以用<keep-alive>元素包裹这些组件,使得失活的组件被缓存下来。

<keep-alive>
	<component :is='currentComponent' />
</keep-alive>
<button @click='handleClick'>change</button>

在本例中,input 框输入的内容将会在切换之后得到缓存。

异步组件

在父组件调用子组件时,组件随即渲染,这种组件称为同步组件。

const app = Vue.createApp({
    template:`
    	<hello />
    	<async-component />
    `
})

app.component('hello',{
	template:`<div>hello</div>`
})

下面通过 Vue 中的defineAsyncComponent方法定义一个异步组件:

app.component('async-component', Vue.defineAsyncComponent(() => {
	return new Promise((resolve, reject) => {
            setTimeout(() =>{
                resolve({template:`<div>This is a async component.</div>`})  
            }, 3000)
        })
    })
)
<div>hello</div>
<!-- 3 秒后 -->
<div>This is a async component.</div>

通过异步组件,我们可以将大型项目拆分成小的 JS 文件,在需要的时候通过异步组件引入 JS 文件。

模板引用

ref 被用来给元素或子组件注册引用信息。引用信息将会被注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是那个 DOM 元素;如果用在子组件上,引用就指向组件实例:

const app = Vue.createApp({
    mounted() {
	console.log(this.$refs.count);
    	console.log(this.$refs.hello);
        this.$refs.hello.sayHello();
    },
    template:`
    	<div ref='count' />
    	<hello ref='hello'/>
    `
})

app.component('hello',{
    methods:{
        sayHello() { alert('hello')}
    },
    template:`<div>hello</div>`
})

控制台输出 DOM 元素和对子组件的引用。

应该避免访问 $refs