vue3学习随便记9-深入组件(注册、prop/非prop attribute)

深入组件

组件注册

注册组件时必须给组件命名。组件名的命名规则和组件要使用在哪里有关,如果只是要混在 DOM 中使用组件,那名字应该全部小写,多个单词用连字符连接(即kebab-case),这样可以避免与 HTML元素发生冲突。如果在字符串模板或单文件组件中定义组件,那么既可以用 kebab-case,也可以用 PascalCase (首字母大写的驼峰式)。当使用后者(用字符串模板或单个vue文件定义组件)时,使用组件(实例)时,既可以 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.vueComponentA.js,导入时,可以不带后缀,即

import ComponentA from './ComponentA'

尽管一般使用构建工具来使用组件,但我们的确可以直接按Javascript模块来使用组件。下面的例子纯粹为了演示,我们把前面的 todo-list 例子改一改。

先编写模块 TodoItem.mjs (模块概念可以参考 JavaScript modules 模块 - JavaScript | MDN,不使用Vue编译构建工具,我们只能使用JS内的字符串模板),TodoItem模块导出 TodoItem对象

const TodoItem = {
    template: `
        <li>
            {{ title }}
            <button @click="$emit('remove')">移除</button>
        </li>
    `,
    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')
<html>

<head>
    <script src="vue.global.js"></script>
</head>

<body>
    <div id="list-rendering">
        <form v-on:submit.prevent="addNewTodo">
            <label for="new-todo">添加待办事项</label>
            <input v-model="newTodoText" id="new-todo" placeholder="例如: 收拾房间" />
            <button>添加</button>
        </form>
        <ol>
            <todo-item v-for="todo in todos" :key="todo.id"
                :title="todo.text" @remove="todos.splice(index, 1)"
            ></todo-item>
        </ol>
    </div>
</body>
<script type="module" src="./main.mjs"></script>

</html>

然后,在这些文件所在目录,启动 Web服务监听某端口 (例如用php命令  php  -S  0.0.0.0:8000),再在浏览器访问 http://localhost:8000/test.html 即可。(不能使用本地加载Html文件的方式来访问,否则会遇到 CORS错误,这是Javascript模块的安全性限制)

Props

我们前面是以字符串数组形式列出 prop,从而这些属性的取值类型是没有明确限定的。我们可以以对象形式列出 prop,对象 property的名称和值分别对应 prop的名称和类型:

props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // 或任何其他构造函数
}

使用组件时,可以给 prop传入静态的值(字符串),如

<blog-post title="My journey with Vue"></blog-post>

更常见的,我们会为 prop 动态绑定变量的值或者表达式的值

<!-- 动态赋予一个变量的值 -->
<blog-post :title="post.title"></blog-post>

<!-- 动态赋予一个复杂表达式的值 -->
<blog-post :title="post.title + ' by ' + post.author.name"></blog-post>

当传入数字时,我们需要用动态绑定,以明确不是字符串,而是表达式

<!-- 即便 `42` 是静态的,我们仍需通过 `v-bind` 来告诉 Vue     -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。             -->
<blog-post :likes="42"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post :likes="post.likes"></blog-post>

传入布尔值,同样如此

<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。          -->
<!-- 如果没有在 props 中把 is-published 的类型设置为 Boolean,
则这里的值为空字符串,而不是“true”。 -->
<blog-post is-published></blog-post>

<!-- 即便 `false` 是静态的,我们仍需通过 `v-bind` 来告诉 Vue  -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。             -->
<blog-post :is-published="false"></blog-post>

<!-- 用一个变量进行动态赋值。                                -->
<blog-post :is-published="post.isPublished"></blog-post>

传入数组

<!-- 即便数组是静态的,我们仍需通过 `v-bind` 来告诉 Vue        -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。             -->
<blog-post :comment-ids="[234, 266, 273]"></blog-post>

<!-- 用一个变量进行动态赋值。                                -->
<blog-post :comment-ids="post.commentIds"></blog-post>

传入对象

<!-- 即便对象是静态的,我们仍需通过 `v-bind` 来告诉 Vue        -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。             -->
<blog-post
  :author="{
    name: 'Veronica',
    company: 'Veridian Dynamics'
  }"
></blog-post>

<!-- 用一个变量进行动态赋值。                                 -->
<blog-post :author="post.author"></blog-post>

如果要把一个对象的 property 都作为 prop 传入,即对于以下对象

post: {
  id: 1,
  title: 'My Journey with Vue'
}

要实现下述绑定效果

<blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>

可以有更简单的绑定语法(v-bind = "对象名",对象的每个 property 自动绑定到每个 prop)

<blog-post v-bind="post"></blog-post>

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

非 prop 的 Attribute

组件作为自定义元素,我们除了添加在 props 或 emits 有对应定义的 attribute,还可以添加非 prop 的 attribute,常见的非 prop attribute 有 class、style、id。组件内部可以通过实例 property $attrs 来访问到这些 attributes。

Attribute 继承

当组件返回的是单个根节点时,非 prop 的 attribute 将自动添加到根节点的 attribute 中。例如,日期选择组件 date-picker 返回单个根节点

app.component('date-picker', {
  template: `
    <div class="date-picker">
      <input type="datetime-local" />
    </div>
  `
})

如果我们用 data-status attribute 来表示 <date-picker> 组件的状态,即

<date-picker data-status="activated"></date-picker>

data-status attribute 会合并到根节点 div.date-picker,从而渲染为

<div class="date-picker" data-status="activated">
  <input type="datetime-local" />
</div>

对于事件监听器,也有同样的规则。我们下面的例子来演示非prop attribute的合并情况和父组件向子组件传递初始值的情况:

    <div id="app">
        <date-picker :initial-date="date"
             data-status="activated" @change="submitChange">
        </date-picker>
    </div>
    <script>
        const app = Vue.createApp({
            data() {
                return {
                    date: '2021-11-11'
                }
            },
            methods: {
                submitChange(e) {
                    console.log(e)
                }
            }
        })
        app.component('date-picker', {
            props: ['initialDate'],
            data() {
                return {
                    date: this.initialDate
                }
            },
            template: `
                <div class="date-picker">
                    <input type="datatime-local" :value="date" />
                </div>
            `,
            created() {
                console.log(this.$attrs)
            }
        })
        app.mount('#app')
    </script>

对于一个HTML根元素本身具有 change事件的组件来说,给它附加非prop的事件监听器change是有意义的,因为该事件监听器会从父组件传递到子组件,我们不需要显式从 date-picker 用代码去引发事件,因为子组件可以自动触发事件。

    <div id="app">
        <date-picker @change="showChange"></date-picker>
    </div>
    <script>
        const app = Vue.createApp({
            methods: {
                showChange(e) {
                    console.log(e.target.value)
                }
            }
        })
        app.component('date-picker', {
            template: `
                <select>
                    <option value="1">昨天</option>
                    <option value="2">今天</option>
                    <option value="3">明天</option>
                </select>
            `,
            created() {
                console.log(this.$attrs)
            }
        })
        app.mount('#app')
    </script>

禁用 Attribute 继承

和继承相反,如果不希望组件的根元素继承 attribute,可以在组件选项中设置 inheritAttrs: false,这样组件就不会进行自动合并的操作了。禁用 attribute 自动继承的常见场景是希望将 attribute 应用到异于根元素的其他元素。

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

上面的代码,会把外部 attribute 绑定到组件内的 input 上,即把

<date-picker data-status="activated"></date-picker>

渲染为

<div class="date-picker">
  <input type="datetime-local" data-status="activated" />
</div>

多个根节点的 Attribute 继承

Vue 没有规定多个根节点如何自动实现 attribute 继承,所以,多个根节点时,必须类似禁用Attribute那样手动绑定来实现继承(对某个元素 v-bind="$attrs"),不然会出现运行时警告。

<custom-layout id="custom-layout" @click="changeValue"></custom-layout>
app.component('custom-layout', {
  template: `
    <header>...</header>
    <main v-bind="$attrs">...</main>
    <footer>...</footer>
  `
})

抱歉,这是因为 Vue 2.6+ 版本中废弃了 `slot-scope`,取而代之的是使用 `v-slot` 来定义插槽内容。以下是一个更新后的示例代码: ```html <template> <div class="category-management"> <h2>分类管理</h2> <el-table :data="categories" style="width: 100%"> <el-table-column prop="name" label="名称"></el-table-column> <el-table-column prop="description" label="描述"></el-table-column> <el-table-column label="操作"> <template v-slot="scope"> <el-button size="small" @click="editCategory(scope.row)">编辑</el-button> <el-button size="small" @click="deleteCategory(scope.row)">删除</el-button> </template> </el-table-column> </el-table> </div> </template> <script> export default { data() { return { categories: [ { id: 1, name: '分类1', description: '这是分类1的描述' }, { id: 2, name: '分类2', description: '这是分类2的描述' }, { id: 3, name: '分类3', description: '这是分类3的描述' }, ], }; }, methods: { editCategory(category) { // 编辑分类逻辑 console.log('编辑分类', category); }, deleteCategory(category) { // 删除分类逻辑 console.log('删除分类', category); }, }, }; </script> <style scoped> .category-management { padding: 20px; } </style> ``` 在新的示例代码中,我们使用 `v-slot` 来定义插槽内容,并将插槽的内容赋给了 `scope` 变量。这样就可以在插槽内部使用 `scope` 来访问对应的数据。 希望这次的回答能满足你的要求。如果还有其他问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值