TodoList –第一次前端和前后端分离尝试
- 技术
- 2024-05-01
- 600热度
- 0评论
关于
大三学到现在,发现自己的技术栈实在是窄的可怜,出了寥寥几个语言和基础知识,就再没有更多东西了,特别是对于前端,也仅仅是停留在知道html, css, js三件套上,怎么说也得薅一个框架学一学了,于是掏出了大红大紫大前端框架vue,由于是重点学习前端部分,所以后端采用了最最简单的python框架fastapi来快速搭建一套api
fastapi
一个todolist的后端逻辑我们也使用最简单的写法,我们将一个todo包含id, text, completed三个指令,使用fastapi自带的api文档即可快速查看
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI()
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
middleware_class=CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
)
# 随机生成一个4位ID,保证不和已有的ID重复
import random
def generate_id():
# if db is full, raise an error
db = load_db()
if len(db) == 9000:
raise HTTPException(status_code=500, detail="Database is full")
while True:
id = random.randint(1000, 9999)
if id not in db:
return id
# 模拟存储 ToDo 项的数据库
# {id: [text, completed]}
# 将db写入db.json文件
import atexit
def save_db(db):
with open("db.json", "w") as f:
json.dump(db, f)
atexit.register(save_db)
# 从db.json文件中读取数据
import json
def load_db():
try:
with open("db.json", "r") as f:
return json.load(f)
except:
raise HTTPException(status_code=500, detail="Failed to load database")
# 获取所有的 ToDo 项, 返回一个json数组
@app.get("/todos")
async def get_todos():
db = load_db()
return db
# 获取特定的 ToDo 项
@app.get("/todos/{todo_id}")
async def get_todo(todo_id: int):
db = load_db()
todo_id = str(todo_id)
todo = db.get(todo_id)
if todo is None:
raise HTTPException(status_code=404, detail="ToDo item not found")
return todo
# 创建一个新的 ToDo 项,使用url参数传递text
@app.post("/todos")
async def create_todo(text: str):
db = load_db()
id = generate_id()
db[id] = [text, False]
print(db)
save_db(db)
result = {"id": id, "text": text, "completed": False}
return result
# 更新一个 ToDo 项
@app.put("/todos/{todo_id}")
async def update_todo(todo_id: int):
db = load_db()
print(todo_id)
todo_id = str(todo_id)
todo = db.get(todo_id)
if todo is None:
raise HTTPException(status_code=404, detail="ToDo item not found")
todo[1] = not todo[1]
db[todo_id] = todo
save_db(db)
return todo
# 删除一个 ToDo 项
@app.delete("/todos/{todo_id}")
async def delete_todo(todo_id: int):
db = load_db()
print(todo_id)
todo_id = str(todo_id)
todo = db.get(todo_id)
if todo is None:
raise HTTPException(status_code=404, detail="ToDo item not found")
del db[todo_id]
save_db(db)
return todo
大家只要稍微搜索fastapi的用法就可以看懂这一部分,不过这里还要留一个悬念:
app.add_middleware(
middleware_class=CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
)
这一部份是什么意思呢,我们稍等一下后面说到前端再来解释
vue
本着目标导向的方法,vue3完全不会,教程一点不看,不管了,先安装上再说!
这里我用的是brew的安装方法,vue要使用npm(node.js的包管理工具),所以先安装node.js
brew install node
哎每次安装都觉得brew和apt真是伟大的发明,windows当然也有scoop等工具,但比起brew来说显得还是差了许多,接下来使用npm安装vue-cli即可
npm install -g vue-cli
这时候vue框架就已经安装好啦
vue create todolist
使用 vue create命令就可以创建一个默认的vue项目啦
来看看它的大致结构:
README.md
:通常包含有关项目的信息,如如何使用以及其他相关信息。babel.config.js
:Babel 的配置文件,Babel 是一个工具链,主要用于将 ECMAScript 2015+ 代码转换为向后兼容的 JavaScript 版本。jsconfig.json
:JavaScript 语言特性和项目设置的配置文件。package-lock.json
:自动生成的文件,用于锁定每个软件包的依赖项的版本。有助于确保所有安装是一致的。package.json
:包含有关项目及其依赖项的元数据,以及用于各种任务的脚本,如构建和运行项目。public
:包含直接提供的静态资源的目录。favicon.ico
是在浏览器标签中显示的图标,index.html
是应用程序的主 HTML 文件。src
:包含 Vue.js 应用程序源代码的目录。App.vue
:主 Vue 组件,作为应用程序的入口点。assets
:静态资源的目录,如图像、字体等。components
:应用程序中使用的 Vue 组件的目录。HelloWorld.vue
是 Vue 组件的示例。main.js
:应用程序的入口点,在此初始化 Vue 并挂载主组件。
vue.config.js
:Vue CLI 服务的配置文件。
我们忽略太多的配置文件,主要关注几个文件即可:
main.js:是这个程序的入口点,它导入Vue框架并创建主组件:App.vue
App.vue: 这个前端的主组件,通俗来说就是整个界面,比如整个博客页面就是一个主组件
Components:组件文件夹,页面里每一个小部分就是一个组件,比如上图中的文章导航是一个组件,右边文章列表是一个组件,顶部是一个组件,这个文件家里可以创建多个.vue文件,每一个都代表一个组件,可以将组件使用<Name />的形式嵌入主组件,当然组件也可以相互嵌套
前端调试
在文件package.json中定义了vue的启动指令serve,build为项目打包,lint为错误修复,可以使用
npm run serve
命令运行前端服务进行调试
当然在vscode中如果安装了vue插件,也可以直接点击调试启动
启动后就可以看到其默认运行在8080端口,访问localhost:8080即可看到页面,默认是vue的欢迎界面
编写todolist组件
在todolist中我们还使用了Axios进行异步请求
目前做好了配置的准备,我们就新建一个TodoList.vue组件开始编写吧:
我们知道最基础的html css js三件套分别定义了一个页面的结构,布局和逻辑,在一个vue组件里也同样包含了这三个部份,但是表达方式有一些不同,我们使用<template><script><style>三个标签来代表这三个部份,下面我们就来细说我是怎么写这三部份的
Template
<template>
<div class="todo-list">
<input type="text" v-model="newTodo" @keyup.enter="addTodo" placeholder="Add a new task" class="todo-input">
<ul class="todo-items">
<li v-for="todo in todos" :key="todo.id" class="todo-item">
<input type="checkbox" v-model="todo.completed" @click="toggleTodo(todo)" class="todo-checkbox">
<span :class="{ completed: todo.completed }" class="todo-text">{{ todo.text }}</span>
<button @click="deleteTodo(todo)" class="todo-delete">Delete</button>
</li>
</ul>
</div>
</template>
<input type="text" v-model="newTodo" @keyup.enter="addTodo" placeholder="Add a new task" class="todo-input">
这是一个文本输入框,用于输入新的待办任务。它与newTodo
属性使用v-model
指令双向绑定,这意味着当输入框中的内容改变时,newTodo
属性也会相应地更新。@keyup.enter
监听键盘事件,当用户按下回车键时触发addTodo
方法,用于添加新的任务。placeholder
属性设置了输入框的占位符文本。
<ul class="todo-items">
<li v-for="todo in todos" :key="todo.id" class="todo-item">
<input type="checkbox" v-model="todo.completed" @click="toggleTodo(todo)" class="todo-checkbox">
<span :class="{ completed: todo.completed }" class="todo-text">{{ todo.text }}</span>
<button @click="deleteTodo(todo)" class="todo-delete">Delete</button>
</li>
</ul>
这部分是待办事项列表,使用了v-for
指令迭代todos
数组中的每个待办事项,并为每个待办事项创建一个列表项(<li>
)。:key="todo.id"
用于为每个列表项设置唯一的键,以便Vue.js能够有效地跟踪列表项的变化。
列表项包含了以下内容:
- 复选框:用于标记任务的完成状态。使用
v-model
指令与todo.completed
属性双向绑定,以便在复选框状态改变时更新任务的完成状态。@click
监听复选框的点击事件,当用户点击复选框时触发toggleTodo
方法,用于切换任务的完成状态。 - 任务文本:显示待办任务的文本内容。通过
{{ todo.text }}
插值表达式显示每个待办任务的文本内容。使用:class
动态绑定类,根据任务是否完成,设置了不同的样式,包括文本划线和颜色变化。 - 删除按钮:用于删除任务。当用户点击删除按钮时,触发
deleteTodo
方法,用于从待办事项列表中删除相应的任务。
Script
import axios from 'axios';
export default {
name: 'TodoList',
data() {
return {
newTodo: '',
todos: []
};
},
methods: {
async fetchTodos() {
const response = await axios.get('url/todos');
this.todos = Object.entries(response.data).map(([id, [text, completed]]) => ({
id: Number(id),
text: text,
completed: completed
}));
},
addTodo() {
if (this.newTodo.trim() === '') {
return;
}
axios.post('url/todos?text='+this.newTodo).then(response => {
this.todos.push({
id: response.data.id,
text: response.data.text,
completed: false
});
this.newTodo = '';
}).catch(error => {
console.error(error);
});
// clear input
this.newTodo = '';
},
toggleTodo(todo) {
axios.put(`url/todos/${todo.id}`).then(() => {
this.fetchTodos();
}).catch(error => {
console.error(error);
});
},
deleteTodo(todo) {
axios.delete(`url/todos/${todo.id}`).then(() => {
this.todos = this.todos.filter(t => t.id !== todo.id);
}).catch(error => {
console.error(error);
})
}
},
created() {
this.fetchTodos();
}
}
export default {
name: 'TodoList',
// ...
}
通过export default
语法,将这个组件导出,使其能够在其他地方引用。name
属性指定了组件的名称,这个名称在使用组件时会被用到。
在data
函数中,我们定义了组件的数据。newTodo
用于存储用户输入的新任务文本,todos
则用于存储所有的待办事项。
在methods
对象中定义了组件的方法:
await
是 JavaScript 中用于等待一个异步操作完成的关键字。在异步函数内部,await
可以用于等待一个返回 Promise 对象的表达式,并暂停异步函数的执行,直到该 Promise 对象状态变为 resolved(已完成)或 rejected(已拒绝)。具体来说,在这个组件中,
await
用于等待 Axios 发送的 HTTP 请求完成,并且获取到响应数据。在fetchTodos()
方法中,await
用于等待axios.get()
方法返回的 Promise 对象,这个 Promise 对象表示对服务器的 GET 请求。只有当该请求成功返回了数据(Promise 对象状态变为 resolved)时,await
才会获取到这个数据,并将其赋值给response
变量。通过
await
关键字,我们可以使异步代码看起来更像同步代码,提高了代码的可读性。在等待异步操作完成期间,JavaScript 引擎会自动暂停当前的异步函数的执行,直到异步操作完成,然后再继续执行后续的代码。
fetchTodos()
:异步方法,用于从服务器获取待办事项列表。addTodo()
:用于添加新的待办任务。toggleTodo(todo)
:用于切换任务的完成状态。deleteTodo(todo)
:用于删除指定的待办任务。
这些方法将在用户与组件交互时被调用,执行相应的逻辑操作,例如向服务器发送HTTP请求、更新组件的数据等。
created
生命周期钩子在组件被创建后立即调用,这里我们在组件创建后立即调用fetchTodos()
方法,以获取初始的待办事项列表。
Axios库用于处理HTTP请求。在fetchTodos()
方法中,我们使用Axios发送GET请求来获取服务器上的待办事项列表。
Style
.todo-list {
max-width: 400px;
margin: 0 auto;
padding: 20px;
}
.todo-input {
width: calc(100% - 80px);
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 8px;
}
.todo-items {
list-style-type: none;
padding: 0;
}
.todo-item {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todo-checkbox {
margin-right: 10px;
}
.todo-text {
flex-grow: 1;
}
.todo-delete {
background-color: #ff6347;
color: #fff;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
.todo-text.completed {
text-decoration: line-through;
color: #aaa;
}
style则和css大差不差了
一个注意点
现在我们来说说前端部分留下的一个悬念
CORS(跨域资源共享)是一种网络安全机制,用于在浏览器和服务器之间进行跨域通信。在Web开发中,由于安全原因,浏览器会阻止从一个源(域、协议、端口)加载的页面或脚本访问另一个源的数据。CORS允许服务器指定哪些源可以访问其资源,以及哪些HTTP方法(如GET、POST)是允许的。通过在服务器响应中包含特定的HTTP头部,如
Access-Control-Allow-Origin
,服务器可以告知浏览器哪些源被授权访问资源。
因此我们需要在后端运行跨域请求,即fastapi部份引用的代码来允许跨域请求