
简介这份资源是一个基于 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 加 sqlite32.1 技术栈的取舍逻辑这个 Demo 的技术栈非常克制后端只有 Flask数据库直接用 Python 标准库自带的 sqlite3模板用 Jinja2Flask 默认集成前端就是原生 HTML 少量 CSS没有引入任何前端框架和 ORM。这种组合的好处是依赖极少pip install flask之后基本就能跑不需要装数据库服务、不需要配连接池、不需要迁移工具。为什么不上 SQLAlchemy在这个体量下ORM 带来的抽象成本大于收益。一个试验楼 Demo 的表结构通常就两三张表字段十几个手写 SQL 反而更直观也更容易让新手看清「一次请求到底对数据库做了什么」。常见做法是把数据库连接封装成一个get_db()函数配合 Flask 的g对象做请求级复用请求结束自动关闭——这个模式在 Flask 官方文档里也有属于这个场景下最稳妥的写法。为什么用 sqlite3 而不是 MySQL因为 sqlite3 是文件型数据库整个库就是一个.db文件拷走就能带走删掉就能重置对试验性项目来说这是巨大的便利。代价是并发写入能力弱但一个内部小工具、一个练手 Demo根本碰不到这个瓶颈。2.2 目录结构长什么样一个能长期维护的 Flask 小项目目录一般会拆成下面这样而不是把所有代码塞进一个app.pyflask-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里通常只有一行核心依赖Flask2.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.sqlimport sqlite3 from flask import g, current_app def get_db(): 获取当前请求的数据库连接不存在则创建 if db not in g: g.db sqlite3.connect( current_app.config[DATABASE], detect_typessqlite3.PARSE_DECLTYPES ) # 让查询结果支持按列名访问如 row[title] g.db.row_factory sqlite3.Row return g.db def close_db(eNone): 请求结束时关闭连接 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, itemsrows) 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 langzh head meta charsetutf-8 title{% block title %}试验楼 Demo{% endblock %}/title link relstylesheet href{{ url_for(static, filenamestyle.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 classflash{{ 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_iditem[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_iditem[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省去手动校验。如果删除不存在的 idexecute不会报错只是影响行数为 0需要的话可以用cursor.rowcount判断。5. 避坑与常见问题那些跑不通时该看哪里5.1 现象启动报ModuleNotFoundError: No module named flask原因几乎都是虚拟环境没激活或者pip install装到了另一个 Python 解释器。解决先which pythonWindows 用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 todocreate视图里从request.form.get(status)取值并加进 INSERTform.html里加一个select namestatusindex.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, itemsrows)参数说明request.args.get读的是 URL 查询串和表单的request.form要分清f%{keyword}%里的百分号是 SQL 通配符但值本身仍然通过参数化传入注入风险由驱动处理。数据量上千条以后LIKE会变慢那时候再考虑 sqlite 的 FTS5 扩展但那是另一个话题了。还有一个容易被忽略的改造点把app.py拆成蓝图。当路由超过十条单文件会变得难读。做法是建views/目录每个业务模块一个蓝图文件app.py里只负责register_blueprint。这个改动不影响功能但决定了这个 Demo 能不能长成一个正经项目。最后说个我自己的习惯每次改完数据库相关代码我都会先删库、重跑init-db、再手动走一遍增删改查而不是直接接着旧数据测。因为旧数据里往往残留着上一版表结构的痕迹跳过重置这一步很多字段错位的问题会被掩盖到上线才爆。这个动作花不了两分钟但省下的排查时间远不止。希望帮到你。本文还有配套的精品资源点击获取