Python Web开发实战:从Flask入门到应用部署
1. Web框架:选择适合你的工具
在Python的Web开发世界中,Flask和Django代表了两种不同的哲学。Django是“全功能套装”,提供了从ORM到Admin后台的一站式解决方案;而Flask是“微型工具箱”,给你最核心的路由和模板,其他功能按需添加。
选择哪一个?问自己三个问题:
- 项目复杂度如何?(简单项目选Flask,复杂企业级选Django)
- 需要快速原型还是长期维护?
- 团队熟悉什么技术栈?
# Flask的最小应用 vs Django的最小应用
# Flask版 (5行代码)
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello, World!'
if __name__ == '__main__':
app.run(debug=True)
# Django版(需要多个文件和配置)
# 此处不展示完整代码,因为Django需要项目结构对于初学者和大多数中小型项目,我推荐从Flask开始。它的学习曲线平缓,能让你真正理解Web如何工作,而不是被框架的魔法迷惑。
2. Flask快速入门:15分钟搭建第一个Web应用
让我们从一个实际的项目开始:一个简单的个人博客系统。我们将一步步构建它,涵盖从安装到基本功能的全部流程。
# 首先,创建项目环境
mkdir my-blog && cd my-blog
python -m venv venv # 创建虚拟环境
# 激活虚拟环境(Windows)
venv\Scripts\activate
# 激活虚拟环境(Mac/Linux)
source venv/bin/activate
# 安装必要的包
pip install flask flask-sqlalchemy flask-wtf flask-login
pip install python-dotenv # 管理环境变量现在创建我们的应用文件结构:
my-blog/
├── app/
│ ├── __init__.py
│ ├── routes.py
│ ├── models.py
│ ├── forms.py
│ ├── templates/
│ │ ├── base.html
│ │ ├── index.html
│ │ ├── post.html
│ │ └── login.html
│ └── static/
│ ├── css/
│ └── js/
├── config.py
├── .env
└── run.py创建主要的应用文件:
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from config import Config
import os
db = SQLAlchemy()
login_manager = LoginManager()
login_manager.login_view = 'login'
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
login_manager.init_app(app)
# 注册蓝图(稍后创建)
from app.routes import main
app.register_blueprint(main)
# 创建数据库表
with app.app_context():
db.create_all()
return app# config.py
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-key-change-in-production'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///blog.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False# run.py
from app import create_app
app = create_app()
if __name__ == '__main__':
app.run(debug=True)现在,运行python run.py,访问 http://localhost:5000,你的Flask应用已经启动了!
3. 路由与视图函数:处理HTTP请求的核心
路由是Web应用的入口点,它决定了哪个URL由哪个函数处理。Flask使用装饰器语法让路由定义变得直观易懂。
# app/routes.py
from flask import Blueprint, render_template, request, redirect, url_for, flash
from app.models import Post, User
from app.forms import LoginForm, PostForm
from flask_login import login_user, logout_user, login_required, current_user
from app import db
from datetime import datetime
main = Blueprint('main', __name__)
# 主页 - 显示所有博客文章
@main.route('/')
@main.route('/home')
def index():
page = request.args.get('page', 1, type=int)
posts = Post.query.order_by(Post.created_at.desc()).paginate(
page=page, per_page=5
)
return render_template('index.html', posts=posts)
# 动态路由 - 显示单篇文章
@main.route('/post/<int:post_id>')
def show_post(post_id):
post = Post.query.get_or_404(post_id)
return render_template('post.html', post=post)
# 支持多种HTTP方法的路由
@main.route('/create', methods=['GET', 'POST'])
@login_required # 需要登录才能访问
def create_post():
form = PostForm()
if form.validate_on_submit():
post = Post(
title=form.title.data,
content=form.content.data,
author=current_user
)
db.session.add(post)
db.session.commit()
flash('文章发布成功!', 'success')
return redirect(url_for('main.index'))
return render_template('create_post.html', form=form)
# 带查询参数的路由
@main.route('/search')
def search():
keyword = request.args.get('q', '')
if keyword:
# 简单的搜索功能
posts = Post.query.filter(
Post.title.contains(keyword) |
Post.content.contains(keyword)
).all()
return render_template('search_results.html',
posts=posts, keyword=keyword)
return redirect(url_for('main.index'))
# 错误处理路由
@main.errorhandler(404)
def page_not_found(error):
return render_template('404.html'), 404
@main.errorhandler(500)
def internal_error(error):
db.session.rollback() # 发生错误时回滚数据库会话
return render_template('500.html'), 500
# RESTful API 风格的路由
@main.route('/api/posts', methods=['GET'])
def get_posts_api():
posts = Post.query.all()
return {
'status': 'success',
'count': len(posts),
'posts': [post.to_dict() for post in posts]
}
@main.route('/api/posts/<int:post_id>', methods=['GET'])
def get_post_api(post_id):
post = Post.query.get_or_404(post_id)
return {
'status': 'success',
'post': post.to_dict()
}路由设计的最佳实践:
- 使用名词复数形式(如
/posts而不是/get_posts) - 保持URL简洁且有语义
- 使用连字符而不是下划线
- 版本化API路由(如
/api/v1/posts)
4. 模板渲染:动态HTML生成
Flask使用Jinja2模板引擎,它结合了HTML的静态性和Python的动态性。让我们创建基础模板和页面模板。
<!-- app/templates/base.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}我的博客{% endblock %}</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/style.css') }}"
/>
{% block head %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('main.index') }}">我的博客</a>
<div class="navbar-nav">
<a class="nav-link" href="{{ url_for('main.index') }}">首页</a>
{% if current_user.is_authenticated %}
<a class="nav-link" href="{{ url_for('main.create_post') }}"
>写文章</a
>
<a class="nav-link" href="{{ url_for('main.logout') }}">退出</a>
{% else %}
<a class="nav-link" href="{{ url_for('main.login') }}">登录</a>
<a class="nav-link" href="{{ url_for('main.register') }}">注册</a>
{% endif %}
</div>
<form class="d-flex" action="{{ url_for('main.search') }}" method="GET">
<input
class="form-control me-2"
type="search"
name="q"
placeholder="搜索文章"
/>
<button class="btn btn-outline-light" type="submit">搜索</button>
</form>
</div>
</nav>
<div class="container mt-4">
<!-- 闪存消息 -->
{% with messages = get_flashed_messages(with_categories=true) %} {% if
messages %} {% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show">
{{ message }}
<button
type="button"
class="btn-close"
data-bs-dismiss="alert"
></button>
</div>
{% endfor %} {% endif %} {% endwith %}
<!-- 主要内容 -->
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html><!-- app/templates/index.html -->
{% extends "base.html" %} {% block title %}首页 - 我的博客{% endblock %} {%
block content %}
<div class="row">
<div class="col-md-8">
<h1 class="mb-4">最新文章</h1>
{% for post in posts.items %}
<div class="card mb-4">
<div class="card-body">
<h2 class="card-title">
<a
href="{{ url_for('main.show_post', post_id=post.id) }}"
class="text-decoration-none"
>
{{ post.title }}
</a>
</h2>
<p class="card-text">{{ post.content|truncate(200) }}</p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
作者: {{ post.author.username }} | 发布时间: {{
post.created_at.strftime('%Y-%m-%d %H:%M') }}
</small>
<a
href="{{ url_for('main.show_post', post_id=post.id) }}"
class="btn btn-primary"
>阅读全文</a
>
</div>
</div>
</div>
{% else %}
<div class="alert alert-info">还没有文章,赶快写一篇吧!</div>
{% endfor %}
<!-- 分页导航 -->
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if posts.has_prev %}
<li class="page-item">
<a
class="page-link"
href="{{ url_for('main.index', page=posts.prev_num) }}"
>
上一页
</a>
</li>
{% endif %} {% for page_num in posts.iter_pages(left_edge=2,
left_current=2, right_current=3, right_edge=2) %} {% if page_num %} {%
if page_num == posts.page %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% else %}
<li class="page-item">
<a
class="page-link"
href="{{ url_for('main.index', page=page_num) }}"
>
{{ page_num }}
</a>
</li>
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %} {% endfor %} {% if posts.has_next %}
<li class="page-item">
<a
class="page-link"
href="{{ url_for('main.index', page=posts.next_num) }}"
>
下一页
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">关于本站</div>
<div class="card-body">
<p>这是一个使用Flask构建的个人博客系统。</p>
<p>总文章数: {{ posts.total }}</p>
{% if current_user.is_authenticated %}
<p>欢迎回来,{{ current_user.username }}!</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}Jinja2的强大功能:
- 控制结构:`{% if %}`, `{% for %}`, `{% macro %}`
- 过滤器:`{{ content|truncate(200)|safe }}`
- 模板继承:`{% extends %}`, `{% block %}`
- 包含:`{% include 'widget.html' %}`5. 表单处理:安全地接收用户输入
Web应用离不开表单,Flask-WTF扩展提供了强大的表单处理功能,包括CSRF保护、数据验证等。
# app/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
from app.models import User
class LoginForm(FlaskForm):
username = StringField('用户名',
validators=[DataRequired(), Length(min=3, max=20)])
password = PasswordField('密码',
validators=[DataRequired()])
remember = BooleanField('记住我')
submit = SubmitField('登录')
class RegistrationForm(FlaskForm):
username = StringField('用户名',
validators=[DataRequired(), Length(min=3, max=20)])
email = StringField('邮箱',
validators=[DataRequired(), Email()])
password = PasswordField('密码',
validators=[DataRequired()])
confirm_password = PasswordField('确认密码',
validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('注册')
# 自定义验证器
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError('该用户名已被使用,请选择其他用户名。')
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user:
raise ValidationError('该邮箱已被注册,请使用其他邮箱。')
class PostForm(FlaskForm):
title = StringField('标题',
validators=[DataRequired(), Length(min=3, max=100)])
content = TextAreaField('内容',
validators=[DataRequired()],
render_kw={"rows": 10})
submit = SubmitField('发布')
class CommentForm(FlaskForm):
content = TextAreaField('评论内容',
validators=[DataRequired(), Length(min=1, max=500)],
render_kw={"rows": 3, "placeholder": "请输入您的评论..."})
submit = SubmitField('提交评论')# app/routes.py(续)
@main.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
# 验证用户和密码
if user and user.check_password(form.password.data):
login_user(user, remember=form.remember.data)
next_page = request.args.get('next')
# 重定向到原请求页面或首页
return redirect(next_page) if next_page else redirect(url_for('main.index'))
flash('用户名或密码错误', 'danger')
return render_template('login.html', title='登录', form=form)
@main.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegistrationForm()
if form.validate_on_submit():
# 创建新用户
user = User(
username=form.username.data,
email=form.email.data
)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('注册成功!现在可以登录了。', 'success')
return redirect(url_for('main.login'))
return render_template('register.html', title='注册', form=form)
@main.route('/logout')
@login_required
def logout():
logout_user()
flash('您已成功退出登录。', 'info')
return redirect(url_for('main.index'))<!-- app/templates/login.html -->
{% extends "base.html" %} {% block title %}{{ title }}{% endblock %} {% block
content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="mb-0">用户登录</h4>
</div>
<div class="card-body">
<form method="POST" action="">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.username.label(class="form-label") }} {% if
form.username.errors %} {{ form.username(class="form-control
is-invalid") }}
<div class="invalid-feedback">
{% for error in form.username.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %} {{ form.username(class="form-control") }} {% endif %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }} {% if
form.password.errors %} {{ form.password(class="form-control
is-invalid") }}
<div class="invalid-feedback">
{% for error in form.password.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %} {{ form.password(class="form-control") }} {% endif %}
</div>
<div class="mb-3 form-check">
{{ form.remember(class="form-check-input") }} {{
form.remember.label(class="form-check-label") }}
</div>
<div class="d-grid">{{ form.submit(class="btn btn-primary") }}</div>
</form>
<div class="mt-3 text-center">
<small class="text-muted">
还没有账号? <a href="{{ url_for('main.register') }}">立即注册</a>
</small>
</div>
</div>
</div>
</div>
</div>
{% endblock %}表单安全要点:
- 永远使用CSRF令牌
- 验证所有输入,包括类型、长度、格式
- 使用参数化查询防止SQL注入
- 对输出进行HTML转义
6. 数据库集成:使用SQLAlchemy管理数据
Flask-SQLAlchemy是Flask的ORM扩展,它让数据库操作变得简单而Pythonic。
# app/models.py
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from app import db, login_manager
# 用户加载回调
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# 关联表(多对多关系)
post_tags = db.Table('post_tags',
db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True),
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True)
)
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 关系
posts = db.relationship('Post', backref='author', lazy='dynamic',
cascade='all, delete-orphan')
comments = db.relationship('Comment', backref='author', lazy='dynamic')
def __repr__(self):
return f'<User {self.username}>'
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def to_dict(self):
return {
'id': self.id,
'username': self.username,
'email': self.email,
'created_at': self.created_at.isoformat() if self.created_at else None
}
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow,
onupdate=datetime.utcnow)
# 外键
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# 关系
comments = db.relationship('Comment', backref='post', lazy='dynamic',
cascade='all, delete-orphan')
tags = db.relationship('Tag', secondary=post_tags, lazy='subquery',
backref=db.backref('posts', lazy=True))
def __repr__(self):
return f'<Post {self.title}>'
def to_dict(self):
return {
'id': self.id,
'title': self.title,
'content': self.content,
'author': self.author.username if self.author else None,
'created_at': self.created_at.isoformat() if self.created_at else None,
'comment_count': self.comments.count(),
'tags': [tag.name for tag in self.tags]
}
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 外键
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
def __repr__(self):
return f'<Comment {self.id}>'
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20), unique=True, nullable=False)
def __repr__(self):
return f'<Tag {self.name}>'
# 数据库操作示例
def database_operations_example():
"""演示常见的数据库操作"""
# 创建
user = User(username='test', email='test@example.com')
user.set_password('password123')
db.session.add(user)
db.session.commit()
# 查询
user = User.query.filter_by(username='test').first()
users = User.query.all()
paginated_users = User.query.paginate(page=1, per_page=10)
# 复杂查询
from sqlalchemy import and_, or_
recent_posts = Post.query.filter(
Post.created_at > '2024-01-01'
).order_by(Post.created_at.desc()).all()
# 更新
user.email = 'new@example.com'
db.session.commit()
# 删除
db.session.delete(user)
db.session.commit()
# 事务处理
try:
user1 = User(username='user1', email='user1@example.com')
user1.set_password('pass1')
user2 = User(username='user2', email='user2@example.com')
user2.set_password('pass2')
db.session.add_all([user1, user2])
db.session.commit()
except:
db.session.rollback()
raise7. 用户认证与会话:保护用户数据
用户认证是Web应用的核心功能之一。Flask-Login提供了会话管理和用户认证的基本功能。
# app/routes.py(续 - 用户相关路由)
@main.route('/profile')
@login_required
def profile():
return render_template('profile.html', user=current_user)
@main.route('/profile/edit', methods=['GET', 'POST'])
@login_required
def edit_profile():
form = EditProfileForm()
if form.validate_on_submit():
if form.username.data != current_user.username:
# 检查用户名是否已存在
existing_user = User.query.filter_by(
username=form.username.data
).first()
if existing_user:
flash('该用户名已被使用', 'danger')
return render_template('edit_profile.html', form=form)
if form.email.data != current_user.email:
# 检查邮箱是否已存在
existing_email = User.query.filter_by(
email=form.email.data
).first()
if existing_email:
flash('该邮箱已被注册', 'danger')
return render_template('edit_profile.html', form=form)
# 更新用户信息
current_user.username = form.username.data
current_user.email = form.email.data
if form.about_me.data:
current_user.about_me = form.about_me.data
db.session.commit()
flash('个人信息已更新', 'success')
return redirect(url_for('main.profile'))
elif request.method == 'GET':
form.username.data = current_user.username
form.email.data = current_user.email
form.about_me.data = current_user.about_me
return render_template('edit_profile.html', form=form)
@main.route('/change-password', methods=['GET', 'POST'])
@login_required
def change_password():
form = ChangePasswordForm()
if form.validate_on_submit():
if not current_user.check_password(form.current_password.data):
flash('当前密码错误', 'danger')
return render_template('change_password.html', form=form)
current_user.set_password(form.new_password.data)
db.session.commit()
flash('密码已修改', 'success')
return redirect(url_for('main.profile'))
return render_template('change_password.html', form=form)
# 管理员功能示例
@main.route('/admin')
@login_required
def admin_panel():
# 检查用户是否是管理员
if not current_user.is_admin:
flash('需要管理员权限', 'danger')
return redirect(url_for('main.index'))
users = User.query.all()
posts = Post.query.all()
return render_template('admin.html', users=users, posts=posts)
# 密码重置功能
@main.route('/reset-password', methods=['GET', 'POST'])
def reset_password_request():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = ResetPasswordRequestForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user:
# 发送密码重置邮件(实际项目中需要实现)
send_password_reset_email(user)
flash('如果该邮箱已注册,您将收到密码重置说明', 'info')
return redirect(url_for('main.login'))
return render_template('reset_password_request.html', form=form)
@main.route('/reset-password/<token>', methods=['GET', 'POST'])
def reset_password(token):
if current_user.is_authenticated:
return redirect(url_for('main.index'))
user = User.verify_reset_password_token(token)
if not user:
flash('无效或过期的重置链接', 'danger')
return redirect(url_for('main.index'))
form = ResetPasswordForm()
if form.validate_on_submit():
user.set_password(form.password.data)
db.session.commit()
flash('密码已重置', 'success')
return redirect(url_for('main.login'))
return render_template('reset_password.html', form=form)会话安全要点:
- 使用安全的Session配置
- 设置合理的Session过期时间
- 使用HTTPS传输Session Cookie
- 实现密码重置的安全流程
8. 部署Web应用:从开发到生产
将Flask应用部署到生产环境需要考虑性能、安全和可维护性。以下是使用Gunicorn和Nginx部署到Linux服务器的步骤。
# 1. 服务器准备
sudo apt update
sudo apt install python3-pip python3-venv nginx
# 2. 创建部署用户
sudo useradd -m -s /bin/bash deploy
sudo passwd deploy
sudo usermod -aG sudo deploy
# 3. 设置项目目录
sudo mkdir -p /var/www/myblog
sudo chown -R deploy:deploy /var/www/myblog
# 4. 克隆代码(或上传文件)
cd /var/www/myblog
git clone <你的仓库地址> .创建生产环境配置文件:
# config.py(生产环境配置)
import os
class ProductionConfig(Config):
DEBUG = False
TESTING = False
# 使用环境变量中的密钥
SECRET_KEY = os.environ.get('SECRET_KEY')
# 生产数据库
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
# 安全相关设置
SESSION_COOKIE_SECURE = True # 仅HTTPS
REMEMBER_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
REMEMBER_COOKIE_HTTPONLY = True
# 邮件配置
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT', 587))
MAIL_USE_TLS = True
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER')创建Gunicorn配置文件:
# gunicorn_config.py
bind = "0.0.0.0:8000"
workers = 3
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
# 日志配置
accesslog = "/var/log/myblog/access.log"
errorlog = "/var/log/myblog/error.log"
loglevel = "warning"
# 进程名称
proc_name = "myblog"创建Systemd服务文件:
# /etc/systemd/system/myblog.service
[Unit]
Description=MyBlog Flask Application
After=network.target
[Service]
User=deploy
Group=deploy
WorkingDirectory=/var/www/myblog
Environment="PATH=/var/www/myblog/venv/bin"
Environment="FLASK_APP=run.py"
Environment="FLASK_ENV=production"
ExecStart=/var/www/myblog/venv/bin/gunicorn --config gunicorn_config.py run:app
[Install]
WantedBy=multi-user.target配置Nginx:
# /etc/nginx/sites-available/myblog
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# 重定向HTTP到HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
# SSL证书
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# SSL配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 静态文件
location /static {
alias /var/www/myblog/app/static;
expires 30d;
add_header Cache-Control "public, immutable";
}
# 媒体文件
location /uploads {
alias /var/www/myblog/uploads;
expires 30d;
add_header Cache-Control "public";
}
# 代理到Gunicorn
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 安全头部
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self';" always;
}部署脚本:
#!/bin/bash
# deploy.sh
echo "开始部署MyBlog应用..."
# 激活虚拟环境
cd /var/www/myblog
source venv/bin/activate
# 拉取最新代码
git pull origin main
# 安装依赖
pip install -r requirements.txt
# 运行数据库迁移
flask db upgrade
# 收集静态文件(如果有的话)
# flask assets build
# 重启服务
sudo systemctl restart myblog
sudo systemctl reload nginx
echo "部署完成!"生产环境检查清单:
- [ ] 关闭调试模式
- [ ] 使用强密钥和数据库密码
- [ ] 配置HTTPS
- [ ] 设置正确的文件权限
- [ ] 配置日志轮转
- [ ] 设置备份策略
- [ ] 配置监控和告警
- [ ] 实现CI/CD流程
总结
Flask提供了一条从初学者到专业开发者的清晰路径。我们从最简单的"Hello World"开始,逐步构建了一个功能完整的博客系统,涵盖了路由、模板、表单、数据库、用户认证等核心概念。
关键要点:
- Flask的微内核设计让你可以按需添加功能
- Jinja2模板提供了强大的动态HTML生成能力
- SQLAlchemy让数据库操作变得简单而安全
- 合理的项目结构是长期维护的基础
- 生产部署需要考虑性能、安全和监控
Web开发是一个持续学习的过程。掌握了Flask的基础后,你可以继续探索更高级的主题,如REST API设计、异步处理、缓存优化、容器化部署等。
记住,好的Web应用不仅仅是功能的堆砌,更是用户体验、性能和安全的完美结合。从这个小博客开始,去创造更大的世界吧!