简介这份资源是一套面向Python Web开发初学者的Flask入门实践Demo围绕一个简易博文系统展开涵盖用户登录登出、博文发布与查看等核心功能适合刚接触Flask、希望从零跑通一个完整小项目来理解Web开发基本流程的读者。压缩包共8个文件约5KB以html模板、py主程序、sql建表脚本、db数据库文件及css样式为主另含一个pyc编译文件体量轻巧便于快速阅读与本地运行。目前已有3088人学习下载说明其作为练手案例具备一定参考价值。内容涉及Flask路由与视图函数、Jinja2模板渲染、SQLite3数据存储、session身份验证、表单处理以及错误处理与日志记录等知识点读者可据此了解用户表与博文表的结构设计思路并对照模板与静态资源组织方式掌握一个轻量Web应用从建表、写路由到页面展示的完整脉络为后续扩展功能打下基础。1. 一个 Flask 试验楼小 Demo能拿来干什么很多人学 Flask 卡在一个尴尬的位置官方教程的hello world三行就跑通了可一旦想做个带登录、带数据库、带增删改查的小系统就不知道从哪下手。这个「试验楼小 Demo」就是冲着这个空档来的——它不是一个玩具级的单文件示例而是一套用 Python Flask sqlite3 搭起来的、结构完整、能直接跑起来的小型 Web 应用骨架。你可以把它当成自己的第一栋「试验楼」楼层不高但水电管线齐全后面想加房间、改户型都有地方下手。它适合两类人。一类是刚学完 Python 基础语法、想摸一摸 Web 开发全流程的新手照着跑一遍就能理解「浏览器发请求 → 路由分发 → 查数据库 → 渲染页面」这条链路到底怎么串起来。另一类是手上有小工具需求、又不想上重型框架的开发者拿它当起点改一改做个内部台账、设备登记、任务清单之类的轻量系统成本很低。sqlite3 是 Python 标准库自带的不用单独装数据库服务这一点对本地试验特别友好。2. 项目骨架与技术选型为什么是 Flask 加 sqlite32.1 技术栈的取舍逻辑先把这个 Demo 的技术选型说清楚因为它决定了后面所有代码的写法。后端用 Flask前端用 Jinja2 模板加少量原生 JavaScript数据层用 Python 标准库里的 sqlite3不引入 ORM。这套组合的核心考虑是「依赖少、看得见」。Flask 本身是微框架路由、请求对象、模板渲染这些核心能力都在但它不替你决定项目怎么组织。这意味着目录结构、数据库连接管理、错误处理这些事得自己写——听起来麻烦其实正是学习价值所在。你在这个 Demo 里写的每一行都能对应到一次真实的 HTTP 请求没有框架魔法挡在中间。sqlite3 直接写 SQL 而不是用 SQLAlchemy也是同样的思路。ORM 会把 SQL 藏起来新手容易「会调 API 不会查数据」。这里每个查询都是明晃晃的SELECT、INSERT参数怎么传、结果怎么取一目了然。代价是要自己防 SQL 注入所以后面代码里你会看到全部用占位符?传参这是硬规矩。提示sqlite3 适合单机、低并发场景。如果后面要多人同时写得换 PostgreSQL 或 MySQL但接口层改动不大因为数据访问都收在一个模块里。2.2 目录结构长什么样一个能长期改的项目目录必须先立住。这个 Demo 的结构大致如下我按实际落地时最顺手的划分来写flask-lab-demo/ ├── app.py # 应用入口创建 app、注册蓝图 ├── config.py # 配置项数据库路径、密钥等 ├── db.py # 数据库连接与初始化 ├── schema.sql # 建表语句 ├── requirements.txt # 依赖清单 ├── blueprints/ │ ├── __init__.py │ ├── auth.py # 登录、注册、登出 │ └── items.py # 业务主模块增删改查 ├── templates/ │ ├── base.html # 公共布局 │ ├── login.html │ ├── register.html │ └── items/ │ ├── list.html │ └── form.html └── static/ ├── style.css └── app.js这个划分的关键在于blueprints目录。Flask 的蓝图Blueprint机制允许你把一组相关路由打包最后在入口统一注册。业务一多如果所有路由都堆在app.py里文件很快上千行改一处要滚半天。按功能拆蓝图是 Flask 项目从 Demo 走向可维护的第一步。db.py单独拎出来是因为数据库连接的获取和释放有讲究。Flask 每个请求是独立上下文连接不能全局共享得挂在g对象上请求结束再关。这个模式后面会细讲。2.3 依赖与启动方式依赖清单很薄requirements.txt里通常就两行核心Flask2.3sqlite3 是标准库不用写。如果你还想加密码哈希可以补一个werkzeug不过 Flask 本身就依赖它直接from werkzeug.security import generate_password_hash就能用。启动方式在开发阶段用 Flask 自带的服务器# 建虚拟环境避免污染全局 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt # 初始化数据库建表 python -c from db import init_db; init_db() # 启动开发服务器 flask --app app run --debug--debug打开后改代码会自动重载出错页面会显示调用栈调试期非常省事。但要注意这个模式绝对不能上生产后面避坑章节会专门说。3. 核心链路拆解一个请求从进入到返回3.1 应用入口与蓝图注册app.py是整个项目的装配车间它不写业务逻辑只负责把零件拼起来。典型写法是这样from flask import Flask from config import Config import db from blueprints.auth import auth_bp from blueprints.items import items_bp def create_app(): app Flask(__name__) app.config.from_object(Config) # 注册蓝图url_prefix 决定这组路由的公共前缀 app.register_blueprint(auth_bp) app.register_blueprint(items_bp, url_prefix/items) # 请求结束时自动关闭数据库连接 app.teardown_appcontext(db.close_db) return app app create_app()这里用的是「应用工厂」模式create_app()返回 app 实例。好处是测试时可以造多个互不干扰的实例配置也能按环境切换。register_blueprint的第二个参数url_prefix很关键items_bp里的路由写成/list实际访问路径就是/items/list前缀统一管理改起来只动一处。teardown_appcontext注册的函数会在每个请求上下文销毁时调用用来关数据库连接。这是 Flask 里管理请求级资源的标配手法漏了它连接会越积越多最后报「database is locked」。3.2 数据库连接的获取与释放db.py是数据层的门卫核心是「一个请求一个连接」import sqlite3 from flask import g, current_app def get_db(): # 当前请求上下文里还没有连接就新建一个挂到 g 上 if db not in g: g.db sqlite3.connect( current_app.config[DATABASE], detect_typessqlite3.PARSE_DECLTYPES, ) # 让查询结果支持按列名取值row[name] 而不是 row[0] 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(): 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这行别省它让查询结果能按列名访问模板里写item[title]比item[1]可读太多也不怕改表顺序。init_db()读schema.sql一次性执行建表脚本适合首次部署或重置环境。注意它依赖请求上下文因为用了g所以命令行调用时要包一层with app.app_context():否则会报「working outside of application context」。3.3 登录鉴权session 与装饰器登录模块是这类 Demo 的标配也是理解 Flask session 的好入口。核心逻辑是校验用户名密码 → 往 session 里写用户 id → 后续请求靠装饰器拦截。from functools import wraps from flask import Blueprint, request, session, redirect, url_for, render_template from werkzeug.security import check_password_hash from db import get_db auth_bp Blueprint(auth, __name__) def login_required(view): wraps(view) def wrapped(*args, **kwargs): if user_id not in session: # 未登录跳回登录页并记住原本想去的地址 return redirect(url_for(auth.login, nextrequest.path)) return view(*args, **kwargs) return wrapped auth_bp.route(/login, methods[GET, POST]) def login(): if request.method POST: username request.form[username] password request.form[password] db get_db() user db.execute( SELECT * FROM users WHERE username ?, (username,) ).fetchone() if user is None or not check_password_hash(user[password_hash], password): return render_template(login.html, error用户名或密码错误) session.clear() session[user_id] user[id] return redirect(url_for(items.list_items)) return render_template(login.html)login_required是个装饰器工厂套在需要登录的视图上就能拦截。wraps(view)保留原函数的名字和文档调试时调用栈不会乱。session是 Flask 基于签名 cookie 实现的密钥来自config.py里的SECRET_KEY这个值必须随机且保密泄露了别人就能伪造登录态。密码校验用check_password_hash对应注册时用generate_password_hash存哈希。千万别存明文也别自己写加密逻辑血泪经验自己搓的哈希方案十有八九有漏洞。3.4 业务模块的增删改查items.py是业务主战场把列表、新增、编辑、删除四个动作写全基本就覆盖了 CRUD 的完整套路from flask import Blueprint, request, redirect, url_for, render_template, abort from db import get_db from blueprints.auth import login_required items_bp Blueprint(items, __name__) items_bp.route(/list) login_required def list_items(): db get_db() rows db.execute( SELECT id, title, status, created_at FROM items ORDER BY id DESC ).fetchall() return render_template(items/list.html, itemsrows) items_bp.route(/create, methods[POST]) login_required def create_item(): title request.form.get(title, ).strip() if not title: abort(400, 标题不能为空) db get_db() db.execute( INSERT INTO items (title, status, owner_id) VALUES (?, ?, ?), (title, todo, session[user_id]), ) db.commit() return redirect(url_for(items.list_items))fetchall()拿回全部行模板里用{% for item in items %}循环渲染。db.commit()别忘sqlite3 默认不开自动提交漏了它数据写不进去页面刷新还是老样子新手最容易在这卡住。参数全部走?占位符这是防 SQL 注入的底线。哪怕title是用户直接填的拼进 SQL 字符串也会被当成语句执行占位符则把它当纯数据处理。abort(400)直接返回错误页比静默失败好排查。4. 跑起来之后模板渲染与静态资源4.1 Jinja2 模板的继承与复用页面不重复写导航栏和页脚靠的是模板继承。base.html定义骨架子模板只填内容块!-- templates/base.html -- !DOCTYPE html html langzh head meta charsetutf-8 title{% block title %}试验楼{% endblock %}/title link relstylesheet href{{ url_for(static, filenamestyle.css) }} /head body nav a href{{ url_for(items.list_items) }}清单/a {% if session.get(user_id) %} a href{{ url_for(auth.logout) }}退出/a {% endif %} /nav main {% block content %}{% endblock %} /main /body /html子模板{% extends base.html %}后只写{% block content %}里的部分。url_for按端点名生成 URL路由改了路径模板不用跟着改这是它比手写href强的地方。session在模板里可直接访问用来控制导航显隐。4.2 表单提交与错误回显新增和编辑共用一个form.html靠传不传item区分模式。表单methodpost后端用request.form取值。校验失败时把error变量传回模板页面顶部显示提示用户不用重填整张表。这套「提交 → 校验 → 回显」的闭环是所有表单类页面的通用骨架改业务时照抄结构即可。静态资源放static/url_for(static, filename...)生成路径Flask 自动处理。开发期浏览器可能缓存旧 CSS改样式不生效时强制刷新一下别急着怀疑代码。5. 避坑与排查这几个坑我踩过5.1 报「working outside of application context」现象是命令行跑初始化脚本或后台任务时抛这个错。原因是用了g或current_app但当前没有应用上下文。解决是包一层with app.app_context():把init_db()放进去。记住凡是碰g、current_app、session的代码都得在上下文里跑。5.2 数据写不进刷新还是旧数据现象是提交表单后页面没变化。原因多半是漏了db.commit()sqlite3 默认手动提交。解决是在每个写操作后补上 commit。另一种可能是浏览器缓存了 GET 结果用无痕窗口验证一下就能区分。5.3 报「database is locked」现象是并发访问时偶发这个错。原因是连接没及时关闭或者有长事务占着写锁。解决是确认teardown_appcontext注册了close_db并且查询完尽快 commit。sqlite3 同一时刻只允许一个写者高并发场景本就该换数据库别硬扛。5.4 改了代码不生效现象是保存文件后页面还是老逻辑。原因是没开--debug或者开了但进程没重载。解决是启动时加--debug并确认终端有「Restarting with stat」字样。生产环境别开 debug错误页会暴露源码和配置。5.5 登录后刷新就掉线现象是登录成功跳转后又回到登录页。原因是SECRET_KEY每次启动都变比如用了随机生成旧 cookie 签名对不上就失效。解决是把SECRET_KEY固定写在配置里从环境变量读别每次随机。6. 二次开发从试验楼到能用的系统拿到这个 Demo最值的改造不是加页面而是先把数据层和配置层立稳再往上堆功能。我的习惯是分三步走。第一步把config.py拆成开发和生产两套SECRET_KEY、DATABASE路径都从环境变量读代码里不留硬编码。这样同一份代码换环境不用改源码部署时少一类事故。第二步给数据访问加一层薄封装。现在 SQL 散在各个视图里业务一多就难维护。可以在db.py旁边加个repository.py把「按 id 查、按条件筛、批量更新」这些操作收成函数视图只调函数不写 SQL。改动量不大但后面换数据库或加缓存时只动这一层。第三步补上分页和搜索。列表页数据一多就卡LIMIT ? OFFSET ?配合总数查询就能分页搜索用WHERE title LIKE ?参数写成f%{keyword}%。这两块是轻量系统最常被要求的功能提前留好接口后面加需求不慌。验证改造是否成功别只看页面能开。我的做法是写几条冒烟测试未登录访问列表应跳转登录页、登录后新增一条能查到、删除后列表少一条。用 Flask 的test_client()几行就能跑def test_create_requires_login(client): resp client.post(/items/create, data{title: x}) assert resp.status_code 302 # 未登录被重定向这几条测试跑通说明鉴权和主链路没被改坏再往上加功能心里有底。最后说个教训。早些年我改这类 Demo总喜欢先动模板、加样式结果业务逻辑没理顺页面越堆越乱回头重构成本翻倍。从那以后我每次二次开发都强制先跑一遍冒烟测试、再动数据层、最后碰前端顺序不乱。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?