简介:这份资源是一个基于 Python Flask 的轻量级 Web 开发练习项目,面向刚接触 Web 框架的初学者与需要动手巩固基础的开发者,围绕一个简易博文系统展开,涵盖用户登录登出、博文发布与浏览等核心功能,帮助读者理解从路由设计到数据存储的完整开发链路。压缩包共 8 个文件,约 5KB,以 html 模板、py 主程序、sql 建表脚本、db 数据库文件及 css 样式为主,另有编译缓存文件,体量虽小但结构完整,便于快速运行与逐行研读。资源已有 3088 人学习下载,具备一定的参考热度。项目将 Flask 路由与视图函数、Jinja2 模板渲染、SQLite3 数据表设计、session 身份验证及表单处理等知识点串联为可运行实例,读者可据此掌握用户表与博文表的外键关联思路、登录状态跟踪方式以及静态资源组织方法,适合作为入门练手与课程实验的参考样例。
1. 一个 Flask 试验楼小 Demo,能拿来干什么
很多人学 Flask 卡在一个尴尬的位置:官方教程的hello world三行就跑通了,但真让自己从零搭一个带数据库、带页面、带增删改查的小站,又不知道从哪下手。这个「试验楼小 Demo」就是填这个空档的——它不是玩具级的单文件脚本,而是一个用 Python + Flask + sqlite3 手敲出来的完整小项目,把路由、模板、表单、数据库连接、静态资源这几块拼在一起,跑起来就是一个能用的轻量 Web 应用。适合两类人:刚学完 Python 语法想找个真实项目练手的,以及想快速起一个内部小工具原型、不想上 Django 那么重的开发者。下面我按「它长什么样 → 怎么跑起来 → 核心代码怎么写的 → 怎么改」的顺序拆一遍。
2. 项目骨架与技术选型:为什么是 Flask 加 sqlite3
2.1 技术栈的取舍逻辑
这个 Demo 的技术栈非常克制:后端只有 Flask,数据库直接用 Python 标准库自带的 sqlite3,模板用 Jinja2(Flask 默认集成),前端就是原生 HTML + 少量 CSS,没有引入任何前端框架和 ORM。这种组合的好处是依赖极少,pip install flask之后基本就能跑,不需要装数据库服务、不需要配连接池、不需要迁移工具。
为什么不上 SQLAlchemy?在这个体量下,ORM 带来的抽象成本大于收益。一个试验楼 Demo 的表结构通常就两三张表,字段十几个,手写 SQL 反而更直观,也更容易让新手看清「一次请求到底对数据库做了什么」。常见做法是把数据库连接封装成一个get_db()函数,配合 Flask 的g对象做请求级复用,请求结束自动关闭——这个模式在 Flask 官方文档里也有,属于这个场景下最稳妥的写法。
为什么用 sqlite3 而不是 MySQL?因为 sqlite3 是文件型数据库,整个库就是一个.db文件,拷走就能带走,删掉就能重置,对试验性项目来说这是巨大的便利。代价是并发写入能力弱,但一个内部小工具、一个练手 Demo,根本碰不到这个瓶颈。
2.2 目录结构长什么样
一个能长期维护的 Flask 小项目,目录一般会拆成下面这样,而不是把所有代码塞进一个app.py:
flask-demo/ ├── app.py # 应用入口,创建 app、注册路由 ├── config.py # 配置项:数据库路径、密钥等 ├── db.py # 数据库连接与初始化 ├── schema.sql # 建表语句 ├── requirements.txt # 依赖清单 ├── static/ │ ├── style.css # 样式 │ └── app.js # 少量交互脚本 ├── templates/ │ ├── base.html # 基础模板,含导航和公共结构 │ ├── index.html # 列表页 │ ├── form.html # 新增/编辑表单页 │ └── detail.html # 详情页 └── instance/ └── demo.db # sqlite3 数据库文件(运行时生成)这个结构的关键点在于instance/目录:Flask 约定把运行时产生的、不该进版本库的文件放这里,数据库文件放进去最合适。schema.sql单独抽出来,是为了让「建表」这件事可重复执行——换台机器、删库重来,跑一遍初始化命令就行,不用手动进数据库敲 SQL。
2.3 依赖与配置
requirements.txt里通常只有一行核心依赖:
Flask>=2.0如果用到表单校验,可能再加Flask-WTF;如果要做登录,加Werkzeug的密码哈希(其实 Flask 已经间接带了)。但一个纯练手 Demo,我一般建议先不加,把最基础的跑通再说。
config.py里放两类东西:数据库文件路径和SECRET_KEY。后者用于 session 签名,开发阶段随便写个字符串,上线必须换成随机值。常见做法是从环境变量读,读不到给个开发默认值:
import os class Config: # 数据库文件放在 instance 目录下,避免污染源码目录 DATABASE = os.path.join(os.path.dirname(__file__), "instance", "demo.db") # 开发用默认值,生产环境务必通过环境变量覆盖 SECRET_KEY = os.environ.get("SECRET_KEY", "dev-only-key-change-me")参数说明:DATABASE用绝对路径拼接,避免从不同工作目录启动时找不到库文件;SECRET_KEY的os.environ.get写法保证本地不配环境变量也能跑,但注释里必须写清楚生产要换。
3. 跑起来:从零到浏览器看到页面的完整步骤
3.1 环境准备与依赖安装
第一步永远是隔离环境。不要图省事直接往系统 Python 里装,血泪经验是版本冲突排查起来非常痛苦:
# 创建虚拟环境 python -m venv venv # 激活(Linux/macOS) source venv/bin/activate # 激活(Windows) venv\Scripts\activate # 安装依赖 pip install -r requirements.txt激活成功的标志是命令行前面出现(venv)。如果python -m venv报错,多半是系统缺python3-venv包,按提示装上即可。这一步翻车的典型现象是pip install装到了全局环境,后面flask run却提示找不到模块——原因就是虚拟环境没激活。
3.2 初始化数据库
数据库文件不会自动出现,需要先跑一次建表脚本。db.py里通常提供两个函数:get_db()负责拿连接,init_db()负责执行schema.sql:
import sqlite3 from flask import g, current_app def get_db(): """获取当前请求的数据库连接,不存在则创建""" if "db" not in g: g.db = sqlite3.connect( current_app.config["DATABASE"], detect_types=sqlite3.PARSE_DECLTYPES ) # 让查询结果支持按列名访问,如 row["title"] g.db.row_factory = sqlite3.Row return g.db def close_db(e=None): """请求结束时关闭连接""" db = g.pop("db", None) if db is not None: db.close() def init_db(): """执行 schema.sql 建表""" db = get_db() with current_app.open_resource("schema.sql") as f: db.executescript(f.read().decode("utf8"))逻辑说明:g是 Flask 的请求上下文对象,同一次请求内多次调用get_db()拿到的是同一个连接,避免频繁开关连接。row_factory = sqlite3.Row这一行很关键,它让查询结果可以像字典一样用列名取值,模板里写{{ row['title'] }}才不会报错。close_db需要注册到app.teardown_appcontext,保证请求结束自动清理。
初始化命令一般做成 Flask CLI 命令:
flask --app app init-db对应在app.py里注册:
@app.cli.command("init-db") def init_db_command(): init_db() print("数据库初始化完成")schema.sql内容示例:
DROP TABLE IF EXISTS item; CREATE TABLE item ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );注意DROP TABLE IF EXISTS只适合开发阶段反复重置,正式数据千万别这么干。
3.3 启动与验证
flask --app app run --debug--debug开启调试模式,改代码自动重载,出错页面会显示堆栈。启动后访问http://127.0.0.1:5000,能看到列表页就说明链路通了。验证顺序建议是:先看首页是否 200,再点「新增」提交一条数据,最后刷新列表确认数据落库。如果首页 500,先看终端堆栈;如果页面空白但没报错,多半是模板变量名写错,Jinja2 会静默渲染成空。
4. 核心模块拆解:一次请求从进入到返回的完整链路
4.1 路由与视图函数
Flask 的核心就是「URL 映射到函数」。这个 Demo 的典型路由有四条:列表、详情、新增、删除。以列表和新增为例:
from flask import render_template, request, redirect, url_for, flash from db import get_db @app.route("/") def index(): """列表页:查出所有记录,交给模板渲染""" db = get_db() rows = db.execute( "SELECT id, title, created_at FROM item ORDER BY id DESC" ).fetchall() return render_template("index.html", items=rows) @app.route("/create", methods=["GET", "POST"]) def create(): """新增:GET 显示表单,POST 处理提交""" if request.method == "POST": title = request.form.get("title", "").strip() content = request.form.get("content", "").strip() if not title: flash("标题不能为空") return redirect(url_for("create")) db = get_db() db.execute( "INSERT INTO item (title, content) VALUES (?, ?)", (title, content) ) db.commit() return redirect(url_for("index")) return render_template("form.html")逻辑说明:index只做查询和渲染,不掺业务判断;create用同一个路由处理 GET 和 POST,靠request.method分流,这是 Flask 里最省事的写法。参数说明:request.form.get("title", "")的第二个参数是默认值,防止字段缺失时抛KeyError;.strip()去掉首尾空格,避免用户提交纯空格。db.execute的第二个参数用元组传值,这是参数化查询,能防 SQL 注入——千万别用字符串拼接 SQL,这是新手最容易踩的坑。
flash用于一次性消息提示,需要在模板里配合get_flashed_messages()渲染,并且依赖SECRET_KEY。
4.2 模板继承与页面组织
Jinja2 的模板继承是这个 Demo 里最值得学的部分。base.html定义骨架,子模板只填内容块:
<!-- templates/base.html --> <!DOCTYPE html> <html lang="zh"> <head> <meta charset="utf-8"> <title>{% block title %}试验楼 Demo{% endblock %}</title> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> </head> <body> <nav> <a href="{{ url_for('index') }}">首页</a> <a href="{{ url_for('create') }}">新增</a> </nav> <main> {% for msg in get_flashed_messages() %} <p class="flash">{{ msg }}</p> {% endfor %} {% block content %}{% endblock %} </main> </body> </html>子模板index.html只需:
{% extends "base.html" %} {% block content %} <h1>记录列表</h1> <ul> {% for item in items %} <li> <a href="{{ url_for('detail', item_id=item['id']) }}">{{ item['title'] }}</a> <small>{{ item['created_at'] }}</small> </li> {% else %} <li>还没有记录,去新增一条吧。</li> {% endfor %} </ul> {% endblock %}逻辑说明:{% extends %}必须放第一行;url_for用函数名反查 URL,改路由规则时模板不用动,这是比硬编码路径更稳的做法;{% for %}配{% else %}可以在列表为空时给提示,比在视图里判断更干净。参数说明:url_for('detail', item_id=item['id'])里的item_id必须和路由定义@app.route("/item/<int:item_id>")的参数名一致,写错会直接抛BuildError。
4.3 数据库读写与事务
sqlite3 的写操作必须commit()才生效,这是新手最常忘的一步。读操作不需要 commit,但连接要复用。删除操作典型写法:
@app.route("/item/<int:item_id>/delete", methods=["POST"]) def delete(item_id): db = get_db() db.execute("DELETE FROM item WHERE id = ?", (item_id,)) db.commit() flash("已删除") return redirect(url_for("index"))逻辑说明:删除用 POST 而不是 GET,是为了避免浏览器预取或爬虫误删——这是 REST 风格的基本纪律。参数说明:<int:item_id>里的int转换器保证非数字 URL 直接 404,省去手动校验。如果删除不存在的 id,execute不会报错,只是影响行数为 0,需要的话可以用cursor.rowcount判断。
5. 避坑与常见问题:那些跑不通时该看哪里
5.1 现象:启动报ModuleNotFoundError: No module named 'flask'
原因几乎都是虚拟环境没激活,或者pip install装到了另一个 Python 解释器。解决:先which python(Windows 用where python)确认当前解释器路径在venv目录下,再重装依赖。如果用的是 IDE 内置终端,注意 IDE 可能自动切换了解释器,要在设置里手动指到venv。
5.2 现象:页面能打开,但提交表单后数据没进库
原因通常是漏了db.commit(),或者get_db()每次返回了新连接导致写入丢失。解决:检查写操作后是否有 commit;确认get_db()用的是g缓存而不是每次sqlite3.connect。另一个隐蔽原因是schema.sql里表名和 SQL 语句里的表名大小写不一致,sqlite3 对表名大小写不敏感但字段名敏感,字段拼错会直接报no such column。
5.3 现象:模板里{{ item.title }}渲染成空白
原因是用sqlite3.Row时属性访问和字典访问的行为差异,或者根本没设row_factory。解决:统一用item['title']这种字典写法,并在get_db()里确认g.db.row_factory = sqlite3.Row这行没被删。如果查询用了SELECT *而表结构改过,也可能字段对不上,建议查询时显式列出字段。
5.4 现象:改了代码页面没变化
原因多半是没开--debug,或者开了但重载失败(比如语法错误导致进程挂了)。解决:启动命令加--debug,观察终端是否有Detected change in ... reloading日志。如果终端显示重载了但页面还是旧的,强制刷新浏览器清缓存。注意调试模式下不要暴露到公网,错误页面会泄露源码路径。
5.5 现象:flask --app app init-db提示找不到命令
原因是 CLI 命令注册在app创建之后但没被加载,或者--app指向的模块名不对。解决:确认@app.cli.command装饰器写在app = Flask(__name__)之后;确认当前目录下有app.py;如果项目用了工厂函数create_app(),命令要注册在工厂内部,启动时用flask --app app:create_app init-db。
6. 二次开发:把这个 Demo 改成你自己的工具
拿到这个骨架之后,最值钱的改造方向不是加页面,而是把「数据模型」换掉。这个 Demo 的item表是个占位符,你真正要做的是把它替换成自己业务里的实体——比如设备台账、任务清单、读书笔记。改造顺序我一般这么走:先改schema.sql定字段,再改db.py的初始化,然后改视图里的 SQL,最后调模板。这个顺序的好处是每一步都能单独验证,不会出现「页面改了但数据对不上」的连锁翻车。
具体到代码,新增一个字段的完整链路是这样的:schema.sql里给item加一列status TEXT DEFAULT 'todo';create视图里从request.form.get("status")取值并加进 INSERT;form.html里加一个<select name="status">;index.html里显示{{ item['status'] }}。四步改完,重启(调试模式会自动重载),提交一条带状态的数据验证。如果列表页报no such column: status,说明数据库还是旧表结构,删掉instance/demo.db重新跑init-db即可——这也是为什么开发阶段建表语句要带DROP TABLE IF EXISTS。
再往深一层,如果要做搜索,别急着上全文索引,先用LIKE顶住:
@app.route("/search") def search(): keyword = request.args.get("q", "").strip() db = get_db() rows = db.execute( "SELECT id, title FROM item WHERE title LIKE ?", (f"%{keyword}%",) ).fetchall() return render_template("index.html", items=rows)参数说明:request.args.get读的是 URL 查询串,和表单的request.form要分清;f"%{keyword}%"里的百分号是 SQL 通配符,但值本身仍然通过参数化传入,注入风险由驱动处理。数据量上千条以后LIKE会变慢,那时候再考虑 sqlite 的 FTS5 扩展,但那是另一个话题了。
还有一个容易被忽略的改造点:把app.py拆成蓝图。当路由超过十条,单文件会变得难读。做法是建views/目录,每个业务模块一个蓝图文件,app.py里只负责register_blueprint。这个改动不影响功能,但决定了这个 Demo 能不能长成一个正经项目。
最后说个我自己的习惯:每次改完数据库相关代码,我都会先删库、重跑init-db、再手动走一遍增删改查,而不是直接接着旧数据测。因为旧数据里往往残留着上一版表结构的痕迹,跳过重置这一步,很多字段错位的问题会被掩盖到上线才爆。这个动作花不了两分钟,但省下的排查时间远不止。希望帮到你。
本文还有配套的精品资源,点击获取