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

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

一个让我半夜爬起来改代码的Bug

上周帮朋友调试一个Flask博客项目,页面死活返回404。我盯着路由文件看了半小时,最后发现他写的是@app.route(‘/post/’+str(id))——这种拼接路由的方式在Flask里根本行不通。Flask的路由是静态匹配的,动态参数必须用<int:id>这种尖括号语法。这个坑我当年也踩过,今天就从这里开始,带你把Flask博客从零搭起来。

路由:别把URL当字符串拼接

Flask的路由装饰器@app.route()接受的是URL模式,不是字符串模板。正确的动态路由写法:

from flask import Flask
app = Flask(__name__)

# 这里踩过坑:别写成 @app.route('/post/'+id)
@app.route('/post/<int:post_id>')
def show_post(post_id):
    return f'文章ID: {post_id}'

<int:post_id>表示这个段必须是整数,Flask会自动做类型转换。如果你想要字符串,用<string:slug>或者直接<slug>(默认就是字符串)。还有<path:subpath>可以匹配包含斜杠的路径,比如做分类嵌套时很有用。

路由的HTTP方法默认只响应GET。写博客系统时,处理表单提交必须显式声明:

@app.route('/create', methods=['GET', 'POST'])
def create_post():
    if request.method == 'POST':
        # 处理表单数据
        return '文章已创建'
    return render_template('create.html')

别这样写:@app.route('/create', methods=['POST'])然后单独写一个GET路由——Flask允许一个函数处理多个方法,代码更干净。

模板:Jinja2的坑与技巧

Flask默认用Jinja2模板引擎。模板文件放在templates/目录下,这是硬性规定,别自作聪明改路径。

模板里最常用的就是变量替换和控制结构:

<!-- templates/post.html -->
<h1>{{ post.title }}</h1>
<p>{{ 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

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}我的博客{% endblock %}</title>
</head>
<body>
    <nav>
        <a href="/">首页</a>
        <a href="/create">写文章</a>
    </nav>
    <main>
        {% block content %}{% endblock %}
    </main>
</body>
</html>

子模板只需要填充block

{% extends "base.html" %}
{% block title %}{{ post.title }} - 我的博客{% endblock %}
{% block content %}
    <article>
        <h1>{{ post.title }}</h1>
        <div>{{ post.body_html | safe }}</div>
    </article>
{% endblock %}

请求与响应:别把数据搞丢了

Flask的request对象封装了所有HTTP请求数据。处理表单时,最常用的就是request.form

from flask import request, redirect, url_for

@app.route('/create', methods=['POST'])
def create_post():
    title = request.form.get('title', '').strip()
    content = request.form.get('content', '').strip()
    
    # 这里踩过坑:别用 request.form['title'],键不存在会抛KeyError
    if not title:
        return '标题不能为空', 400
    
    # 保存到数据库...
    return redirect(url_for('show_post', post_id=new_id))

url_for()是路由的逆向解析——根据函数名生成URL。好处是路由路径改了,代码不用改。别硬编码URL,比如redirect('/post/1'),哪天路由改成/article/1你就得满世界找。

文件上传用request.files

@app.route('/upload', methods=['POST'])
def upload_image():
    file = request.files.get('image')
    if file and file.filename:
        # 别这样写:直接保存用户文件名
        # 安全做法:生成随机文件名
        import uuid
        ext = file.filename.rsplit('.', 1)[1].lower()
        filename = f"{uuid.uuid4().hex}.{ext}"
        file.save(f'static/uploads/{filename}')
        return url_for('static', filename=f'uploads/{filename}')
    return '没有文件', 400

响应除了返回字符串,还可以返回元组(响应体, 状态码, 头部字典)。做API时常用:

@app.route('/api/post/<int:post_id>')
def api_post(post_id):
    post = get_post(post_id)
    if post is None:
        return {'error': '文章不存在'}, 404
    return {
        'id': post.id,
        'title': post.title,
        'content': post.content
    }

Flask 2.0+支持直接返回字典,会自动转JSON。老版本需要jsonify()

实战:一个极简博客的核心逻辑

把上面这些拼起来,一个博客的CRUD雏形就有了:

from flask import Flask, render_template, request, redirect, url_for

app = Flask(__name__)

# 假装这是数据库
posts = []
counter = 0

@app.route('/')
def index():
    return render_template('index.html', posts=posts)

@app.route('/post/<int:post_id>')
def show_post(post_id):
    post = next((p for p in posts if p['id'] == post_id), None)
    if post is None:
        return '文章不存在', 404
    return render_template('post.html', post=post)

@app.route('/create', methods=['GET', 'POST'])
def create_post():
    global counter
    if request.method == 'POST':
        title = request.form.get('title', '').strip()
        content = request.form.get('content', '').strip()
        if not title:
            return '标题不能为空', 400
        counter += 1
        posts.append({
            'id': counter,
            'title': title,
            'content': content
        })
        return redirect(url_for('show_post', post_id=counter))
    return render_template('create.html')

if __name__ == '__main__':
    app.run(debug=True)

模板文件templates/index.html

{% extends "base.html" %}
{% block content %}
    <h1>最新文章</h1>
    {% for post in posts %}
        <article>
            <h2><a href="{{ url_for('show_post', post_id=post.id) }}">{{ post.title }}</a></h2>
        </article>
    {% else %}
        <p>还没有文章,<a href="{{ url_for('create_post') }}">写一篇</a></p>
    {% endfor %}
{% endblock %}

个人经验建议

  1. 调试模式一定要开app.run(debug=True)。改代码自动重启,报错直接显示在浏览器里。生产环境记得关掉,不然用户能看到你的源码。

  2. 路由别用正则:Flask的路由语法已经够用了。非要正则的话,用@app.route('/post/<regex("[a-z]+"):slug>'),但维护起来很痛苦。

  3. 模板里少写逻辑:复杂的计算、数据库查询放在视图函数里,模板只负责展示。Jinja2不是编程语言,别为难它。

  4. 404页面要优雅:注册一个404处理器:

@app.errorhandler(404)
def not_found(e):
    return render_template('404.html'), 404
  1. 别用全局列表当数据库:上面的例子只是为了演示。真实项目用SQLite+Flask-SQLAlchemy,或者直接上PostgreSQL。

Flask的哲学是“微框架”——给你最核心的东西,剩下的你自己选。这种自由既是优点也是陷阱。刚开始学,先按我说的套路来,等熟悉了再折腾扩展。下一篇我会讲Flask-SQLAlchemy怎么集成,把博客的数据持久化做起来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值