注册组件时必须给组件命名。组件名的命名规则和组件要使用在哪里有关,如果只是要混在 DOM 中使用组件,那名字应该全部小写,多个单词用连字符连接(即kebab-case),这样可以避免与 HTML元素发生冲突。如果在字符串模板或单文件组件中定义组件,那么既可以用 kebab-case,也可以用 PascalCase (首字母大写的驼峰式)。当使用后者时,使用组件时,即可以 PascalCase,也可以 kebab-case,但混入 DOM 使用仍然必须 kebab-case。
我们前面用的都是全局注册,即使用 app.component() 函数。和变量的情形一样,一旦全局注册,那么它可以在 app 的模板中使用,也可以在后续组件的模板中使用。
和全局注册相对的是局部注册,即在应用 app 的配置项 components 或者组件的配置项 components 中进行声明(每个组件包含一个组件名作为键,值则是该组件的配置),然后在app范围内或者(父)组件范围内使用。局部注册可以使得构建工具编译时跳过那些并未被实际使用的组件。
const ComponentA = {
/* ... */
}
const ComponentB = {
/* ... */
}
const ComponentC = {
/* ... */
}
const app = Vue.createApp({
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
const ComponentA = {
/* ... */
}
const ComponentB = {
components: {
'component-a': ComponentA
}
// ...
}
当我们使用 Babel 和 webpack 那样的构建工具,按 ES2015+模块方式使用则如下
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
}
// ...
}
组件文件可以是 ComponentA.vue 或 ComponentA.js,导入时,可以不带后缀,即
import ComponentA from './ComponentA'
尽管一般使用构建工具来使用组件,但我们的确可以直接按Javascript模块来使用组件。下面的例子纯粹为了演示,我们把前面的 todo-list 例子改一改。
先编写模块 TodoItem.mjs (模块概念可以参考 JavaScript modules 模块 - JavaScript | MDN,不使用Vue编译构建工具,我们只能使用JS内的字符串模板),TodoItem模块导出 TodoItem对象
const TodoItem = {
template: `
{
{ title }}
`,
props: ['title'],
emits: ['remove']
}
export { TodoItem }
然后编写 TodoList.mjs 模块,该模块从 TodoItem.mjs模块 导入 TodoItem对象,同时自身又导出 TodoList 对象。TodoList 组件内部局部注册了 TodoItem组件。
import { TodoItem } from "./TodoItem.mjs"
const TodoList = {
components: {
TodoItem
},
data() {
return {
newTodoText: '',
todos: [
{ id:1, text: 'Learn JavaScript' },
{ id: 2, text: 'Learn Vue' },
{ id: 3, text: 'Build something awesome' }
],
nextTodoId: 4
}
},
methods: {
addNewTodo() {
this.todos.push({
id: this.nextTodoId++,
text: this.newTodoText
})
this.newTodoText = ''
}
}
}
export { TodoList }
最后,编写 main.mjs 模块,它引入 TodoList,并作为最后给 HTML 使用的模块
import { TodoList } from './TodoList.mjs'
Vue.createApp(TodoList).mount('#list-rendering')
然后,在这些文件所在目录,启动 Web服务监听某端口 (例如用php命令 php -S 0.0.0.0:8000),再在浏览器访问 http://localhost:8000/test.html 即可。(不能使用本地加载Html文件的方式来访问,否则会遇到 CORS错误,这是Javascript模块的安全性限制)
我们前面是以字符串数组形式列出 prop,从而这些属性的取值类型是没有明确限定的。我们可以以对象形式列出 prop,对象 property的名称和值分别对应 prop的名称和类型:
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // 或任何其他构造函数
}
使用组件时,可以给 prop传入静态的值(字符串),如
更常见的,我们会为 prop 动态绑定变量的值或者表达式的值
当传入数字时,我们需要用动态绑定,以明确不是字符串,而是表达式
传入布尔值,同样如此
传入数组
传入对象
如果要把一个对象的 property 都作为 prop 传入,即对于以下对象
post: {
id: 1,
title: 'My Journey with Vue'
}
要实现下述绑定效果
可以有更简单的绑定语法(v-bind = "对象名",对象的每个 property 自动绑定到每个 prop)
Vue组件 prop 的数据流向是单向的,即从父组件向子组件单向下行绑定:父级 prop 的更新会流动到子组件,导致子组件 prop 刷新为最新的值。用户不应该在子组件内部改变 prop。可能让用户想在子组件内部改变 prop 值的情形有:
1、想用 prop 传递一个初始值,之后子组件想把它作为一个本地的 prop 数据来使用。例如,一个计算器变量 counter,希望父组件传递一个初始值。这种情形下,不应该把 counter 定义为 prop,应该额外定义一个 initialCounter 作为 prop,counter 作为组件的数据属性变量,并初始化为 initialCounter的值,即
props: ['initialCounter'],
data() {
return {
counter: this.initialCounter
}
}
2、想用 prop 传递一个值,但这个值需要经过转换才适合子组件使用。这种情况下,最好定义一个计算属性来转换 prop 的值,例如
props: ['size'],
computed: {
normalizedSize() {
return this.size.trim().toLowerCase()
}
}
说明:在 Javascript 中,数组和对象是通过引用传递的,所以,对于 数组或者对象类型的 prop,如果在子组件中改变这个对象或数组本身将会影响到父组件的状态,这一点也足以说明 prop 单向数据流的必要性。
因为子组件可能是被他人使用的,所以,对 prop 进行相关的类型和数据验证就是必要的(对子组件开发者来说其实也必要)。
app.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() {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator(value) {
// 这个值必须与下列字符串中的其中一个相匹配
return ['success', 'warning', 'danger'].includes(value)
}
},
// 具有默认值的函数
propG: {
type: Function,
// 与对象或数组的默认值不同,这不是一个工厂函数——这是一个用作默认值的函数
default() {
return 'Default function'
}
}
}
})
prop 验证发生在组件实例创建之前,因此,default() 函数 或 validator() 函数中无法使用实例的 property (data、computed等)
验证中的类型检查,type 除了可以是原生构造函数 String、Number、Boolean、Array、Object、Date、Function、Symbol 之一,还可以是自定义构造函数,通过 instanceof 来进行检查确认。例如,给定如下构造函数
function Person(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
就可以使用
app.component('blog-post', {
props: {
author: Person
}
})
来验证 author 这个 prop 的值是否是通过 new Person 创建的(实例)。
prop 的大小写命名规则,同样遵循 HTML 中使用 kebab-case 和 JS 中使用 camelCase (字符串模板则没有限制): JS中 prop postTitle 对应 HTML 中 attribute post-title
组件作为自定义元素,我们除了添加在 props 或 emits 有对应定义的 attribute,还可以添加非 prop 的 attribute,常见的非 prop attribute 有 class、style、id。组件内部可以通过实例 property $attrs 来访问到这些 attributes。
Attribute 继承
当组件返回的是单个根节点时,非 prop 的 attribute 将自动添加到根节点的 attribute 中。例如,日期选择组件 date-picker 返回单个根节点
app.component('date-picker', {
template: `
`
})
如果我们用 data-status attribute 来表示
data-status attribute 会合并到根节点 div.data-picker,从而渲染为
对于事件监听器,也有同样的规则。我们下面的例子来演示非prop attribute的合并情况和父组件向子组件传递初始值的情况:
对于一个HTML根元素本身具有 change事件的组件来说,给它附加非prop的事件监听器change是有意义的,因为该事件监听器会从父组件传递到子组件,我们不需要显式从 date-picker 用代码去引发事件,因为子组件可以自动触发事件。
禁用 Attribute 继承
和继承相反,如果不希望组件的根元素继承 attribute,可以在组件选项中设置 inheritAttrs: false,这样组件就不会进行自动合并的操作了。禁用 attribute 自动继承的常见场景是希望将 attribute 应用到异于根元素的其他元素。
app.component('date-picker', {
inheritAttrs: false,
template: `
`
})
上面的代码,会把外部 attribute 绑定到组件内的 input 上,即把
渲染为
多个根节点的 Attribute 继承
Vue 没有规定多个根节点如何自动实现 attribute 继承,所以,多个根节点时,必须类似禁用Attribute那样手动绑定来实现继承(对某个元素 v-bind="$attrs"),不然会出现运行时警告。
app.component('custom-layout', {
template: `
...
...
`
})