1.全局注册
用法:app.component()
import {createApp} from 'vue'
const app = createApp({})
app.component(
//注册的名字
'myComponent'
//组件的实现
)
2.单文件件组件
import myComponent from './App.vue'
app.component('myComponent',myComponent)
3.app.component()方法可以被链式调用:
app
.component('ComponentA',ComponentA)
.Component('ComponentB',ComponentB)
.component('ComponentC',ComponentC)
全局注册的组件可以再此应用的任意模板中使用:
<ComponentA/>
<ComponentB/>
<ComponentC/>
全局注册
全局注册虽然很方便,但有以下几个问题:
1.全局注册,但并没有被使用的组件无法在生产打包时自动移除(也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它依然会出现在打包后的js中。
2.全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护下。
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。他们的有点是使组件之间的依赖关系更加明确。并且对tee-shaking更加友好。
//在使用scri setup>的单文件组件中,导入的组件可以直接在模板中使用,无需注册。
<template>
<ComponentA />
</template>
<script setup>
import ComponentA from './ComponentA.vue'
</script>
//在没有使用script setup ,就需要使用components选项来显示注册:
import ComponentA from '.ComponentA.vue'
export default {
components:{ComponentA},
setup(){
}
}
对于每个components对象里的属性,他们的key名就是注册的组件名,而值就是相应组件的实现。上面的例子使用的ES2015的缩写语法,等价于:
export deault {
components:{
ComponetA:ComponentA
}
}
请注意:局部注册的组件在后代组建中并不可用。在这个例子中,ComponentA注册后仅在当前组件可用,而在任何的子组件或更深层的子组件中都不可用。
组件名格式 在整个指引中,我们都使用pascalCase作为组件名的注册格式 1.pascalCase 是合法的javascript标识符,这使得在javascript中导入和注册组件都很容易,同时IDE也能提供比较的自动补全。 2.在模板中更明显地表明了这是一个Vue组件,而不是原生HTML元素。同时也能够将Vue组件和自定义元素(web components)区别开来。
在单文件组件和内联字符串模板中,我们都推荐这样做。但是,PascalCase的标签名在DOM模板中是不可用的。为了方便,Vue支持将模板中使用kebab-case的标签解析为使用PascalCase注册的组件,这意味着和以MyComponent为名注册的组件,在模板中科院通过
<MyComponent>或者<my-component>引用。这让我们能够使用同样的javaScript组件注册代码来配合不同来源的模板。
Props声明
一个组件需要显示声明它所接受的props,这样Vue才能知道外部传入的那些是props,那些是透传attribute.
在使用
<script setup>的单文件组件中,props可以使用defineProps()宏来声明
<script setoup>
const prpos=defineProps(['foo']}
console.log(props.foo)
</script>
在没有使用<script setup>的组件中,prop可以使用props选项来声明:
export default {
props:['foo']
setup(props){
console.log(props.foo)
}
}
注意传递给defineProps()的参数和提供给props选项的值是相同的,两种声明方式背后其实使用的都是prop选项。
除了使用字符串数组来声明prop外,还可以使用对象的形式:
//使用<script setup>
defineProps({
title:String,
likes:Number
})
//非<script setup>
export default {
props:{
title:String,
likes:Number
}
}
对于以对象形式声明中的每一个属性,key是prop的名称,而值则是该prop预期类型的构造函数,比如,如果要求一个prop的值是number类型,则可使用Number构造函数作为其声明的值。
对象形式的props声明不仅可以一定程度作为组件的文档,而且如果其他开发者在使用你的组件传递了错误的类型,也会在浏览器控制台中抛出警告,我们将在本章节稍后进一步,讨论有关prop校验的更多细节。
如果你正在搭配TypeScript使用<script setup>,也可以使用类型标注来声明props:
<scrpit setup>
defineprops<{
titile?:string
like?:number
}>()
</script>
props传递不同的值类型
//静态
<Nna title="do you know ?"/>
//根据一个变量的值动态传入
<Nna :title="post.title"/>
//根据一个更复杂表达式的值动态传入
<Nna :title="post.title + by + post.author.name"/>
//Number
<Nna :likes="42"/>
<Nna :likes="post.likes"/>
//Boolean
<Nav is-published />
<Nav :is-pubilished ="fasle" />
Nna :is-published="post.isPublished"/>
//Array
<Nav :comment-ids="[234,266,273]" />
<Nav :comment-ids="post.commentids"/>
//object
<Nav :author="{name:'cccc',company:'Google' }"/>
<Nav :author="post.author" />
//使用一个对象绑定多个prop
const post ={
id:1,
title:'my ly with vale'
}
<Nav v-bing="post"/>
<Nav :id="post.id" :title="post.title" />
单向数据流 所有的props都遵循这单向绑定原则,prpos因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
另外,每次父组件更新后,所有的子组件中的props都会被更新到最新值,这意味着你不应该在子组件中去更改一个prop。若你这么做了,控制台就会给你抛出警告。
const props=defineProps(['foo'])
//警告!prop是只读的!
props.foo
//导致想要更改prop的需求常来源于一下两种场景:
//1.prop被用于传入初始值;而子组件想要在之后将其作为一个局部数据属性。在这种情况下,最好是新定义一个局部数据属性,从props上获取初始值即可:
const props = defineProps(['initalCounter'])
//计算器只将props.initialCounter作为初始值
//像下面这样做就使prop和后续更新无关了。
const counter = ref(props.initalCounter)
//2.需要对传入的prop值做进一步的转换。在这种情况中,最好是居于该prop值定义一个计算属性:
const props=defineProps(['size'])
//该prop变更时计算属性也会自动更新
const normalizedSize = computed(()=>props.size.trim().toLowerCase())
更改对象、数组类型的props 当对象活数组作为props被传入时,虽然子组件无法更改props绑定,但仍然可以更改对象或者数组内部的值。这是因为javascript的对象和数组是按引用传递,而对Vue来说,禁止这样的改动的虽然可能,但有很大的性能耗损,比较得不偿失。这种更改的缺陷是他允许了子组件以某种不明显的方式来影响父组件的 状态,可能会使数据流在将来变得更难以理解,在最佳实践中,应该避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在多数情况下,子组件应该抛出一个事件来通知父组件做改变。
defineProps({
// 基础类型检查
// (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// Number 类型的默认值
propD: {
type: Number,
default: 100
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或数组的默认值
// 必须从一个工厂函数返回。
// 该函数接收组件所接收到的原始 prop 作为参数。
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
propF: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
})
//vue 会通过instanceof Person 来校验 author prop 的值是否是 Person 类的一个实例。
class Person { constructor(firstName, lastName) { this.firstName = firstName this.lastName = lastName } }
defineProps({ author: Person })
Boolean类型转换
为了更贴近原生boolean attribute的行为,声明为Boolean类型的props有特别的类型转换规则。以带有如下声明的组件为例
defineProps({
disabled:Boolean
})
//该组件可以这样使用
//等同于传入 :disabled="true"
<MyComponent disabled />
//等同于传入 :disabled="false"
<MyComponent />
//当一个prop被声明为允许多种类型时,例如
defineProps({ disabled: [Boolean, Number] })
组件事件
- (触发与监听事件) 在组件的模板表达式中,可以直接使用$emit方法触发自定义事件(例如:在v-on的处理函数中):
在模板中我们也推荐使用 kebab-case 形式来编写监听器。
组件触发的事件没有冒泡机制。你只能监听直接子组件触发的事件。平级组件或是跨越多层嵌套的组件间通信,应使用一个外部的事件总线,或是使用一个全局状态管理方案
<button @click="$emit('someEvent')">click me</button>
//父组件可以通过v-on(缩写为@)来监听事件
<MyCompoent @some-event="callback" />
//同样,组件的时间监听器也支持.once修饰符:
<MyCompoent @some-event.once="callback"
- (事件参数)
注意所有传入$emit()的额外阐述都会被直接传向监听器。举例来说。$e,et('foo',1,2,3)触发之后,监听器函数将会收到这三个参数值
//有时候我们会需要在触发事件时附带一个特定的值。举例`<BlogPost>`组件来管理文本会缩放得多大。在这个场景下,我们可以给¥emit提供一个额外的参数:
<button @click="$emit('increaseBy',1)" increase by 1 </button>
//然后我们在父组件中监听事件,我们可以先简单写一个内联的箭头函数作为监听器,此函数会接收到事件附带的参数
<MyButton @increase-by="(n) => count += n" />
//或者,也可以用一个组件方法来作为事件处理函数:
<MyButton @increase-by="increaseCount" />
//该方法也会接收到事件锁传递的参数:
function increaseCount (n){
count.value +=n
}
- (声明触发的事件)
//组件可以显式地通过defineEmits()宏来声明它要触发的事件
<script setup>
defineEmits(['inFocus', 'submit'])
</script>
//我们在<template>中使用的$emit方法不能在组件的<script setup>部分中使用,但defineEmits()会返回一个相同作用的函数供我们使用
<script setup>
const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() {
emit('submit')
}
</script>
//defineEmits()宏不能在子函数中使用。如上所示,它必须直接放置在<script setup>的顶级作用域下。
//如果你显式地使用了setup函数而不是<script setup>,则事件需要通过emits选来定义。emit函数也被暴露在setup()的上下文对象上:
export default {
emits:['inFocus','submit'],
setup(props,ctx){
ctx.emit('submit')
}
}
//这个emits选项还支持对象语法,它允许我们对触发事件的参数进行验证:
<script setup>
const emit = defineRimts({
submit(payload){
//通过fanhui 值为true 还是false来判断
//验证是否通过
}
})
//如果你正在搭配 TypeScript 使用 `<script setup>`,也可以使用纯类型标注来声明触发的事件:
<script setup lang="ts">
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
如果一个原生事件的名字 (例如 click) 被定义在 emits 选项中,则监听器只会监听组件触发的 click事件而不会再响应原生的 click 事件。
- (事件校验)
和对 props 添加类型校验的方式类似,所有触发的事件也可以使用对象形式来描述。
要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 emit 的内容,返回一个布尔值来表明事件是否合法。
<script setup>
const emit = defineEmits({
// 没有校验 click: null,
// 校验 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
})
function submitForm(email, password) {
emit('submit', { email, password })
}
</script>
组件v-model
//首先让我们回忆一下 `v-model` 在原生元素上的用法:
<input v-model="searchText" />
//在代码背后,模板编译器会对 `v-model` 进行更冗长的等价展开。因此上面的代码其实等价于下面这段:
<input
:value="searchText"
@input="searchText = $event.target.value"
/>
//而当使用在一个组件上时,v-model会被展开为如下的形式
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
//要让这个例子实际工作起来,<CustomInput>组件内部需要做两件事情:
//1.将内部原生input元素的value attribute绑定到modelValue prop
//2.当原生的input事件触发时,触发一个携带了新值的update:modelValue自定义事件
//CustomInput.vue
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"/>
</template>
//现在v-model可以在这个组件上正常工作了
<CustomInput v-model="searchText" />
//另一种在组件内实现 `v-model` 的方式是使用一个可写的,同时具有 getter 和 setter 的 `computed` 属性。`get` 方法需返回 `modelValue` prop,而 `set` 方法需触发相应的事件:
<!-- CustomInput.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const value = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
<template>
<input v-model="value" />
</template>
v-model参数
//默认情况下,`v-model` 在组件上都是使用 `modelValue` 作为 prop,并以 `update:modelValue` 作为对应的事件。我们可以通过给 `v-model` 指定一个参数来更改这些名字:
<MyComponent v-model:title="bookTitle" />
//在这个例子中,子组件应声明一个 `title` prop,并通过触发 `update:title` 事件更新父组件值:
<!-- MyComponent.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
多个c-model绑定
//利用刚才在 v-model 参数小节中学到的指定参数与事件名的技巧,我们可以在单个组件实例上创建多个 v-model 双向绑定。组件上的每一个 v-model 都会同步不同的 prop,而无需额外的选项:
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
<script setup>
defineProps({
firstName: String,
lastName: String
})
defineEmits(['update:firstName', 'update:lastName'])
</script>
<template>
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
</template>
处理v-model 修饰符
内置修饰符例如 .trim .number .lazy
自定义组件的v-model
//自定义修饰符capitalize,它会自动将v-model绑定输入的字符串第一个字母转为大写:
<MyCompnent v-model.capitalize="myText"/>
//组件的 v-model 上所添加的修饰符,可以通过 modelModifiers prop 在组件内访问到。
//在下面的组件中,我们声明了 modelModifiers 这个 prop,它的默认值是一个空对象:
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
defineEmits(['update:modelValue'])
console.log(props.modelModifiers) // { capitalize: true }
</script>
<template>
<input
type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
//注意这里组件的`modelModifiers prop 包含了 capitalize 且其值为 true,因为它在模板中的 v-model 绑定 v-model.capitalize="myText" 上被使用了。
//有了这个 prop,我们就可以检查 modelModifiers 对象的键,并编写一个处理函数来改变抛出的值。在下面的代码里,我们就是在每次 <input /> 元素触发 `input` 事件时将值的首字母大写:
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])
function emitValue(e) {
let value = e.target.value
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
<template>
<input type="text" :value="modelValue" @input="emitValue" />
</template>
//对于又有参数又有修饰符的 `v-model` 绑定,生成的 prop 名将是 `arg + "Modifiers"`。举例来说:
<MyComponent v-model:title.capitalize="myText">
//相应的声明应该是:
const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])
console.log(props.titleModifiers) // { capitalize: true }
总结: 组件+组件传值+组件事件+组件绑定 单文件绑定