刚接触 vue3,基于 vue 3.x + bootstrap 做了个任务列表的小案例,在这里简单梳理一下流程以便加深理解
1. 初始化项目
>> 在终端运行命令,初始化项目
npm init vite-app todos
>> 使用 vscode 打开项目,安装依赖
npm install
>> 安装 less 语法相关依赖
npm i less -D
2. 梳理项目结构
初始化 index.css 文件,创建 TodoList 组件并引入到 App.vue 中。初始项目列表如下
>> 重置 index.css
:root {
font-size: 12px;
}
body {
padding: 8px;
}
>> 重置 App.vue 组件
<template>
<div>
<h1>App 根组件</h1>
<hr>
<todo-list></todo-list>
</div>
</template>
<script>
import TodoList from './components/todo-list/TodoList.vue'
export default {
name: 'MyApp',
data() {
return {
// 任务列表数据
todolist: [
{ id: 1, task: '周一早晨9点开会', done: false },
{ id: 2, task: '周一晚上8点聚餐', done: false },
{ id: 3, task: '准备周三上午的演讲稿', done: true }
]
}
},
components: {
TodoList
}
}
</script>
<style lang="less" scoped>
</style>
3. 封装 todo-list 组件
>> 基于 bootstrap 渲染列表组件
将 bootstrap 文件拷贝至 src/assets
静态资源目录中,在 main.js
入口文件中导入 src/assets/css/bootstrap.css
样式表。
import { createApp } from 'vue'
import App from './App.vue'
// bootstrap
import './assets/css/bootstrap.css'
import './index.css'
createApp(App).mount('#app')
根据 bootstrap 提供的 列表组 和 复选框 渲染列表组件的基本效果。
<ul class="list-group">
<!-- 列表组 -->
<li class="list-group-item d-flex justify-content-between align-items-center">
<!-- 复选框 -->
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="customCheck1">
<label class="custom-control-label" for="customCheck1">Check this custom checkbox</label>
</div>
<!-- 徽标 badge -->
<span class="badge badge-success badge-pill">完成</span>
<span class="badge badge-danger badge-pill">未完成</span>
</li>
</ul>
>> 为 TodeList 声明 props 属性
为了接受外界传递的列表数据,需要在 TodoList 组件中声明如下 props 属性:
export default {
name: 'TodoList',
props: {
// 列表数据
list: {
type: Array,
required: true,
default: []
}
}
}
之后,在 App 组件中通过 list
属性,将数据传递到 TodoList 组件中:
<todo-list :list="todolist"></todo-list>
>> 渲染列表的 DOM 结构
通过 v-for
指令,循环渲染列表的 DOM 结构:
<template>
<ul class="list-group">
<!-- 列表组 -->
<li class="list-group-item d-flex justify-content-between align-items-center" v-for="item in list" :key="item.id">
<!-- 复选框 -->
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" :id="item.id">
<label class="custom-control-label" :for="item.id">{{ item.task }}</label>
</div>
<!-- 徽标 badge -->
<span class="badge badge-success badge-pill">完成</span>
<span class="badge badge-danger badge-pill">未完成</span>
</li>
</ul>
</template>
通过 v-if
和 v-else
指令,按需渲染 badge 效果(完成/未完成):
<span class="badge badge-success badge-pill" v-if="item.done">完成</span>
<span class="badge badge-danger badge-pill" v-else>未完成</span>
通过 v-model
指令,双向绑定任务的完成状态:
<input type="checkbox" class="custom-control-input" :id="item.id" v-model="item.done">
通过 v-bind
属性绑定,动态切换元素的 class 类名:
<label class="custom-control-label" :class="item.done ? 'delete':''" :for="item.id">{{ item.task }}</label>
TodoList 组件中样式声明:
<style lang="less" scoped>
.list-group {
width: 400px; // 为列表设置固定宽度
}
.delete {
text-decoration: line-through;
color: gray;
font-style: italic;
}
</style>
4. 封装 todo-input 组件
>> 创建并注册 TodoInput 组件
在 src/components/todo-input/
目录下新建 TodoInput.vue
组件,并导入至 App.vue 组件中。
>> 基于 bootstrap 渲染组件结构
根据 bootstrap 提供的 inline-forms 渲染 TodoInput 组件的基本结构。
<form class="form-inline">
<div class="input-group mb-2 mr-sm-2">
<div class="input-group-prepend">
<div class="input-group-text">任务</div>
</div>
<input type="text" class="form-control" placeholder="请输入任务信息" style="with: 356px">
</div>
<button type="submit" class="btn btn-primary mb-2">添加新任务</button>
</form>
>> 通过自定义事件向外传递数据
需求:在 App 组件中,监听 TodoInput 组件的自定义事件,获取要添加的任务名称
在 TodoInput
组件的 data 中声明如下数据:
data() {
return {
// 新任务名称
taskname: ''
}
}
为 TodoInput
组件中 input 输入框进行 v-model
双向数据绑定:
<input type="text" class="form-control" placeholder="请输入任务信息" style="with: 356px" v-model.trim="taskname">
监听 form
表单的 submit 事件,阻止默认提交行为并指定事件处理函数:
<form class="form-inline" @submit.prevent="onFormSubmit">
在 methods
中声明 onFormSubmit
事件处理函数:
emits: ['add'],
methods: {
onFormSubmit() {
// 1. 判断任务名称不能为空
if (!this.taskname) return alert('任务名称不能为空!')
// 2. 触发自定义 add 事件,并向外界传递数据
this.$emit('add', this.taskname)
// 3. 清空文本框
this.taskname = ''
}
}
>> 实现添加任务的功能
在 App.vue
组件中监听 TodoInput
组件自定义的 add
事件:
<todo-input @add="onAddNewTask"></todo-input>
在 App.vue
组件的 data 中声明 nextId
来模拟 id 自增 +1 的操作:
data() {
return {
// 任务列表数据
todolist: [
{ id: 1, task: '周一早晨9点开会', done: false },
{ id: 2, task: '周一晚上8点聚餐', done: false },
{ id: 3, task: '准备周三上午的演讲稿', done: true }
],
// 下一个可用 Id
nextId: 4
}
},
在 App.vue
组件的 methods 中声明 onAddNewTask
事件处理函数如下:
methods: {
onAddNewTask(taskname) {
// 向任务列表中新增任务信息
this.todolist.push({
id: this.nextId,
task: taskname,
done: false
})
// nextId 自增
this.nextId++
}
},
5. 封装 todo-button 组件
>> 创建并注册 TodoButton 组件
在 src/components/todo-button/
目录下新建 TodoButton.vue
组件,并导入至 App.vue
中:
>> 基于 bootstrap 渲染组件结构
根据 bootstrap 提供的 Button group 渲染 TodoButton
组件的基本结构:
<div class="button-container">
<div class="btn-group">
<button type="button" class="btn btn-primary">全部</button>
<button type="button" class="btn btn-secondary">已完成</button>
<button type="button" class="btn btn-secondary">未完成</button>
</div>
</div>
btn-primary
、btn-secondary
为 Bootstrap 内置的预定义的按钮样式,参见 按钮(Buttons)
通过类名 button-container
美化样式
<style lang="less" scoped>
.button-container {
width: 400px;
text-align: center;
}
</style>
>> 通过 props 指定默认激活的按钮
在 TodoButton
组件中声明如下的 props,用来指定默认激活按钮的索引:
props: {
active: {
type: Number,
required: true,
default: 0 // 默认激活全部按钮
}
}
通过动态绑定 class 类名的方式控制按钮的激活状态:
<div class="button-container">
<div class="btn-group">
<button type="button" class="btn" :class="active === 0 ? 'btn-primary' : 'btn-secondary'">全部</button>
<button type="button" class="btn" :class="active === 1 ? 'btn-primary' : 'btn-secondary'">已完成</button>
<button type="button" class="btn" :class="active === 2 ? 'btn-primary' : 'btn-secondary'">未完成</button>
</div>
</div>
在 App
组件中声明默认激活项的索引,并通过属性绑定的方式传递给 TodoButton
组件:
<todo-button class="mt-3" :active="activeBtnIndex"></todo-button>
data() {
...
return {
activeBtnIndex: 0
}
},
>> 通过 v-model 更新激活项的索引
为 TodoButton
组件中的三个按钮分别绑定 click
事件处理函数:
<button type="button" class="btn" :class="active === 0 ? 'btn-primary' : 'btn-secondary'" @click="onBtnClick(0)">全部</button>
<button type="button" class="btn" :class="active === 1 ? 'btn-primary' : 'btn-secondary'" @click="onBtnClick(1)">已完成</button>
<button type="button" class="btn" :class="active === 2 ? 'btn-primary' : 'btn-secondary'" @click="onBtnClick(2)">未完成</button>
在 TodoButton
组件中声明如下的自定义事件,用来更新父组件通过 v-model 指令传递来的 props 数据:
export default {
name: 'TodoButton',
emits: ['update:active'], // 声明和 v-model 相关的自定义事件
props: {
// 激活项的索引
active: {
type: Number,
required: true,
default: 0 // 默认激活全部按钮
}
},
}
在 TodoButton
组件的 methods
节点中声明 onBtnClick
事件处理函数:
methods: {
onBtnClick(index) {
if (index === this.active) return // 索引值不变,没必要传数据
this.$emit('update:active', index) // 触发自定义事件,传递新索引
}
}
>> 通过计算属性动态切换列表数据
需求:点击不同按钮,切换显示不同列表数据。可以根据当前激活按钮的索引,动态计算出要显示的列表数据并返回。
在 App
根组件中声明如下计算属性:
computed: {
// 根据激活按钮的索引,动态计算要显示的列表数据
tasklist() {
switch (this.activeBtnIndex) {
case 0:
return this.todolist
case 1:
return this.todolist.filter(x => x.done === true)
case 2:
return this.todolist.filter(x => x.done === false)
}
}
},
在 App
根组件的 DOM 结构中,将 TodoList
组件的 :list="todolist"
修改为该计算属性:
<!-- 使用 TodoList 组件 -->
<todo-list :list="tasklist" class="mt-2"></todo-list>