081、Flask 入门:路由、模板、请求响应——一个博客的从零搭建

081、Flask 入门:路由、模板、请求响应——一个博客的从零搭建
081、Flask 入门路由、模板、请求响应——一个博客的从零搭建一个让我半夜爬起来改代码的Bug上周帮朋友调试一个Flask博客项目页面死活返回404。我盯着路由文件看了半小时最后发现他写的是app.route(‘/post/’str(id))——这种拼接路由的方式在Flask里根本行不通。Flask的路由是静态匹配的动态参数必须用int:id这种尖括号语法。这个坑我当年也踩过今天就从这里开始带你把Flask博客从零搭起来。路由别把URL当字符串拼接Flask的路由装饰器app.route()接受的是URL模式不是字符串模板。正确的动态路由写法fromflaskimportFlask appFlask(__name__)# 这里踩过坑别写成 app.route(/post/id)app.route(/post/int:post_id)defshow_post(post_id):returnf文章ID:{post_id}int:post_id表示这个段必须是整数Flask会自动做类型转换。如果你想要字符串用string:slug或者直接slug默认就是字符串。还有path:subpath可以匹配包含斜杠的路径比如做分类嵌套时很有用。路由的HTTP方法默认只响应GET。写博客系统时处理表单提交必须显式声明app.route(/create,methods[GET,POST])defcreate_post():ifrequest.methodPOST:# 处理表单数据return文章已创建returnrender_template(create.html)别这样写app.route(/create, methods[POST])然后单独写一个GET路由——Flask允许一个函数处理多个方法代码更干净。模板Jinja2的坑与技巧Flask默认用Jinja2模板引擎。模板文件放在templates/目录下这是硬性规定别自作聪明改路径。模板里最常用的就是变量替换和控制结构!-- templates/post.html --h1{{ post.title }}/h1p{{ post.content }}/p{% if post.tags %}ul{% for tag in post.tags %}li{{ tag }}/li{% endfor %}/ul{% endif %}这里有个坑Jinja2的{{ }}会自动转义HTML。如果你存的是富文本内容需要用|safe过滤器div{{ post.body_html | safe }}/div但别滥用safe——用户输入的内容直接safe等于给XSS攻击开门。正确的做法是用Markdown解析后再safe或者用bleach库过滤。模板继承是博客系统的骨架。写一个base.html!DOCTYPEhtmlhtmlheadtitle{% block title %}我的博客{% endblock %}/title/headbodynavahref/首页/aahref/create写文章/a/navmain{% block content %}{% endblock %}/main/body/html子模板只需要填充block{% extends base.html %} {% block title %}{{ post.title }} - 我的博客{% endblock %} {% block content %}articleh1{{ post.title }}/h1div{{ post.body_html | safe }}/div/article{% endblock %}请求与响应别把数据搞丢了Flask的request对象封装了所有HTTP请求数据。处理表单时最常用的就是request.formfromflaskimportrequest,redirect,url_forapp.route(/create,methods[POST])defcreate_post():titlerequest.form.get(title,).strip()contentrequest.form.get(content,).strip()# 这里踩过坑别用 request.form[title]键不存在会抛KeyErrorifnottitle:return标题不能为空,400# 保存到数据库...returnredirect(url_for(show_post,post_idnew_id))url_for()是路由的逆向解析——根据函数名生成URL。好处是路由路径改了代码不用改。别硬编码URL比如redirect(/post/1)哪天路由改成/article/1你就得满世界找。文件上传用request.filesapp.route(/upload,methods[POST])defupload_image():filerequest.files.get(image)iffileandfile.filename:# 别这样写直接保存用户文件名# 安全做法生成随机文件名importuuid extfile.filename.rsplit(.,1)[1].lower()filenamef{uuid.uuid4().hex}.{ext}file.save(fstatic/uploads/{filename})returnurl_for(static,filenamefuploads/{filename})return没有文件,400响应除了返回字符串还可以返回元组(响应体, 状态码, 头部字典)。做API时常用app.route(/api/post/int:post_id)defapi_post(post_id):postget_post(post_id)ifpostisNone:return{error:文章不存在},404return{id:post.id,title:post.title,content:post.content}Flask 2.0支持直接返回字典会自动转JSON。老版本需要jsonify()。实战一个极简博客的核心逻辑把上面这些拼起来一个博客的CRUD雏形就有了fromflaskimportFlask,render_template,request,redirect,url_for appFlask(__name__)# 假装这是数据库posts[]counter0app.route(/)defindex():returnrender_template(index.html,postsposts)app.route(/post/int:post_id)defshow_post(post_id):postnext((pforpinpostsifp[id]post_id),None)ifpostisNone:return文章不存在,404returnrender_template(post.html,postpost)app.route(/create,methods[GET,POST])defcreate_post():globalcounterifrequest.methodPOST:titlerequest.form.get(title,).strip()contentrequest.form.get(content,).strip()ifnottitle:return标题不能为空,400counter1posts.append({id:counter,title:title,content:content})returnredirect(url_for(show_post,post_idcounter))returnrender_template(create.html)if__name____main__:app.run(debugTrue)模板文件templates/index.html{% extends base.html %} {% block content %}h1最新文章/h1{% for post in posts %}articleh2ahref{{ url_for(show_post, post_idpost.id) }}{{ post.title }}/a/h2/article{% else %}p还没有文章ahref{{ url_for(create_post) }}写一篇/a/p{% endfor %} {% endblock %}个人经验建议调试模式一定要开app.run(debugTrue)。改代码自动重启报错直接显示在浏览器里。生产环境记得关掉不然用户能看到你的源码。路由别用正则Flask的路由语法已经够用了。非要正则的话用app.route(/post/regex([a-z]):slug)但维护起来很痛苦。模板里少写逻辑复杂的计算、数据库查询放在视图函数里模板只负责展示。Jinja2不是编程语言别为难它。404页面要优雅注册一个404处理器app.errorhandler(404)defnot_found(e):returnrender_template(404.html),404别用全局列表当数据库上面的例子只是为了演示。真实项目用SQLiteFlask-SQLAlchemy或者直接上PostgreSQL。Flask的哲学是“微框架”——给你最核心的东西剩下的你自己选。这种自由既是优点也是陷阱。刚开始学先按我说的套路来等熟悉了再折腾扩展。下一篇我会讲Flask-SQLAlchemy怎么集成把博客的数据持久化做起来。