Python Web 全栈开发实战教程:基于 Flask 与 Layui 的待办事项系统

第一章:开发环境准备与项目初始化

1.1 创建项目目录结构

良好的项目结构是可维护性的基础。在你的工作区(如 ~/projects)创建如下目录:

复制代码
mkdir flask-todo-layui
cd flask-todo-layui

初始结构如下(后续会逐步完善):

复制代码
flask-todo-layui/
├── app.py                 # 主应用入口(初期)
├── requirements.txt       # 依赖清单(后期生成)
├── todos.db               # SQLite 数据库文件(运行后自动生成)
└── templates/             # HTML 模板目录

1.2 创建并激活虚拟环境

虚拟环境隔离项目依赖,避免版本冲突。

复制代码
# 创建虚拟环境
python -m venv venv

# 激活(根据操作系统选择)
# Windows:
venv\Scripts\activate

# macOS / Linux:
source venv/bin/activate

激活成功后,命令行前缀会出现 (venv)

1.3 安装核心依赖

本项目仅需两个核心包:

  • Flask:Web 框架
  • Flask-SQLAlchemy:数据库 ORM 工具

执行安装:

复制代码
pip install Flask Flask-SQLAlchemy

验证安装:

复制代码
python -c "import flask, flask_sqlalchemy; print('OK')"

若无报错,说明安装成功。

1.4 初始化 Git(可选但推荐)

版本控制是专业开发的标配。

复制代码
git init
echo "venv/" > .gitignore
echo "__pycache__/" >> .gitignore
echo "*.pyc" >> .gitignore
echo "todos.db" >> .gitignore  # 数据库含敏感数据,不提交

第二章:Flask 核心机制与基础功能实现

2.1 理解 Flask 应用对象

Flask 应用的核心是一个 Flask 类的实例。它负责:

  • 注册 URL 路由
  • 管理配置
  • 处理请求与响应

创建 app.py,写入最简结构:

复制代码
from flask import Flask

# 创建 Flask 应用实例
app = Flask(__name__)

# 设置密钥(用于 session 等,此处暂不使用,但建议设置)
app.secret_key = 'your-secret-key-change-in-production'

if __name__ == '__main__':
    # 启用调试模式:代码修改自动重启,错误页面详细
    app.run(debug=True)

此时运行 python app.py,会启动一个空服务(访问会 404),因为我们尚未定义任何路由。

2.2 实现第一个路由:首页

路由(Route)将 URL 映射到 Python 函数(视图函数)。

复制代码
from flask import Flask, render_template

app = Flask(__name__)
app.secret_key = 'dev-secret'

@app.route('/')  # 装饰器:将根路径 '/' 绑定到 index 函数
def index():
    return "Hello from Flask + Layui!"

if __name__ == '__main__':
    app.run(debug=True)

运行后访问 http://127.0.0.1:5000,看到文字即成功。

2.3 引入模板渲染

直接返回字符串无法构建复杂页面。Flask 内置 Jinja2 模板引擎,支持动态内容插入。

创建模板目录
复制代码
mkdir templates
编写基础 HTML 模板

新建 templates/index.html

复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>待办事项</title>
</head>
<body>
    <h1>欢迎使用待办事项系统</h1>
    <p>当前共有 {{ count }} 个任务。</p>
</body>
</html>

{``{ count }} 是 Jinja2 的变量占位符。

修改视图函数传递数据
复制代码
@app.route('/')
def index():
    # 暂时模拟任务数量
    task_count = 0
    return render_template('index.html', count=task_count)

render_template() 自动在 templates 目录查找文件,并将关键字参数传入模板。


第三章:Layui 前端框架深度集成

3.1 为什么选择 Layui?

  • 国产开源:文档中文,社区活跃,符合国内开发者习惯。
  • 开箱即用:CSS/JS 一体化,无需复杂构建工具。
  • 组件丰富:表单、表格、弹层、日期等常用组件齐全。
  • 轻量简洁:适合管理后台、内部工具类项目。

官网:https://www.layui.site(原 layui.com 已停止维护,推荐使用社区维护版)

注意:本教程使用 CDN 方式引入,生产环境建议下载到本地。

3.2 构建 Layui 基础模板

为避免重复代码,采用 模板继承 模式。创建 templates/base.html

复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}待办事项系统{% endblock %}</title>
    
    <!-- 引入 Layui CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/layui@2.9.8/dist/css/layui.css">
    
    <!-- 自定义样式 -->
    <style>
        body {
            background-color: #f5f7fa;
        }
        .container {
            max-width: 800px;
            margin: 30px auto;
            padding: 20px;
            background: #fff;
            border-radius: 8px;
            box-shadow: 0 2px 12px rgba(0,0,0,0.1);
        }
        .task-item {
            padding: 12px 0;
            border-bottom: 1px dashed #e6e6e6;
        }
        .task-done {
            text-decoration: line-through;
            color: #999;
        }
        .task-time {
            font-size: 12px;
            color: #999;
            margin-left: 10px;
        }
        .task-actions {
            float: right;
        }
        .empty-tips {
            text-align: center;
            color: #999;
            padding: 40px 0;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1 class="layui-h1">{% block header %}{% endblock %}</h1>
        
        <!-- 子模板内容区域 -->
        {% block content %}{% endblock %}
    </div>

    <!-- 引入 Layui JS -->
    <script src=" https://cdn.jsdelivr.net/npm/layui@2.9.8/dist/layui.js"></script>
    
    <!-- 全局 JS 配置 -->
    <script>
        // 初始化 Layui 模块
        layui.use(['form', 'layer'], function(){
            var form = layui.form;
            var layer = layui.layer;
            
            // 表单提交监听(可选,本项目用传统提交)
            // form.on('submit(add)', function(data){
            //     console.log(data.field);
            //     return false; // 阻止默认提交
            // });
        });
    </script>
    
    <!-- 子模板可追加的 JS -->
    {% block scripts %}{% endblock %}
</body>
</html>

关键点说明

  • {% block ... %}:定义可被子模板覆盖的区域。
  • layui-h1:Layui 提供的标题样式。
  • 自定义 .container:营造卡片式布局。
  • 引入 formlayer 模块:为未来扩展(如 AJAX、弹窗)做准备。

3.3 重构首页模板

修改 templates/index.html,继承 base.html

复制代码
{% extends "base.html" %}

{% block header %}我的待办事项{% endblock %}

{% block content %}
<!-- 添加任务表单 -->
<form class="layui-form" method="POST" action="/add">
    <div class="layui-form-item">
        <div class="layui-input-block" style="margin-left: 0;">
            <div class="layui-input-inline" style="width: 500px;">
                <input type="text" name="title" required lay-verify="required" 
                       placeholder="请输入任务内容(按回车快速添加)" 
                       autocomplete="off" class="layui-input">
            </div>
            <button class="layui-btn" lay-submit type="submit">添加任务</button>
        </div>
    </div>
</form>

<!-- 搜索区域 -->
<form class="layui-form" method="GET" style="margin: 20px 0;">
    <div class="layui-form-item">
        <label class="layui-form-label">搜索</label>
        <div class="layui-input-inline" style="width: 300px;">
            <input type="text" name="q" value="{{ search_query or '' }}" 
                   placeholder="输入关键词" autocomplete="off" class="layui-input">
        </div>
        <button class="layui-btn layui-btn-primary" type="submit">搜索</button>
        {% if search_query %}
        <a href="/" class="layui-btn layui-btn-sm">清空</a>
        {% endif %}
    </div>
</form>

<!-- 任务列表 -->
{% if todos %}
    {% for todo in todos %}
    <div class="task-item">
        <div style="overflow: hidden;">
            {% if todo.done %}
                <span class="task-done">{{ todo.title }}</span>
            {% else %}
                <span>{{ todo.title }}</span>
            {% endif %}
            <span class="task-time">{{ todo.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
            
            <div class="task-actions">
                <a href="/toggle/{{ todo.id }}" class="layui-btn layui-btn-xs layui-btn-normal">切换</a>
                <a href="/delete/{{ todo.id }}" class="layui-btn layui-btn-xs layui-btn-danger"
                   onclick="return confirm('确定要删除「{{ todo.title }}」吗?')">删除</a>
            </div>
        </div>
    </div>
    {% endfor %}
{% else %}
    <div class="empty-tips">
        {% if search_query %}
            <p>未找到包含 "{{ search_query }}" 的任务</p>
        {% else %}
            <p>暂无任务,快去添加吧!</p>
        {% endif %}
    </div>
{% endif %}
{% endblock %}

Layui 组件说明

  • layui-form:启用表单样式和验证。
  • lay-verify="required":Layui 内置必填验证。
  • layui-btn-*:不同颜色的按钮样式。
  • layui-form-label / layui-input-inline:表单标签与输入框布局。

第四章:数据库集成与完整 CRUD 功能

4.1 配置 SQLAlchemy

SQLAlchemy 是 Python 最流行的 ORM(对象关系映射)工具,让我们用 Python 类操作数据库。

app.py 中添加配置:

复制代码
from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import os

app = Flask(__name__)
app.secret_key = 'dev-secret'

# === 数据库配置 ===
# 使用 SQLite,数据库文件存于项目根目录
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'todos.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False  # 关闭无用警告

# 创建数据库对象
db = SQLAlchemy(app)

4.2 定义数据模型(Model)

模型是 Python 类,对应数据库中的表。

复制代码
# === 数据模型 ===
class Todo(db.Model):
    __tablename__ = 'todos'  # 表名(可选,默认为类名小写复数)
    
    id = db.Column(db.Integer, primary_key=True)          # 主键
    title = db.Column(db.String(100), nullable=False)     # 任务标题,最大100字符
    done = db.Column(db.Boolean, default=False)           # 是否完成,默认否
    created_at = db.Column(db.DateTime, default=datetime.utcnow)  # 创建时间

    def __repr__(self):
        return f'<Todo {self.id}: {self.title}>'

字段说明:

  • db.Integer:整数
  • db.String(100):可变长字符串,最大100字符
  • nullable=False:不允许为空
  • default=datetime.utcnow:插入时自动填充当前 UTC 时间

4.3 初始化数据库

首次运行需创建表。在 app.py 末尾添加:

复制代码
# === 初始化数据库(仅开发时使用)===
with app.app_context():
    db.create_all()  # 创建所有继承 db.Model 的表

重要with app.app_context(): 是 Flask 2.0+ 的要求,确保在应用上下文中操作数据库。

运行 python app.py 后,项目目录将生成 todos.db 文件。

4.4 实现 CRUD 功能

C - Create(创建任务)
复制代码
@app.route('/add', methods=['POST'])
def add_todo():
    title = request.form.get('title', '').strip()
    if title:  # 非空才添加
        new_todo = Todo(title=title)
        db.session.add(new_todo)
        db.session.commit()
    return redirect(url_for('index'))  # 重定向回首页
  • request.form.get('title', ''):安全获取表单数据,避免 KeyError。
  • db.session.commit():提交事务,写入数据库。
R - Read(读取任务列表)

更新首页路由,从数据库读取数据:

复制代码
@app.route('/')
def index():
    # 获取搜索关键词
    query = request.args.get('q', '').strip()
    
    if query:
        # 模糊搜索:标题包含关键词
        todos = Todo.query.filter(Todo.title.contains(query)).all()
    else:
        # 查询所有任务,按创建时间倒序(新任务在前)
        todos = Todo.query.order_by(Todo.created_at.desc()).all()
    
    return render_template('index.html', todos=todos, search_query=query)
  • request.args:获取 URL 查询参数(如 ?q=学习)。
  • Todo.query.filter(...).all():执行查询。
  • order_by(Todo.created_at.desc()):按时间倒序排列。
U - Update(更新任务状态)
复制代码
@app.route('/toggle/<int:todo_id>')
def toggle_todo(todo_id):
    todo = Todo.query.get_or_404(todo_id)  # 不存在则返回 404
    todo.done = not todo.done
    db.session.commit()
    return redirect(url_for('index'))
  • <int:todo_id>:URL 变量,自动转换为整数。
  • get_or_404():便捷方法,找不到时自动返回 404 页面。
D - Delete(删除任务)
复制代码
@app.route('/delete/<int:todo_id>')
def delete_todo(todo_id):
    todo = Todo.delete_todo = Todo.query.get_or_404(todo_id)
    db.session.delete(todo)
    db.session.commit()
    return redirect(url_for('index'))

4.5 完整 app.py 代码(当前阶段)

复制代码
from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import os

# === 应用初始化 ===
app = Flask(__name__)
app.secret_key = 'dev-secret'

# === 数据库配置 ===
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'todos.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

# === 数据模型 ===
class Todo(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    done = db.Column(db.Boolean, default=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    def __repr__(self):
        return f'<Todo {self.id}: {self.title}>'

# === 路由 ===
@app.route('/')
def index():
    query = request.args.get('q', '').strip()
    if query:
        todos = Todo.query.filter(Todo.title.contains(query)).all()
    else:
        todos = Todo.query.order_by(Todo.created_at.desc()).all()
    return render_template('index.html', todos=todos, search_query=query)

@app.route('/add', methods=['POST'])
def add_todo():
    title = request.form.get('title', '').strip()
    if title:
        new_todo = Todo(title=title)
        db.session.add(new_todo)
        db.session.commit()
    return redirect(url_for('index'))

@app.route('/toggle/<int:todo_id>')
def toggle_todo(todo_id):
    todo = Todo.query.get_or_404(todo_id)
    todo.done = not todo.done
    db.session.commit()
    return redirect(url_for('index'))

@app.route('/delete/<int:todo_id>')
def delete_todo(todo_id):
    todo = Todo.query.get_or_404(todo_id)
    db.session.delete(todo)
    db.session.commit()
    return redirect(url_for('index'))

# === 初始化数据库 ===
with app.app_context():
    db.create_all()

# === 启动 ===
if __name__ == '__main__':
    app.run(debug=True)

第五章:代码工程化与结构优化

当项目变大,单文件 app.py 会难以维护。我们使用 Blueprint(蓝图) 拆分代码。

5.1 重构项目结构

调整目录如下:

复制代码
flask-todo-layui/
├── app.py                 # 应用工厂入口
├── config.py              # 配置文件
├── models.py              # 数据模型
├── routes/
│   ├── __init__.py
│   └── main.py            # 主蓝图
├── templates/
│   ├── base.html
│   └── index.html
└── requirements.txt

5.2 创建配置文件 config.py

复制代码
import os

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard-to-guess-string'
    SQLALCHEMY_TRACK_MODIFICATIONS = False

class DevelopmentConfig(Config):
    basedir = os.path.abspath(os.path.dirname(__file__))
    SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'todos.db')

# 注册配置
config = {
    'development': DevelopmentConfig,
    'default': DevelopmentConfig
}

5.3 拆分模型到 models.py

复制代码
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

db = SQLAlchemy()

class Todo(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    done = db.Column(db.Boolean, default=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    def __repr__(self):
        return f'<Todo {self.id}: {self.title}>'

5.4 创建主蓝图 routes/main.py

复制代码
from flask import Blueprint, render_template, request, redirect, url_for
from models import db, Todo

main = Blueprint('main', __name__)

@main.route('/')
def index():
    query = request.args.get('q', '').strip()
    if query:
        todos = Todo.query.filter(Todo.title.contains(query)).all()
    else:
        todos = Todo.query.order_by(Todo.created_at.desc()).all()
    return render_template('index.html', todos=todos, search_query=query)

@main.route('/add', methods=['POST'])
def add_todo():
    title = request.form.get('title', '').strip()
    if title:
        new_todo = Todo(title=title)
        db.session.add(new_todo)
        db.session.commit()
    return redirect(url_for('main.index'))

@main.route('/toggle/<int:todo_id>')
def toggle_todo(todo_id):
    todo = Todo.query.get_or_404(todo_id)
    todo.done = not todo.done
    db.session.commit()
    return redirect(url_for('main.index'))

@main.route('/delete/<int:todo_id>')
def delete_todo(todo_id):
    todo = Todo.query.get_or_404(todo_id)
    db.session.delete(todo)
    db.session.commit()
    return redirect(url_for('main.index'))

5.5 重构 app.py 为应用工厂

复制代码
from flask import Flask
from config import config
from models import db
from routes.main import main as main_blueprint

def create_app(config_name='default'):
    app = Flask(__name__)
    app.config.from_object(config[config_name])

    # 初始化扩展
    db.init_app(app)

    # 注册蓝图
    app.register_blueprint(main_blueprint)

    # 初始化数据库(仅开发)
    with app.app_context():
        db.create_all()

    return app

if __name__ == '__main__':
    app = create_app()
    app.run(debug=True)

优势

  • 配置集中管理
  • 模型与路由解耦
  • 便于未来添加新功能模块(如用户认证蓝图)

第六章:项目收尾与部署准备

6.1 生成依赖清单

在虚拟环境中执行:

复制代码
pip freeze > requirements.txt

典型内容:

复制代码
Flask==3.0.3
Flask-SQLAlchemy==3.1.1
SQLAlchemy==2.0.30
Werkzeug==3.0.3

6.2 本地完整测试流程

  1. 启动应用

    复制代码
    python app.py
  2. 功能验证

    • 访问首页,界面应为 Layui 风格
    • 添加任务:"学习 Flask"
    • 添加任务:"练习 Layui"
    • 点击"切换",状态应变化(带删除线)
    • 搜索"Flask",应只显示相关任务
    • 删除一个任务,列表应实时更新
  3. 数据持久化验证

    • 关闭服务器
    • 重新运行 python app.py
    • 之前添加的任务应依然存在

6.3 安全与生产注意事项

虽然本项目为学习用途,但需了解以下生产要点:

项目 开发环境 生产环境建议
SECRET_KEY 固定字符串 从环境变量读取,强随机值
debug 模式 True 必须设为 False
数据库 SQLite 改用 PostgreSQL/MySQL
静态文件 CDN 使用 Nginx 托管
部署 flask run Gunicorn + Nginx
相关推荐
光影少年18 小时前
vite为什么速度快?
前端·学习
万物得其道者成18 小时前
用 Python + MySQL + Web 打造我的私有 Apple 设备监控面板
前端·python·mysql
Hi_kenyon18 小时前
快速入门VUE与JS(二)--getter函数(取值器)与setter(存值器)
前端·javascript·vue.js
海云前端118 小时前
前端面试加分技巧:文本省略 + Tooltip 优雅实现,附可直接复用代码(求职党必看)
前端
在西安放羊的牛油果18 小时前
浅谈 storeToRefs
前端·typescript·vuex
triumph_passion18 小时前
Zustand 从入门到精通:我的工程实践笔记
前端·性能优化
pusheng202518 小时前
双气联防技术在下一代储能系统安全预警中的应用
前端·安全
C_心欲无痕18 小时前
ts - 交叉类型
前端·git·typescript
彭涛36118 小时前
Blog-SSR 系统操作手册(v1.0.0)
前端