Flask Flaskr 教程实战:Blog 蓝图与博客文章 CRUD 视图的完整实现
发布时间:2026/9/5 21:36:41来源:尧图网络
Flask Flaskr 教程实战Blog 蓝图与博客文章 CRUD 视图的完整实现【免费下载链接】flaskThe Python micro framework for building web applications.项目地址: https://gitcode.com/gh_mirrors/fl/flask本文基于 Flask 官方教程中的 Blog Blueprint 章节docs/tutorial/blog.rst完整讲解 Flaskr 博客应用中blog蓝图的设计与实现从蓝图定义与注册、帖子列表页的JOIN查询到创建、编辑、删除帖子的完整 CRUD 视图并结合仓库中 examples/tutorial/flaskr/blog.py 的源码与 examples/tutorial/tests/test_blog.py 的测试用例说明登录鉴权、作者权限校验403/404、路由参数转换等关键机制的落地细节。读完后你能够掌握用 Flask 蓝图组织业务模块、在视图中完成数据库校验与模板渲染的标准写法并理解url_for、abort、g等核心组件在真实业务流中的调用链。功能目标与整体设计blog蓝图是 Flaskr 应用的核心业务模块它的功能目标非常明确列出所有帖子最新在前、允许已登录用户创建帖子、允许帖子作者编辑或删除自己的帖子。与负责登录注册的auth蓝图定义在 examples/tutorial/flaskr/auth.py带url_prefix/auth不同blog蓝图不设url_prefix——因为博客是 Flaskr 的主功能帖子列表页就是应用的主页/创建页在/create编辑页在/id/update删除页在/id/delete。整个模块依赖两个前置基础设施get_db()定义在 examples/tutorial/flaskr/db.py。它把sqlite3连接挂到应用上下文的g对象上同一个请求内多次调用会复用同一连接连接通过app.teardown_appcontext(close_db)在每个请求结束时关闭。开启detect_typessqlite3.PARSE_DECLTYPES并注册timestamp类型转换器db.py 第 48 行使created字段自动解析为datetime对象模板里才能直接调用strftime。login_required装饰器定义在 examples/tutorial/flaskr/auth.py。它检查g.user是否为None未登录时重定向到url_for(auth.login)。而g.user本身由同文件中的load_logged_in_userauth.py 第 32-43 行通过bp.before_app_request在每个请求开始前从 session 的user_id加载到g上。定义蓝图并在应用工厂中注册新建flaskr/blog.py定义蓝图实例from flask import ( Blueprint, flash, g, redirect, render_template, request, url_for ) from werkzeug.exceptions import abort from flaskr.auth import login_required from flaskr.db import get_db bp Blueprint(blog, __name__)然后在应用工厂create_app中导入并注册把新代码放在工厂函数返回app之前见 examples/tutorial/flaskr/init.pydef create_app(): app ... # existing code omitted from . import blog app.register_blueprint(blog.bp) app.add_url_rule(/, endpointindex) return app这里有一个关键细节index视图注册在blog蓝图下其 endpoint 全名是blog.index而认证视图如登录成功后的跳转引用的却是裸的indexendpoint。app.add_url_rule(/, endpointindex)把 endpoint 名index关联到/这个 URL使得url_for(index)和url_for(blog.index)都能工作生成同一个/URL。仓库中的 flaskr/init.py 第 42-46 行 还保留了这段解释注释在另一个应用中你也可以给blog蓝图设一个url_prefix并在工厂里单独定义一个主index视图类似hello视图那时index和blog.index的 URL 就不同了——教程为了简单才让两者指向同一个/。Index 视图JOIN 查询展示全部帖子index视图展示所有帖子最新在前。查询用JOIN把user表中的作者信息带出来避免模板里为每个帖子再查一次用户名blog.py 第 16-25 行bp.route(/) def index(): db get_db() posts db.execute( SELECT p.id, title, body, created, author_id, username FROM post p JOIN user u ON p.author_id u.id ORDER BY created DESC ).fetchall() return render_template(blog/index.html, postsposts)对应的模板 examples/tutorial/flaskr/templates/blog/index.html{% extends base.html %} {% block header %} h1{% block title %}Posts{% endblock %}/h1 {% if g.user %} a classaction href{{ url_for(blog.create) }}New/a {% endif %} {% endblock %} {% block content %} {% for post in posts %} article classpost header div h1{{ post[title] }}/h1 div classaboutby {{ post[username] }} on {{ post[created].strftime(%Y-%m-%d) }}/div /div {% if g.user[id] post[author_id] %} a classaction href{{ url_for(blog.update, idpost[id]) }}Edit/a {% endif %} /header p classbody{{ post[body] }}/p /article {% if not loop.last %} hr {% endif %} {% endfor %} {% endblock %}模板中有三个值得注意的点{% if g.user %}用户登录状态通过g在模板中直接可见已登录才显示 New 链接g.user[id] post[author_id]只有帖子作者才看到 Edit 链接——这是前端层面的权限控制真正的安全边界在后端的get_post校验见下文loop.lastJinjafor循环内的特殊变量用于在除最后一篇之外的每篇帖子后渲染一条分隔线视觉上分隔各帖。post[created].strftime(%Y-%m-%d)能这样用正是因为 db.py 注册了timestamp类型转换器created列已被解析成datetime而非字符串。Create 视图表单校验与插入create视图的工作方式与auth蓝图里的register视图一致要么显示表单要么校验提交数据、写入数据库或者显示错误。login_required装饰器用于所有需要登录的 blog 视图——未登录用户访问会被重定向到登录页blog.py 第 60-83 行bp.route(/create, methods(GET, POST)) login_required def create(): if request.method POST: title request.form[title] body request.form[body] error None if not title: error Title is required. if error is not None: flash(error) else: db get_db() db.execute( INSERT INTO post (title, body, author_id) VALUES (?, ?, ?), (title, body, g.user[id]) ) db.commit() return redirect(url_for(blog.index)) return render_template(blog/create.html)模板 examples/tutorial/flaskr/templates/blog/create.html{% extends base.html %} {% block header %} h1{% block title %}New Post{% endblock %}/h1 {% endblock %} {% block content %} form methodpost label fortitleTitle/label input nametitle idtitle value{{ request.form[title] }} required label forbodyBody/label textarea namebody idbody{{ request.form[body] }}/textarea input typesubmit valueSave /form {% endblock %}这里的实践要点methods(GET, POST)同时处理显示表单GET和提交表单POST作者身份来自g.user[id]而非表单提交值杜绝了伪造作者的可能db.commit()显式提交成功后redirect(url_for(blog.index))回到列表页——即典型的表单成功后重定向Post/Redirect/Get模式校验失败时flash(error)记录消息重新渲染同一模板用户已输入的内容通过request.form[title]保留在输入框中。get_post 辅助函数404/403 权限模型update和delete视图都需要按id取出帖子并校验作者是否为当前登录用户。为避免重复代码抽出一个共用函数blog.py 第 28-57 行def get_post(id, check_authorTrue): post get_db().execute( SELECT p.id, title, body, created, author_id, username FROM post p JOIN user u ON p.author_id u.id WHERE p.id ?, (id,) ).fetchone() if post is None: abort(404, fPost id {id} doesnt exist.) if check_author and post[author_id] ! g.user[id]: abort(403) return postabort()来自werkzeug.exceptions会抛出一个特殊异常返回对应 HTTP 状态码404Not Found帖子id不存在并附带一条展示给用户的消息fPost id {id} doesnt exist.abort的第二个参数是可选的错误消息缺省时使用默认消息403Forbidden帖子存在但当前用户不是作者。注意与401Unauthorized的区别登录态问题不走401而是由login_required重定向到登录页。check_author参数让该函数也能在不校验作者的场合复用——比如将来写一个查看单篇帖子的详情页视图任何用户都可见只需调用get_post(id, check_authorFalse)。Update 视图路由参数、转换类型与双表单模板update视图带有路由参数blog.py 第 86-110 行bp.route(/int:id/update, methods(GET, POST)) login_required def update(id): post get_post(id) if request.method POST: title request.form[title] body request.form[body] error None if not title: error Title is required. if error is not None: flash(error) else: db get_db() db.execute( UPDATE post SET title ?, body ? WHERE id ?, (title, body, id) ) db.commit() return redirect(url_for(blog.index)) return render_template(blog/update.html, postpost)与之前写的视图相比这里有几个新知识点路由变量与类型转换update函数的参数id对应路由中的int:id。真实 URL 形如/1/updateFlask 会捕获1确保它是int类型后再作为id参数传入视图函数。如果写成id而不用int:前缀传进来的就是字符串。url_for生成带参数的 URL要生成编辑页 URL需要把id传给url_for它才知道要填充什么值url_for(blog.update, idpost[id])index.html模板里也正是这样用的。create与update的差异两个视图结构非常相似主要差别是update使用post对象和UPDATE查询而非INSERT。教程特意不把它们合并成一个视图和模板以保持教学清晰——实际项目中通过重构确实可以合并。模板 examples/tutorial/flaskr/templates/blog/update.html 包含两个表单{% extends base.html %} {% block header %} h1{% block title %}Edit {{ post[title] }}{% endblock %}/h1 {% endblock %} {% block content %} form methodpost label fortitleTitle/label input nametitle idtitle value{{ request.form[title] or post[title] }} required label forbodyBody/label textarea namebody idbody{{ request.form[body] or post[body] }}/textarea input typesubmit valueSave /form hr form action{{ url_for(blog.delete, idpost[id]) }} methodpost input classdanger typesubmit valueDelete onclickreturn confirm(Are you sure?); /form {% endblock %}第一个表单把编辑后的数据 POST 到当前页/id/update第二个表单只有一个按钮通过action属性改为 POST 到 delete 视图。删除按钮用一小段 JavaScriptreturn confirm(Are you sure?)在提交前弹出确认对话框。{{ request.form[title] or post[title] }}模式表单未提交时显示post的原始数据如果提交了无效数据比如标题为空则显示request.form中的值让用户在修正错误时保留已输入的内容。request和g一样是模板中自动可用的变量。Delete 视图只接受 POST 并跳回列表删除操作没有独立模板——删除按钮就内嵌在update.html中POST 到/id/delete。因此该视图只处理POST方法成功后重定向回indexblog.py 第 113-125 行bp.route(/int:id/delete, methods(POST,)) login_required def delete(id): get_post(id) db get_db() db.execute(DELETE FROM post WHERE id ?, (id,)) db.commit() return redirect(url_for(blog.index))注意get_post(id)的返回值在这里被有意忽略——调用它只是为了执行存在性检查404和作者检查403校验通过后才执行删除。把破坏性操作限定在POST方法上也避免了通过直接访问 GET URL 误删数据。测试用例印证权限模型教程仓库中的 examples/tutorial/tests/test_blog.py 用测试客户机完整验证了上述行为test_login_required对/create、/1/update、/1/delete三个路径发起 POST断言响应头Location为/auth/login——印证login_required的重定向行为test_author_required把 1 号帖子的author_id改为另一用户后当前用户执行/1/update和/1/delete均返回403且首页上不再出现href/1/update的编辑链接——印证get_post的作者校验与index.html中的前端条件一致test_exists_required对不存在的/2/update、/2/delete发起 POST返回404——印证abort(404, ...)分支test_create/test_update/test_delete分别验证插入、更新、删除后数据库状态如SELECT COUNT(id) FROM post从 1 变 2、title变为updated、帖子被删test_create_update_validate空标题提交后响应体包含Title is required.——印证flash错误消息会出现在重新渲染的页面中。这些测试运行在 examples/tutorial/tests/conftest.py 提供的测试配置之上数据库由 examples/tutorial/flaskr/schema.sql 定义的user和post两张表初始化post.created使用TIMESTAMP ... DEFAULT CURRENT_TIMESTAMP与index视图按created DESC排序、模板中strftime渲染日期的行为相互对应。小结blog蓝图以不到 130 行代码实现了 Flaskr 的核心业务闭环其中体现了几个在真实 Flask 应用中反复出现的模式蓝图按业务模块组织视图url_prefix与 endpoint 命名策略blog.index与裸index通过add_url_rule指向同一 URL需要在注册时一并规划读取-校验-写入-重定向的视图结构GET 渲染表单POST 校验后写库成功redirect失败flash并重新渲染共用get_post函数集中处理 404/403把存在性与作者权限检查从视图逻辑中剥离check_author参数为将来扩展如公开的单篇详情页留了口子前端展示条件g.user[id] post[author_id]与后端强制校验abort(403)分层协作前者改善体验后者保障安全g承载请求级状态g.user、g.db连接在teardown_appcontext中自动释放视图和模板共享同一份上下文。以上源码与测试均可在仓库中直接查阅examples/tutorial/flaskr/blog.py、examples/tutorial/flaskr/init.py、examples/tutorial/flaskr/db.py、examples/tutorial/tests/test_blog.py以及教程文档 docs/tutorial/blog.rst。【免费下载链接】flaskThe Python micro framework for building web applications.项目地址: https://gitcode.com/gh_mirrors/fl/flask创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网