TodoList –第一次前端和前后端分离尝试

关于

大三学到现在,发现自己的技术栈实在是窄的可怜,出了寥寥几个语言和基础知识,就再没有更多东西了,特别是对于前端,也仅仅是停留在知道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,服务器可以告知浏览器哪些源被授权访问资源。

前后端跨域请求限制
在构建前后端分离的应用时,难免会受到CORS的限制,在向与前端不同源的后端请求资源时,浏览器会向后端发送询问是否运行跨域请求

因此我们需要在后端运行跨域请求,即fastapi部份引用的代码来允许跨域请求