Vue.js组件基础

本章重点:组件是Vue.js最强大的功能之一。掌握组件化开发是构建复杂应用的关键。

什么是组件?

组件是可复用的Vue实例,带有一个名字。它们允许我们将UI划分为独立的、可复用的部分,并对每个部分进行单独的思考。

组件化思想
App组件
Header组件
Sidebar组件
Content组件

应用被分解为一个个独立的组件,每个组件管理自己的状态和视图

组件的优点

可复用性

一次编写,多处使用

可维护性

独立开发、测试和维护

组合性

可以组合成复杂应用

复用性

跨项目复用组件

创建组件

在Vue中有多种方式创建组件,最常见的是使用Vue.component全局注册:

步骤1:全局注册组件

// 全局注册组件
Vue.component('button-counter', {
    data: function() {
        return {
            count: 0
        }
    },
    template: `
        <button @click="count++">
            你点击了 {{ count }} 次
        </button>
    `
})
                                
步骤2:在Vue实例中使用组件

<div id="app">
    <!-- 像使用HTML元素一样使用组件 -->
    <button-counter></button-counter>
    <button-counter></button-counter>
    <button-counter></button-counter>
</div>

<script>
new Vue({
    el: '#app'
});
</script>
                                
注意:组件中的data必须是一个函数,而不是一个对象。这样可以确保每个实例都有自己独立的数据副本。

组件注册

全局注册

// 在任何Vue实例中都可以使用
Vue.component('my-component', {
    // ... 选项 ...
});
                                

优点:随处可用

缺点:增加构建体积

局部注册

// 只在特定Vue实例中可用
const ComponentA = {
    // ... 选项 ...
};

new Vue({
    el: '#app',
    components: {
        'component-a': ComponentA
    }
});
                                

优点:按需加载

缺点:需要显式引入

Props:向子组件传递数据

Props是父组件向子组件传递数据的一种方式。子组件需要显式声明它期望接收的props:


<!-- 父组件模板 -->
<div id="blog-post-demo">
    <blog-post
        v-for="post in posts"
        :key="post.id"
        :title="post.title"
        :content="post.content"
        :published="post.published"
        @publish="handlePublish"
    ></blog-post>
</div>
                            

// 子组件定义
Vue.component('blog-post', {
    // 声明props
    props: {
        title: String,
        content: String,
        published: Boolean
    },
    template: `
        <div class="blog-post">
            <h3>{{ title }}</h3>
            <p>{{ content }}</p>
            <button v-if="!published" @click="$emit('publish')">
                发布文章
            </button>
            <span v-else class="badge bg-success">已发布</span>
        </div>
    `
});

// 父组件
new Vue({
    el: '#blog-post-demo',
    data: {
        posts: [
            { id: 1, title: 'Vue.js入门', content: '...', published: true },
            { id: 2, title: '组件化开发', content: '...', published: false },
            { id: 3, title: 'Vue Router', content: '...', published: false }
        ]
    },
    methods: {
        handlePublish() {
            alert('文章已发布!');
        }
    }
});
                            
Props验证类型 示例 说明
字符串 title: String 必须是字符串类型
数字 age: Number 必须是数字类型
布尔值 isActive: Boolean 必须是布尔类型
数组 items: Array 必须是数组类型
对象 user: Object 必须是对象类型
函数 callback: Function 必须是函数类型
自定义验证
status: {
    type: String,
    required: true,
    validator: function(value) {
        return ['active', 'pending', 'deleted'].indexOf(value) !== -1
    }
}
自定义验证规则
最佳实践:始终使用Props验证,这可以帮助其他开发者理解组件的预期输入,并在开发过程中捕获错误。

自定义事件:子组件向父组件通信

子组件可以使用$emit触发自定义事件,父组件可以监听这些事件:


<!-- 父组件模板 -->
<div id="counter-app">
    <p>总点击次数: {{ totalCount }}</p>
    <button-counter
        @increment="incrementTotal"
        @reset="resetTotal"
    ></button-counter>
</div>
                        

// 子组件
Vue.component('button-counter', {
    data() {
        return {
            count: 0
        };
    },
    template: `
        <div>
            <button @click="increment">
                点击了 {{ count }} 次
            </button>
            <button @click="reset" class="btn btn-secondary">
                重置
            </button>
        </div>
    `,
    methods: {
        increment() {
            this.count++;
            // 触发自定义事件
            this.$emit('increment');
        },
        reset() {
            this.count = 0;
            // 触发自定义事件并传递数据
            this.$emit('reset', this.count);
        }
    }
});

// 父组件
new Vue({
    el: '#counter-app',
    data: {
        totalCount: 0
    },
    methods: {
        incrementTotal() {
            this.totalCount++;
        },
        resetTotal(newCount) {
            this.totalCount = 0;
            console.log('计数器重置为:', newCount);
        }
    }
});
                        

插槽(Slot)

插槽允许父组件向子组件传递内容:

默认插槽

<!-- 子组件 -->
<div class="alert">
    <slot>默认内容</slot>
</div>

<!-- 父组件使用 -->
<custom-alert>
    这是自定义警告内容
</custom-alert>
                                
具名插槽

<!-- 子组件 -->
<div class="card">
    <div class="card-header">
        <slot name="header"></slot>
    </div>
    <div class="card-body">
        <slot name="body"></slot>
    </div>
</div>

<!-- 父组件使用 -->
<custom-card>
    <template v-slot:header>
        <h4>卡片标题</h4>
    </template>
    <template v-slot:body>
        <p>卡片内容...</p>
    </template>
</custom-card>
                                

<!-- 作用域插槽 -->
<!-- 子组件传递数据给插槽 -->
<ul>
    <li v-for="item in items">
        <slot :item="item">
            {{ item.name }}
        </slot>
    </li>
</ul>

<!-- 父组件接收数据 -->
<item-list :items="items">
    <template v-slot:default="slotProps">
        <span class="text-primary">{{ slotProps.item.name }}</span>
    </template>
</item-list>

<!-- 简写 -->
<item-list :items="items">
    <template #default="{ item }">
        <span class="text-success">{{ item.name }}</span>
    </template>
</item-list>
                        

动态组件

使用<component>元素和is特性可以实现动态组件:


<div id="dynamic-component-demo">
    <!-- 切换按钮 -->
    <button @click="currentTab = 'tab-home'"
            :class="{ active: currentTab === 'tab-home' }">
        首页
    </button>
    <button @click="currentTab = 'tab-posts'"
            :class="{ active: currentTab === 'tab-posts' }">
        文章
    </button>
    <button @click="currentTab = 'tab-archive'"
            :class="{ active: currentTab === 'tab-archive' }">
        归档
    </button>

    <!-- 动态组件 -->
    <component :is="currentTab" class="tab"></component>
</div>
                            

Vue.component('tab-home', {
    template: '<div>Home component</div>'
});

Vue.component('tab-posts', {
    template: '<div>Posts component</div>'
});

Vue.component('tab-archive', {
    template: '<div>Archive component</div>'
});

new Vue({
    el: '#dynamic-component-demo',
    data: {
        currentTab: 'tab-home'
    }
});
                            

组件生命周期

每个Vue组件实例在创建时都要经历一系列的初始化过程:

组件生命周期图示
初始化
挂载
更新
销毁

Vue.component('lifecycle-demo', {
    data() {
        return {
            message: 'Hello Vue!'
        };
    },

    // 生命周期钩子
    beforeCreate() {
        console.log('beforeCreate: 实例初始化之后,数据观测之前');
    },

    created() {
        console.log('created: 实例创建完成,数据观测已建立');
        // 可以在这里发起API请求
    },

    beforeMount() {
        console.log('beforeMount: 挂载开始之前');
    },

    mounted() {
        console.log('mounted: 挂载完成,DOM已渲染');
        // 可以在这里操作DOM
    },

    beforeUpdate() {
        console.log('beforeUpdate: 数据更新时,DOM更新之前');
    },

    updated() {
        console.log('updated: 数据更新后,DOM已更新');
    },

    beforeDestroy() {
        console.log('beforeDestroy: 实例销毁之前');
        // 清理定时器、取消事件监听等
    },

    destroyed() {
        console.log('destroyed: 实例销毁完成');
    },

    template: '<div>{{ message }}</div>'
});
                            

完整示例:待办事项应用

使用组件构建一个完整的待办事项应用:


<div id="todo-app">
    <h2>我的待办事项</h2>

    <!-- 添加待办事项组件 -->
    <todo-form @add-todo="addTodo"></todo-form>

    <!-- 待办事项列表组件 -->
    <todo-list
        :todos="todos"
        @toggle-todo="toggleTodo"
        @delete-todo="deleteTodo"
    ></todo-list>

    <!-- 统计信息组件 -->
    <todo-stats :todos="todos"></todo-stats>
</div>
                            

// 1. 待办事项表单组件
Vue.component('todo-form', {
    data() {
        return {
            newTodo: '',
            priority: 'medium'
        };
    },
    template: `
        <form @submit.prevent="addTodo" class="mb-4">
            <div class="input-group">
                <input
                    type="text"
                    v-model="newTodo"
                    placeholder="输入新的待办事项"
                    class="form-control"
                >
                <select v-model="priority" class="form-select">
                    <option value="low">低优先级</option>
                    <option value="medium">中优先级</option>
                    <option value="high">高优先级</option>
                </select>
                <button type="submit" class="btn btn-primary">
                    添加
                </button>
            </div>
        </form>
    `,
    methods: {
        addTodo() {
            if (this.newTodo.trim()) {
                this.$emit('add-todo', {
                    text: this.newTodo,
                    priority: this.priority
                });
                this.newTodo = '';
                this.priority = 'medium';
            }
        }
    }
});

// 2. 待办事项项组件
Vue.component('todo-item', {
    props: {
        todo: Object
    },
    template: `
        <div class="todo-item" :class="{
            'completed': todo.completed,
            'priority-high': todo.priority === 'high',
            'priority-low': todo.priority === 'low'
        }">
            <input
                type="checkbox"
                :checked="todo.completed"
                @change="$emit('toggle', todo.id)"
            >
            <span class="todo-text">{{ todo.text }}</span>
            <span class="badge bg-secondary">{{ todo.priority }}</span>
            <button @click="$emit('delete', todo.id)" class="btn btn-sm btn-danger">
                删除
            </button>
        </div>
    `
});

// 3. 待办事项列表组件
Vue.component('todo-list', {
    props: {
        todos: Array
    },
    template: `
        <div class="todo-list">
            <div v-if="todos.length === 0" class="alert alert-info">
                暂无待办事项
            </div>
            <todo-item
                v-for="todo in todos"
                :key="todo.id"
                :todo="todo"
                @toggle="$emit('toggle-todo', $event)"
                @delete="$emit('delete-todo', $event)"
            ></todo-item>
        </div>
    `
});

// 4. 统计信息组件
Vue.component('todo-stats', {
    props: {
        todos: Array
    },
    computed: {
        total() {
            return this.todos.length;
        },
        completed() {
            return this.todos.filter(todo => todo.completed).length;
        },
        pending() {
            return this.total - this.completed;
        }
    },
    template: `
        <div class="todo-stats mt-4">
            <div class="row">
                <div class="col-md-4">
                    <div class="card bg-primary text-white">
                        <div class="card-body text-center">
                            <h4>{{ total }}</h4>
                            <p>总任务数</p>
                        </div>
                    </div>
                </div>
                <div class="col-md-4">
                    <div class="card bg-success text-white">
                        <div class="card-body text-center">
                            <h4>{{ completed }}</h4>
                            <p>已完成</p>
                        </div>
                    </div>
                </div>
                <div class="col-md-4">
                    <div class="card bg-warning text-white">
                        <div class="card-body text-center">
                            <h4>{{ pending }}</h4>
                            <p>待完成</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    `
});

// 5. 主应用
new Vue({
    el: '#todo-app',
    data: {
        todos: [
            { id: 1, text: '学习Vue组件', completed: true, priority: 'high' },
            { id: 2, text: '编写文档', completed: false, priority: 'medium' },
            { id: 3, text: '测试应用', completed: false, priority: 'low' }
        ],
        nextId: 4
    },
    methods: {
        addTodo(todoData) {
            this.todos.push({
                id: this.nextId++,
                text: todoData.text,
                completed: false,
                priority: todoData.priority
            });
        },
        toggleTodo(todoId) {
            const todo = this.todos.find(t => t.id === todoId);
            if (todo) {
                todo.completed = !todo.completed;
            }
        },
        deleteTodo(todoId) {
            this.todos = this.todos.filter(t => t.id !== todoId);
        }
    }
});
                            
注意事项:
  • 组件命名:使用短横线命名法(kebab-case)或帕斯卡命名法(PascalCase)
  • Props:使用小驼峰命名法(camelCase)定义,但在模板中使用短横线命名法
  • 根元素:Vue 2.x中组件必须有一个根元素,Vue 3.x支持片段
  • 避免直接修改Props,使用事件向上通信
  • 合理使用计算属性和侦听器处理复杂逻辑

组件设计最佳实践

单一职责原则

每个组件应该只做一件事。如果一个组件变得过于复杂,应该考虑拆分成更小的组件。

明确通信方式

Props向下传递,事件向上传递。避免直接修改子组件的状态。

可复用性设计

通过Props和插槽使组件更加灵活,可以适应不同的使用场景。

Props验证

始终为Props提供验证,这有助于捕获错误并提高代码可维护性。