Flask 入门实战:路由、模板与 SQLite 增删改查完整 Demo 简介这是一份面向Python Web开发初学者的Flask入门实践项目以“试验楼小Demo”的形式呈现一个简易博文系统涵盖用户登录登出、博文发布与查看等核心功能适合刚接触Web开发、希望从零理解请求处理流程与项目结构的学习者动手练习。压缩包共8个文件约5KB以html模板、py主程序、sql建表脚本、db数据库文件及css样式为主另有pyc编译文件体量轻巧却覆盖了Flask路由与视图函数、Jinja2模板渲染、SQLite3数据存储、session身份验证、表单处理及错误处理等关键知识点目录结构清晰便于对照理解各模块职责。该资源已有3088人学习下载读者可借此完整走通一个轻量Web应用从建表、编写视图到页面展示的基本链路并参考其中的表结构设计与模板组织方式为后续扩展功能或搭建自己的小项目积累可复用的实践思路。1. 一个 Flask 试验楼 Demo从零把请求、模板、SQLite 串起来很多人学 Flask 卡在一个尴尬的位置官方教程能跑通但一合上文档就不知道从哪下手。这个试验楼小 Demo 就是冲着这个痛点来的——它不是完整业务系统而是一套自己敲出来的最小可运行骨架把路由注册、Jinja2 模板渲染、SQLite 增删改查、表单提交这几件事串成一条能跑通的链路。适合刚接触 Python web 开发、想找一个能改能拆的起点的人也适合熟手拿它当快速验证某个想法的脚手架。下面按“它是什么、怎么跑起来、哪里会翻车”的顺序拆代码都能直接抄。2. 拆开这个 Demo 的骨架路由、模板与 SQLite 三件套2.1 目录结构与依赖先让项目能启动拿到一个 Flask 项目第一件事不是读代码而是看目录和依赖。这个 Demo 的结构很朴素常见做法是长这样flask-demo/ ├── app.py # 入口创建 app、注册路由 ├── db.py # SQLite 连接与初始化 ├── schema.sql # 建表语句 ├── templates/ │ ├── base.html # 公共布局 │ ├── index.html # 列表页 │ └── edit.html # 编辑页 ├── static/ │ └── style.css └── requirements.txt依赖只有两个核心包requirements.txt里写清楚版本区间避免不同机器装出不同行为Flask2.3,3.0SQLite 是 Python 标准库自带的sqlite3不需要额外装。虚拟环境建好之后python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt这里有个容易被忽略的点app.py里创建应用时模板目录和静态目录默认就是templates/和static/如果你改了目录名必须显式传template_folder和static_folder否则启动不报错、访问 404排查起来很费时间。启动命令用flask --app app run --debug--debug打开后改代码自动重载开发期省事但上线前一定关掉。2.2 路由与视图函数请求进来之后发生了什么Flask 的核心心智模型很简单一个 URL 对应一个函数函数返回字符串或渲染后的模板。这个 Demo 的列表页和新增接口大概是这样from flask import Flask, render_template, request, redirect, url_for import db app Flask(__name__) app.route(/) def index(): # 查询全部记录交给模板渲染 items db.get_all_items() return render_template(index.html, itemsitems) app.route(/add, methods[POST]) def add_item(): # 表单字段名必须和模板里 input 的 name 一致 title request.form.get(title, ).strip() if not title: return 标题不能为空, 400 db.insert_item(title) return redirect(url_for(index))逻辑说明index只做查询和渲染不掺业务判断add_item只接受 POST取表单字段、做最基础的非空校验、写库、重定向回列表页。重定向用url_for而不是硬编码路径改路由时不会漏改。参数上request.form.get第二个参数给默认值避免字段缺失时抛KeyErrormethods不写默认只允许 GETPOST 请求会直接 405这是新手最常见的翻车点之一。2.3 SQLite 连接管理为什么不能每个请求都开新连接SQLite 是文件数据库连接开销小但也不是随便开。这个 Demo 用db.py统一封装常见做法是把连接挂在 Flask 的应用上下文g上请求结束自动关闭import sqlite3 from flask import g DATABASE demo.db def get_db(): # 每个请求复用同一个连接避免频繁开关 if db not in g: g.db sqlite3.connect(DATABASE) 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_app(app): app.teardown_appcontext(close_db)row_factory sqlite3.Row这一行值得单独说不设的话查询结果是元组模板里只能写item[0]设了之后可以写item[title]可读性和抗改表能力都强很多。teardown_appcontext保证每个请求结束连接被回收不会出现连接泄漏。建表语句放在schema.sql初始化时执行一次即可不要每次启动都CREATE TABLE否则数据被反复覆盖。2.4 模板渲染与表单Jinja2 的两个高频用法模板层是这个 Demo 里最容易被低估的部分。base.html定义公共骨架子模板用{% extends %}继承块用{% block %}占位!-- base.html -- !doctype html html headlink relstylesheet href{{ url_for(static, filenamestyle.css) }}/head body {% block content %}{% endblock %} /body /html列表页循环渲染时{{ item[title] }}会自动做 HTML 转义这是 Jinja2 默认行为别为了“显示原样”去加|safe那等于自己拆掉 XSS 防线。表单提交到/addinput的name必须和视图里request.form.get的键一致不一致的表现是“提交成功但数据是空的”不报错纯靠对字段名。这一层没有玄学就是字段名对齐和转义默认值两件事。3. 从建表到增删改查把一条数据完整走通3.1 建表与初始化schema 只跑一次建表语句建议单独放文件字段类型按 SQLite 的动态类型来别照搬 MySQL 那套-- schema.sql DROP TABLE IF EXISTS items; CREATE TABLE items ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime(now, localtime)) );done用 0/1 存布尔SQLite 没有原生布尔类型这是通行做法。created_at用默认值在插入时自动填省得应用层每次拼时间字符串。初始化脚本单独写一个init_db.py只在需要重置时手动跑import sqlite3 conn sqlite3.connect(demo.db) with open(schema.sql, encodingutf-8) as f: conn.executescript(f.read()) conn.commit() conn.close()注意executescript会隐式提交之前的事务别把它塞进业务事务里。另外DROP TABLE IF EXISTS意味着这个脚本是“重置”语义生产环境千万别这么写Demo 里图省事可以。3.2 增删改查四个函数参数化查询是底线db.py里补齐四个操作全部用参数化占位符?不要用字符串拼接def get_all_items(): rows get_db().execute(SELECT * FROM items ORDER BY id DESC).fetchall() return rows def insert_item(title): db get_db() db.execute(INSERT INTO items (title) VALUES (?), (title,)) db.commit() def toggle_item(item_id): db get_db() db.execute(UPDATE items SET done 1 - done WHERE id ?, (item_id,)) db.commit() def delete_item(item_id): db get_db() db.execute(DELETE FROM items WHERE id ?, (item_id,)) db.commit()1 - done是个小技巧一行完成布尔翻转不用先查再改。参数化查询不只是防注入还能让 SQLite 复用查询计划批量插入时性能差别明显。每个写操作后必须commit()忘了提交的表现是“接口返回 200 但数据没进库”这种坑查起来最费劲。删除和翻转接口建议用 POST 而不是 GETGET 被浏览器预取或爬虫命中会误删数据。3.3 用 curl 验证接口不依赖浏览器先跑通链路页面还没调好之前先用命令行验证后端逻辑能快速定位问题出在视图还是模板# 新增一条 curl -X POST -d title第一条测试 http://127.0.0.1:5000/add -i # 查看列表页返回的 HTML curl http://127.0.0.1:5000/ | grep 第一条测试-i把响应头一起打出来能看到 302 重定向和Location确认跳转逻辑对。如果 POST 返回 400先看字段名返回 405看methods返回 500看终端堆栈。这套顺序比在浏览器里点来点去快得多尤其是模板报错被吞掉的时候。4. 避坑排查这个 Demo 最容易翻车的五个地方4.1 现象改了模板刷新没变化原因通常是 Jinja2 模板缓存或者--debug没开。解决开发期用flask --app app run --debug启动模板改动会自动失效缓存如果仍不生效检查是不是起了多个进程改的那个没被访问到。4.2 现象数据库报database is locked原因是有连接没关闭或者长事务占着写锁。解决确认teardown_appcontext注册了close_db写操作后及时commit不要在一个请求里开多个连接同时写。SQLite 同一时刻只允许一个写者这是它的设计边界不是 bug。4.3 现象中文写入后变成乱码或问号原因是连接或文件编码不一致。解决sqlite3.connect默认按 UTF-8 处理问题多出在读schema.sql时没指定encodingutf-8以及终端本身编码不对。统一在打开文件时显式写encodingutf-8能消掉大部分乱码。4.4 现象表单提交后列表页看不到新数据原因是重定向到了缓存页或者查询用了旧连接。解决确认insert_item里commit了确认列表查询走的是同一个get_db()浏览器强刷一次排除前端缓存。如果 curl 能看到数据而浏览器看不到基本就是缓存问题。4.5 现象部署后静态资源 404原因是开发期用 Flask 自带服务器静态文件由它托管换到别的 WSGI 服务器后静态路径规则变了。解决确认url_for(static, filename...)写法没硬编码路径生产环境让前置服务器直接托管static/目录Flask 只处理动态请求。5. 把它改成你自己的试验楼三个可落地的扩展点这个 Demo 的价值不在功能而在它足够小小到你敢改。第一个扩展点是加一层简单的数据校验现在只判空可以引入wtforms或者手写一个validate_title把长度、非法字符挡在写库之前避免脏数据进 SQLite 后再清洗。第二个扩展点是分页列表页数据一多就卡SELECT ... LIMIT ? OFFSET ?配合request.args.get(page, 1, typeint)就能加上注意typeint会在参数非法时回退默认值比手动int()安全。第三个扩展点是把这个骨架接到真实场景比如把items换成你自己的业务表路由按资源拆成蓝图Blueprintapp.py只负责注册代码量上去之后不会乱成一锅粥。验证改动是否可靠我一般会固定跑一遍这条链路init_db.py重置库 →curl新增两条 → 浏览器看列表 → 翻转一条 → 删除一条 → 再curl确认库内状态。这套动作五分钟能跑完但能挡住八成“看起来好了其实没写进去”的问题。血泪经验是别在模板里写业务判断别在视图里拼 SQL别在生产环境跑DROP TABLE。从那以后我每次动数据库相关代码都强制先跑一遍重置加增删改查的闭环再谈页面好不好看。希望帮到你。本文还有配套的精品资源点击获取