前言

使用Vue3最新的Composition API,开发一个Todo List应用。

Todo List应用比较简单,不会使用其他复杂的Vue3组件,但这也使得当前应用与真实的应用脱钩,所以本文重点是体会Vue3的最新语法,下篇文章会阅读element-plus-admin开源项目的源码,从而掌握Vue3目前最近技术栈的使用方式。

所谓Todo List应用主要用于记录你需要完成的工作,完成后,将其打钩,最终效果如下:

写点HTML+CSS吧

先用yarn基于vite构建出vue-ts的项目目录,命令如下。

yarn create vite TodoList --template vue-ts
yarn && yarn dev

在开始编码前,最好弄一下原型稿,我个人为求方便,通常使用PPT来构建原型稿,专业的前端可能会使用专门的原型设计软件,与后端开发前写功能设计一个道理,前端开发前,弄好原型稿,从而从整体把握开发出网页的样式、配色以及可能需要需要的组件。

在弄原型稿时,要刻意关注页面由那几部分组成,一个复杂的业务,拆分到最后,很发现都是一个简单的HTML元素的组合使用,此外,原型稿对CSS编写也很有帮助,复杂的样式效果,拆分到最后也是一些简单的CSS属性。

首先,我们基于原型稿,通过HTML搭建类似的结构,直接全部写到App.vue的template中,代码如下:

<div class="main">
<div class="container"><h1>欢迎使用Todo List</h1><div class="add-todo"><input type="text" name="todo" /><button><!-- 按钮上的加号元素 --><i class="add-icon"></i></button></div><div class="filters"><span class="filter active">全部</span><span class="filter">已完成</span><span class="filter">未完成</span></div><div class="todo-list"><div class="todo-item"><!-- 使用label包裹input与span,后续用户无论点击input还是span,都会触发checkbox事件,从而实现勾选了input的效果 --><label><input type="checkbox" />写开发Todo List的文章<span class="check-button"></span></label></div><div class="todo-item"><label><input type="checkbox" />写开发Todo List的文章<span class="check-button"></span></label></div><div class="todo-item"><label><input type="checkbox" />写开发Todo List的文章<span class="check-button"></span></label></div></div></div>
</div>

运行项目,会得到如下效果这个阶段,不需要考虑HTML元素的样式,只需要将原型图里的组件一步步通过HTML搭建出来就好了,然后再写CSS去美化它。

CSS对很多后端同学来说,是难啃的骨头,其原因在于CSS属性多,而且就算记住了这些属性,也不知道如何配合使用,从而构建出美丽的样式。

我对CSS的思考是,要掌握CSS中比较核心的内容,比如CSS的布局与位置相关的内容,然后多看一些项目,去掌握感觉,当然最重要的是,你要先画原型图,在脑海中,比较清晰的知道自己需要的样子,然后再利用CSS属性一点点组合并配合Chrome的可见可得的特性,一点点组合出来,后续我也会写一篇文章记录我认为比较重要的CSS属性。

回到本文,Todo List的CSS样式如下:

* {box-sizing: border-box;margin: 0;padding: 0;font-family: Helvetica, "PingFang SC", "Microsoft Yahei", sans-serif;
}/* 整个页面 */
.main {width: 100vw;min-height: 100vh;display: grid;align-items: center;justify-items: center;background: rgb(216, 243, 214);
}.container {width: 60%;max-width: 400px;box-shadow: 0px 0px 24px rgba(26, 25, 25, 0.15);border-radius: 24px;padding: 48px 28px;background-color: rgb(229, 230, 235);
}/* 标题 */
h1 {margin: 24px 0;font-size: 28px;color: #384280;
}/* 添加框 */
.add-todo {position: relative;display: flex;align-items: center;
}.add-todo input {padding: 16px 52px 16px 18px;border-radius: 48px;border: none;outline: none;box-shadow: 0px 0px 24px rgba(0, 0, 0, 0.08);width: 100%;font-size: 16px;color: #626262;
}.add-todo button {width: 46px;height: 46px;border-radius: 50%;background: linear-gradient(#b6f1a2, #6df86d);border: none;outline: none;color: white;position: absolute;right: 0px;cursor: pointer;
}.add-todo .add-icon {display: block;width: 100%;height: 100%;background: linear-gradient(#fff, #fff), linear-gradient(#fff, #fff);background-size: 50% 2px, 2px 50%;background-position: center;background-repeat: no-repeat;
}/* 过滤选项 */
.filters {display: flex;margin: 24px 2px;color: #c0c2ce;font-size: 14px;
}.filters .filter {margin-right: 14px;transition: 0.8s;
}.filters .filter.active {color: #6b729c;transform: scale(1.2);
}/* todo 列表 */
.todo-list {display: grid;row-gap: 14px;
}.todo-item {background: white;padding: 16px;border-radius: 8px;color: #626262;
}.todo-item label {position: relative;display: flex;align-items: center;
}.todo-item label span.check-button {position: absolute;top: 0;
}.todo-item label span.check-button::before,
.todo-item label span.check-button::after {content: "";display: block;position: absolute;width: 18px;height: 18px;border-radius: 50%;
}.todo-item label span.check-button::before {border: 1px solid #7ad86d;
}.todo-item label span.check-button::after {transition: 0.4s;background: #7ad86d;transform: translate(1px, 1px) scale(0.8);opacity: 0;
}.todo-item input {margin-right: 16px;opacity: 0;
}.todo-item input:checked + span.check-button::after {opacity: 1;
}

然后效果如下:


拆分成组件

目前,我们将所有的代码都写在App.vue中,这显然是不合适的,特别是项目大起来时,就很难维护,而且也没有发挥Vue组件化开发的优点,在进一步开发Todo List功能逻辑前,先将前面写的HTML、CSS拆分到不同的组件中。

组件拆分的目的是为了减少重复功能(组件复用)、增加可读性(方便维护),具体的拆分过程有一些讲究,这里我按功能将页面中不同元素进行组件的拆分。

简单思考Todo List应用的功能,无非就是添加Todo、展示不同Todo、完成时勾选掉Todo,简单思索,这里将其拆成成4个组件:

  • TodoAdd:添加Todo

  • TodoFilter:展示不同状态的Todo

  • TodoList:todo列表

  • TodoListItem:todo列表中的元素

在components文件夹中创建相应的Vue文件,然后将App.vue中的HTML和CSS分别复制到这些组件中。

components文件夹目录结构:

我们使用Vue3最新的语法糖<script setup>来实现不同的组件,目前网上很多文章还是基于setup()函数的形式去实现项目的,虽说都可以达到最终的目的,但<script setup>会轻松很多(你可能需要先熟悉<script setup>写法,可先阅读Vue3文档)。

先编写TodoAdd.vue,将App.vue中添加Todo相关的代码都复制到TodoAdd.vue中,html复制到template中,css复制到style中,然后来编写TS代码,如下:

// src/components/TodoAdd.vue<script setup lang="ts">import {ref} from "vue"const todoContent = ref("")// 接收父元素的传递下来的值const props = defineProps<{tid: number}>()const emit = defineEmits(['add-todo'])const emitAddTodo = () => {const todo = {id: props.tid,content: todoContent.value,completed: false}// 发出add-todo事件给父组件emit('add-todo', todo);// 置空,让input元素中的内容清空todoContent.value = ''}
</script>

上述代码中,通过defineProps来获取父组件传递下来的tid(number类型),然后通过defineEmits定义出emit,defineEmits参数中的add-todo也与父组件中使用该组件时对应起来,为了方便理解这两句话,将App.vue(父组件)中使用TodoAdd组件的形式粘贴出来:

// src/App.vue
<script setup lang="ts">import {reactive , ref, computed, Ref} from "vue"import TodoAdd from "./components/TodoAdd.vue"import {Todo} from './composables/iTodo'const todos = reactive<Array<Todo>>([])const addTodo = (todo: any) => todos.push(todo)
</script><!-- :tid 会传递给TodoAdd组件,@add-todo会接受TodoAdd组件的事件并调研addTodo函数 -->
<TodoAdd :tid="todos.length" @add-todo="addTodo"/>

App.vue代码中,除了引入TodoAdd.vue外,还引入了iTodo.ts。因为我们使用TypeScript来编写代码,所以会受到TypeScript类型检测的限制,在通过defineProps函数获取父组件传递的todos时,需要指明类型,因为Todo是我们自定义的对象结构,在TS中申明自定义类型的一种方法是定义interface,如果该类型会被多次使用,通常会将对象结构定义到单独的文件在export出来,就如上述代码中的iTodo.ts,该文件代码如下:

// src/composables/iTodo.tsexport interface Todo {id: number,content: string,completed: boolean
}

看回TodoAdd.vue的template,代码如下:

// src/components/TodoAdd.vue<template>
<div class="add-todo"><input type="text" name="todo" v-model="todoContent" @keyup.enter="emitAddTodo"/><button @click="emitAddTodo"><!-- 按钮上的加号元素 --><i class="add-icon"></i></button></div>
</template>

其中input标签,通过v-model将input标签展示的内容与todoContent绑定,todoContent变量由ref创建,todoContent变量后续的变化会通过Vue的双向绑定,直接显示在input中。@keyup.enter用于获取用户在键盘敲击Enter键时的事件,当用户敲击Enter或点击button时,都会触发emitAddTodo函数。

接着实现TodoList.vue,代码如下:

<script setup lang="ts">
import TodoListItem from './TodoListItem.vue';
import {Todo} from '../composables/iTodo'const props = defineProps<{todos: Array<Todo>}>()</script><template>
<div class="todo-list"><TodoListItem v-for="todo in todos" :key="todo.id" :todo-item="todo"></TodoListItem>
</div>
</template>

上述代码引入了TodoListItem组件,该组件用于显示列表中具体的Todo元素,然后通过defineProps获得父组件传递的todos对象,在template中渲染Todo List时,使用v-for遍历todos变量,App.vue与之对应的代码如下:

<script setup lang="ts">const todos = reactive<Array<Todo>>([])const addTodo = (todo: any) => todos.push(todo)
</script><TodoList :todos="todos"/>

结合TodoAdd.vue组件一起看,通过TodoAdd组件,将todo元素添加到todos这个列表中,再将todos传递给TodoList子组件,还有个细节,todos变量使用reactive创建而没有使用ref,Vue3官方文档中,要创建简单的响应式变量,建议使用ref,对于复杂的响应式变量,建议使用reactive,因为这里的是比较复杂的结构(Array),所以使用了reactive。

TodoListItem.vue才是具体展示的对象,代码如下:

// src/components/TodoListItem.vue<script setup lang="ts">
import { Todo } from "../composables/iTodo";
// 接收父组件 todo-item 的数据
const props = defineProps<{todoItem: Todo}>()</script><template>
<div class="todo-item" :class="{done: todoItem.completed}">
<!-- 用 label 包裹后,点击里边任何一个元素都能触发 checkbox 的事件 -->
<label><inputtype="checkbox" :checked="todoItem.completed"@click="$emit('change-state', $event)"/>{{ todoItem.content }}<span class="check-button"></span>
</label>
</div>
</template>

上述代码中,input元素通过@click监听用户的点击事件,当用户点击时,通过emit函数将事件向上传递给TodoList父组件,TodoList.vue中将代码修改一下:

// src/components/TodoList.vue
<template>
<div class="todo-list"><TodoListItem v-for="todo in todos" :key="todo.id" :todo-item="todo"@change-state="todo.completed = $event.target.checked"></TodoListItem>
</div>
</template>

TodoList.vue中通过@change-state来接收子组件传递的数据,如果用户点击了,那么$envent.target.checked为true,将true值赋值给todo.completed,todo.completed的改变会影响到TodoListItem子组件。

// src/components/TodoListItem.vue
<template>
<!-- :class 动态添加done这个class -->
<div class="todo-item" :class="{done: todoItem.completed}"><label><!-- :checked 动态改变input是否可点击的状态 --><inputtype="checkbox" :checked="todoItem.completed"@click="$emit('change-state', $event)"/>{{ todoItem.content }}<span class="check-button"></span>
</label>
</div>
</template><style>.todo-item.done label {/* 划线 */text-decoration: line-through;/* 斜体 */font-style: italic;
}
</style>

最后,还有个过滤Todo组件,选择未完成时,让TodoList只暂时未完成的Todo Item,选择已完成,也是类似的结果,TodoFilter.vue主要代码如下:

// src/components/TodoFilter.vue
<script setup lang="ts">
import { ref } from 'vue';const filters = [{label: "全部", value: "all"},{label: "已完成", value: "done"},{label: "未完成", value: "todo"},
]const props = defineProps<{selected: string
}>()</script><template><div class="filters"><span v-for="filter in filters":key="filter.value"class="filter":class="{active: selected === filter.value}"@click="$emit('change-filter', filter.value)">{{filter.label}}</span>
</div>
</template>

上述代码中,通用通过defineProps获得父组件传递的数据,父组件会选择某个状态,TodoFilter.vue需要过滤出相应的数据,实现方式还是那些,首先通过v-for来循环处理,展示相应的内容,然后:class来控制样式,如果选择了是什么样式,没选择是什么样式,@click用于监听点击事件,如果点击了,通过emit函数将数据向上传递给父组件,而父组件是App.vue,相应代码如下:

<script setup lang="ts">
import {reactive , ref, computed, Ref} from "vue"
const todos = reactive<Array<Todo>>([])
const addTodo = (todo: any) => todos.push(todo)const filter = ref<string>("all");
// 过滤
const filteredTodos = computed(() => {switch(filter.value) {case "done":return todos.filter((todo) => todo.completed);case "todo":return todos.filter((todo) => !todo.completed);default:return todos;}
})
</script><template>
<div class="main">
<div class="container"><h1>欢迎使用Todo List</h1><TodoAdd :tid="todos.length" @add-todo="addTodo"/><TodoFilter :selected="filter"@change-filter="filter=$event"/><TodoList :todos="filteredTodos"/></div>
</div></template>

至此,Todo应用就实现好了。

拆分逻辑

为了进一步提高复用性,通常会将Vue中的TS逻辑抽离出来,当然是将比较通用的部分抽离,或单纯为了方便阅读,将功能相近的代码聚合在一起,这里我们也将Todo应用中TS代码拆分一下,放在src/composables中。

首先,将App.vue中部分代码抽到useTodos.ts中,代码如下:

import {onMounted, reactive} from "vue"
import TodoListItemVue from "../components/TodoListItem.vue"
import {Todo} from "./iTodo"export default function useTodos() {const todos = reactive<Array<Todo>>([])const addTodo = (todo: any) => todos.push(todo)// 请求数据const fetchTodos = async () => {const response = await fetch("http://127.0.0.1:8000/todos")const rawTodos = await response.json() // 数据添加到todos中for (let i=0; i < rawTodos.length;i++) {let rawtodo = rawTodos[i]todos.push({id: rawtodo.id,content: rawtodo.content,completed: rawtodo.completed})}}// 生命周期函数 - 组件加载后,Vue会自动调用onMounted()onMounted(() => {fetchTodos();})return {todos,addTodo}}

通过上述代码可知,所谓抽离,就是就App.vue中部分代码复制到useTodos.ts中,抽离后TodoAdd.vue干爽很多。

<script setup lang="ts">
import {reactive , ref, computed, Ref} from "vue"
import TodoAdd from "./components/TodoAdd.vue"
import TodoFilter from "./components/TodoFilter.vue";
import TodoList from "./components/TodoList.vue"
import useTodos from "./composables/useTodos"
import useFilteredTodos from "./composables/useFilteredTodos"
import {Todo} from './composables/iTodo'const {todos , addTodo} = useTodos();
const {filter, filteredTodos} = useFilteredTodos(todos);</script><template>
<div class="main">
<div class="container"><h1>欢迎使用Todo List</h1><TodoAdd :tid="todos.length" @add-todo="addTodo"/><TodoFilter :selected="filter"@change-filter="filter=$event"/><TodoList :todos="filteredTodos"/></div>
</div></template>

其他组件的抽离的过程也是类似的,文末会有本项目的github地址,就不多赘述了。

在useTodos.ts中新增了fetchTodos函数,在onMounted函数中调用fetchTodos函数,实现在组件加载完成后,通过接口后的Todo List数据,通过类似的逻辑,我们可以将Todo应用改造成后端可存储数据的应用。

与fetchTodos函数配套的后端接口使用Python Flask编写,直接请求会有跨域问题,使用flask-cros解决跨域问题,完整代码如下:

from flask import Flask, jsonify
from flask_cors import CORSapp = Flask(__name__)
CORS(app)@app.route("/todos")
def hello_world():data = [{"id": 1,"content": "阅读书籍","completed": False},{"id": 2,"content": "写基于Vue3开发Todo List的文章","completed": True},{"id": 3,"content": "看电影","completed": False},]return jsonify(data)if __name__ == '__main__':app.run(port=8000)

结尾

Todo应用就编写完成了,我们使用了Vue3最新的语法实现了Todo,而目前网上多数文章都是使用Vue3旧的语法,前端变化真的太快,几个月前的文章,现在可能就不太一样了,后续会尝试剖析Vue3源码,做到掌握其中不变的东西。

Todo List Github: https://github.com/ayuLiao/TodoListVue3

Enjoy Coding!下篇文章见。

[Vue3实操] 开发Todo List相关推荐

  1. [LTTng实操]------开发Babeltrace 2插件

    一.分析提供的示例 1.source 头文件 #include <stdlib.h> #include <stdio.h> #include <stdint.h> ...

  2. 【沙龙】基于MDM9206芯片的gokit4(G)的应用实操

    导读 紧跟前沿通信技术.Get最新开发技能,高通Qualcomm&机智云&移远通信高级工程师手把手教开发:通过GoKit4(G)+MDM9206快速接入机智云,4小时掌握高通MDM92 ...

  3. 华为昇腾师资培训沙龙·南京场 |华为昇腾 ACL 语言开发实践全程干货来了!看完就实操系列...

    自今年疫情以来,AI 技术加速进入了人们的视线,在抗疫过程中发挥了重要作用,产业发展明显提速,我国逐步走出了一条由需求导向引领商业模式创新.市场应用倒逼基础理论和关键技术创新的发展道路,AI 人才的争 ...

  4. tengine简单安装_实操丨如何在EAIDK上部署Tengine开发AI应用之物体检测应用入门(C++)...

    前言:近期推出的嵌入式AI系列直播公开课受到广大开发者的喜爱,并收到非常多的反馈信息,其中对如何在EAIDK上面部署Tengine开发AI应用感兴趣的开发者不在少数,我们将分2期以案例实操的形式详细介 ...

  5. 超硬核的 OCR 开发利器推荐:从场景案例到实操演示,快速掌握OCR模型训练

    现如今OCR能力已经在众多场景中落地应用: 像是常见的身份证.驾驶证.行驶证等卡证识别: 又如出租车发票.增值税发票.火车票等财务票据识别: 再如医疗费用结算单.病案首页.保险单等医疗票据识别: -- ...

  6. 小微数字风控必学-冷启动开发风险评分(含实操)

    新产品上线,往往只能使用规则进行审批与授信.能不能拦住风险是一回事,老板报以不信任的目光更使得风控从业人员倍受挫折.我们提供一个迁移学习风险评分开发方案,尝试在冷启动阶段就完成风险评分的开发. 假定某 ...

  7. atom配置python环境_用Python制作网站Django实操与开发环境配置

    上篇文章简单介绍了Django的基础知识,本篇将进入实际操作部分,包括Django的运行环境.开发环境配置与新建项目等内容.由于篇幅原因,笔者不得不把Demo演示放到下一篇文章,望读者(如果有的话)见 ...

  8. 理论+实操: MySQL索引与事务、视图、存储过程(软件开发用的多)、存储引擎MyISAM和InnoDB

    文章目录 一:索引的概念 二:索引的作用 三:索引的分类 3.1 普通索引 3.2 唯一性索引 3.3 主键 3.4 全文索引 3.5 单列索引与多列索引 四:创建索引的原则依据 五:创建索引的方法 ...

  9. 手把手实操系列|贷中逾期风险预测模型开发流程(上)

    序言: 随着新客的获客成本越来越高,贷中客户的管理越来越受到放贷机构的重视,其中包括用户流失预测,营销响应预测,逾期风险预测,额度利率管理等. B卡,又称为行为评为卡,它的作用对象是老客,根据其在账户 ...

最新文章

  1. LintCode: Edit Distance
  2. 在.net中运用HTMLParser解析网页的原理和方法
  3. 安利一个超好用的Pandas数据挖掘分析神器
  4. C/C++浮点数在内存中的存储方式
  5. STL - 底层实现
  6. 7-211 求前缀表达式的值 (25 分)
  7. 百度元老黯然离职是被开除?向海龙回应...
  8. SAP B1 9.1 跟踪某个查询的执行
  9. IS-IS详解(十二)——IS-IS 路由过载、管理标记和主机名映射
  10. cookie、sessionStorage、localStorage的区别
  11. Arcpy 实现批量按掩膜提取
  12. 2-visio使用与卸载
  13. Linux内核源码分析
  14. linux磁盘扩容不影响原数据,linux 升级磁盘后扩容数据盘大小
  15. c语言实现校园疫情防控系统
  16. SitePoint播客#76:邪恶的WordPress主题
  17. Golang观察者设计模式(十九)
  18. 苹果充电器怎么辨别真假_airpods怎么辨别真假?
  19. 在线还原base64编码的图片
  20. 动手深度学习v2 汇聚层pooling 课后习题

热门文章

  1. 漏洞扫描工具Nessus的安装和使用
  2. JS格式化代码和高亮显示
  3. 通过CSS进行SEO优化
  4. python列表,字典,集合
  5. YLKJ-HS300多合一读卡终端技术规格说明书
  6. 湖人不敌灰熊,已经被两连败!
  7. android 传感器 鼠标,这一次,我们聊聊鼠标传感器的事儿
  8. 程序员:“我有三年测试经验” “不,你只是把一年的工作经验用了三年”
  9. bigemap如在在地图上定位(经纬度定位)
  10. centos 6.4 NTP服务器的搭建过程