第一章:开发环境准备与项目初始化
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:营造卡片式布局。 - 引入
form和layer模块:为未来扩展(如 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 本地完整测试流程
-
启动应用
python app.py -
功能验证
- 访问首页,界面应为 Layui 风格
- 添加任务:"学习 Flask"
- 添加任务:"练习 Layui"
- 点击"切换",状态应变化(带删除线)
- 搜索"Flask",应只显示相关任务
- 删除一个任务,列表应实时更新
-
数据持久化验证
- 关闭服务器
- 重新运行
python app.py - 之前添加的任务应依然存在
6.3 安全与生产注意事项
虽然本项目为学习用途,但需了解以下生产要点:
| 项目 | 开发环境 | 生产环境建议 |
|---|---|---|
SECRET_KEY |
固定字符串 | 从环境变量读取,强随机值 |
debug 模式 |
True |
必须设为 False |
| 数据库 | SQLite | 改用 PostgreSQL/MySQL |
| 静态文件 | CDN | 使用 Nginx 托管 |
| 部署 | flask run |
Gunicorn + Nginx |