|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
1. Flask简介与入门
Flask是一个用Python编写的轻量级Web应用框架。它被称为微框架,因为它不需要特定的工具或库,没有数据库抽象层、表单验证或其他第三方库提供的常见功能。然而,Flask支持扩展,可以添加这些功能到应用程序中,就像它们是在Flask本身中实现的一样。
1.1 为什么选择Flask?
• 轻量级:核心简单但易于扩展
• 灵活性:没有强制性的项目结构或依赖关系
• 易于学习:API简单直观,文档完善
• 强大的社区:拥有丰富的扩展和活跃的社区支持
1.2 安装Flask
在开始之前,确保你已经安装了Python。然后通过pip安装Flask:
1.3 第一个Flask应用
让我们创建一个简单的”Hello World”应用:
- from flask import Flask
- app = Flask(__name__)
- @app.route('/')
- def hello_world():
- return 'Hello, World!'
- if __name__ == '__main__':
- app.run(debug=True)
复制代码
将这段代码保存为app.py,然后在命令行中运行:
现在,打开浏览器并访问http://127.0.0.1:5000/,你将看到”Hello, World!“的输出。
2. Flask基础概念
2.1 路由
路由是URL与Python函数之间的映射关系。在Flask中,使用@app.route()装饰器来定义路由:
- @app.route('/user/<username>')
- def show_user_profile(username):
- # 显示用户的个人资料
- return f'User: {username}'
- @app.route('/post/<int:post_id>')
- def show_post(post_id):
- # 显示给定id的帖子,id是整数
- return f'Post: {post_id}'
复制代码
在上面的例子中,<username>是一个字符串类型的变量部分,而<int:post_id>是一个整数类型的变量部分。
2.2 模板渲染
Flask使用Jinja2模板引擎。创建一个名为templates的文件夹,并在其中创建一个HTML文件:
- <!-- templates/hello.html -->
- <!DOCTYPE html>
- <html>
- <head>
- <title>Hello from Flask</title>
- </head>
- <body>
- <h1>Hello, {{ name }}!</h1>
- </body>
- </html>
复制代码
然后在Flask应用中使用render_template函数渲染这个模板:
- from flask import render_template
- @app.route('/hello/<name>')
- def hello(name):
- return render_template('hello.html', name=name)
复制代码
2.3 静态文件
静态文件如CSS、JavaScript和图像文件通常放在名为static的文件夹中。在模板中,可以使用url_for('static', filename='style.css')来生成静态文件的URL。
- <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
复制代码
2.4 请求处理
Flask提供了全局的request对象来访问传入的请求数据:
- from flask import request
- @app.route('/login', methods=['GET', 'POST'])
- def login():
- if request.method == 'POST':
- username = request.form['username']
- password = request.form['password']
- # 验证用户名和密码
- return f'Logged in as {username}'
- else:
- return '''
- <form method="post">
- <p><input type=text name=username>
- <p><input type=password name=password>
- <p><input type=submit value=Login>
- </form>
- '''
复制代码
3. Flask进阶功能
3.1 数据库集成
Flask本身不包含数据库抽象层,但有许多扩展可以帮助你与数据库交互。最常用的是Flask-SQLAlchemy,它为SQLAlchemy提供了Flask集成。
首先安装Flask-SQLAlchemy:
- pip install flask-sqlalchemy
复制代码
然后配置数据库:
- from flask import Flask
- from flask_sqlalchemy import SQLAlchemy
- app = Flask(__name__)
- # 配置SQLite数据库
- app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///mydatabase.db'
- app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
- db = SQLAlchemy(app)
- # 定义模型
- class User(db.Model):
- id = db.Column(db.Integer, primary_key=True)
- username = db.Column(db.String(80), unique=True, nullable=False)
- email = db.Column(db.String(120), unique=True, nullable=False)
- def __repr__(self):
- return f'<User {self.username}>'
- # 创建数据库表
- @app.before_first_request
- def create_tables():
- db.create_all()
- @app.route('/add_user')
- def add_user():
- user = User(username='john', email='john@example.com')
- db.session.add(user)
- db.session.commit()
- return 'User added!'
- @app.route('/users')
- def get_users():
- users = User.query.all()
- return '<br>'.join([f'{user.username}: {user.email}' for user in users])
复制代码
3.2 表单处理
Flask-WTF扩展简化了Web表单的处理。首先安装必要的包:
- pip install flask-wtf
- pip install email-validator
复制代码
然后创建一个表单类:
- from flask_wtf import FlaskForm
- from wtforms import StringField, PasswordField, SubmitField
- from wtforms.validators import DataRequired, Email, Length
- class RegistrationForm(FlaskForm):
- username = StringField('Username',
- validators=[DataRequired(), Length(min=4, max=20)])
- email = StringField('Email',
- validators=[DataRequired(), Email()])
- password = PasswordField('Password',
- validators=[DataRequired(), Length(min=8)])
- submit = SubmitField('Register')
复制代码
在视图函数中使用表单:
- @app.route('/register', methods=['GET', 'POST'])
- def register():
- 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()
- return 'Registration successful!'
- return render_template('register.html', form=form)
复制代码
对应的模板register.html:
- <!DOCTYPE html>
- <html>
- <head>
- <title>Register</title>
- </head>
- <body>
- <h1>Register</h1>
- <form method="POST" action="">
- {{ form.hidden_tag() }}
- <p>
- {{ form.username.label }}<br>
- {{ form.username(size=32) }}
- </p>
- <p>
- {{ form.email.label }}<br>
- {{ form.email(size=32) }}
- </p>
- <p>
- {{ form.password.label }}<br>
- {{ form.password(size=32) }}
- </p>
- <p>{{ form.submit() }}</p>
- </form>
- </body>
- </html>
复制代码
3.3 用户认证
Flask-Login扩展提供了用户会话管理功能。首先安装:
然后配置Flask-Login:
- from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
- login_manager = LoginManager()
- login_manager.init_app(app)
- login_manager.login_view = 'login'
- # User模型需要继承UserMixin
- class User(UserMixin, db.Model):
- # ...之前的字段...
- password_hash = db.Column(db.String(128))
-
- 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)
- @login_manager.user_loader
- def load_user(user_id):
- return User.query.get(int(user_id))
- @app.route('/login', methods=['GET', 'POST'])
- def login():
- if current_user.is_authenticated:
- return redirect(url_for('index'))
-
- form = LoginForm()
- if form.validate_on_submit():
- user = User.query.filter_by(username=form.username.data).first()
- if user is None or not user.check_password(form.password.data):
- flash('Invalid username or password')
- return redirect(url_for('login'))
- login_user(user, remember=form.remember_me.data)
- return redirect(url_for('index'))
-
- return render_template('login.html', title='Sign In', form=form)
- @app.route('/logout')
- def logout():
- logout_user()
- return redirect(url_for('index'))
- @app.route('/protected')
- @login_required
- def protected():
- return 'This is a protected page. Only logged in users can see this.'
复制代码
3.4 RESTful API
Flask可以轻松创建RESTful API。以下是一个简单的示例:
- from flask import jsonify
- @app.route('/api/users', methods=['GET'])
- def get_users():
- users = User.query.all()
- return jsonify([{'username': user.username, 'email': user.email} for user in users])
- @app.route('/api/users/<int:user_id>', methods=['GET'])
- def get_user(user_id):
- user = User.query.get_or_404(user_id)
- return jsonify({'username': user.username, 'email': user.email})
- @app.route('/api/users', methods=['POST'])
- def create_user():
- data = request.get_json()
- if not data or not 'username' in data or not 'email' in data:
- return jsonify({'error': 'Bad request'}), 400
-
- user = User(username=data['username'], email=data['email'])
- db.session.add(user)
- db.session.commit()
- return jsonify({'message': 'User created successfully'}), 201
- @app.route('/api/users/<int:user_id>', methods=['PUT'])
- def update_user(user_id):
- user = User.query.get_or_404(user_id)
- data = request.get_json()
-
- if 'username' in data:
- user.username = data['username']
- if 'email' in data:
- user.email = data['email']
-
- db.session.commit()
- return jsonify({'message': 'User updated successfully'})
- @app.route('/api/users/<int:user_id>', methods=['DELETE'])
- def delete_user(user_id):
- user = User.query.get_or_404(user_id)
- db.session.delete(user)
- db.session.commit()
- return jsonify({'message': 'User deleted successfully'})
复制代码
4. 实战项目示例
4.1 博客系统
让我们创建一个简单的博客系统,包括文章发布、显示和评论功能。
- blog/
- app.py
- config.py
- models.py
- forms.py
- templates/
- base.html
- index.html
- post.html
- create_post.html
- register.html
- login.html
- static/
- css/
- style.css
复制代码- # config.py
- import os
- class Config:
- SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key'
- SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///blog.db'
- SQLALCHEMY_TRACK_MODIFICATIONS = False
复制代码- # models.py
- from datetime import datetime
- from flask_sqlalchemy import SQLAlchemy
- from flask_login import UserMixin
- from werkzeug.security import generate_password_hash, check_password_hash
- db = SQLAlchemy()
- class User(UserMixin, db.Model):
- id = db.Column(db.Integer, primary_key=True)
- username = db.Column(db.String(64), index=True, unique=True)
- email = db.Column(db.String(120), index=True, unique=True)
- password_hash = db.Column(db.String(128))
- posts = db.relationship('Post', backref='author', lazy='dynamic')
- 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 __repr__(self):
- return f'<User {self.username}>'
- class Post(db.Model):
- id = db.Column(db.Integer, primary_key=True)
- title = db.Column(db.String(140))
- body = db.Column(db.Text)
- timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
- user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
- comments = db.relationship('Comment', backref='post', lazy='dynamic')
- def __repr__(self):
- return f'<Post {self.title}>'
- class Comment(db.Model):
- id = db.Column(db.Integer, primary_key=True)
- body = db.Column(db.Text)
- timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
- user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
- post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
- def __repr__(self):
- return f'<Comment {self.body[:20]}...>'
复制代码- # forms.py
- from flask_wtf import FlaskForm
- from wtforms import StringField, PasswordField, BooleanField, SubmitField, TextAreaField
- from wtforms.validators import DataRequired, Email, EqualTo, ValidationError, Length
- from models import User
- class LoginForm(FlaskForm):
- username = StringField('Username', validators=[DataRequired()])
- password = PasswordField('Password', validators=[DataRequired()])
- remember_me = BooleanField('Remember Me')
- submit = SubmitField('Sign In')
- class RegistrationForm(FlaskForm):
- username = StringField('Username', validators=[DataRequired(), Length(min=4, max=20)])
- email = StringField('Email', validators=[DataRequired(), Email()])
- password = PasswordField('Password', validators=[DataRequired(), Length(min=8)])
- password2 = PasswordField('Repeat Password', validators=[DataRequired(), EqualTo('password')])
- submit = SubmitField('Register')
- def validate_username(self, username):
- user = User.query.filter_by(username=username.data).first()
- if user is not None:
- raise ValidationError('Please use a different username.')
- def validate_email(self, email):
- user = User.query.filter_by(email=email.data).first()
- if user is not None:
- raise ValidationError('Please use a different email address.')
- class PostForm(FlaskForm):
- title = StringField('Title', validators=[DataRequired(), Length(max=140)])
- body = TextAreaField('Content', validators=[DataRequired(), Length(min=1, max=2000)])
- submit = SubmitField('Submit')
- class CommentForm(FlaskForm):
- body = TextAreaField('Comment', validators=[DataRequired(), Length(min=1, max=500)])
- submit = SubmitField('Submit')
复制代码- # app.py
- from flask import Flask, render_template, flash, redirect, url_for, request
- from flask_login import LoginManager, current_user, login_user, logout_user, login_required
- from werkzeug.urls import url_parse
- from datetime import datetime
- from config import Config
- from models import db, User, Post, Comment
- from forms import LoginForm, RegistrationForm, PostForm, CommentForm
- app = Flask(__name__)
- app.config.from_object(Config)
- db.init_app(app)
- login_manager = LoginManager()
- login_manager.init_app(app)
- login_manager.login_view = 'login'
- @login_manager.user_loader
- def load_user(user_id):
- return User.query.get(int(user_id))
- @app.before_request
- def before_request():
- if current_user.is_authenticated:
- current_user.last_seen = datetime.utcnow()
- db.session.commit()
- @app.route('/', methods=['GET', 'POST'])
- @app.route('/index', methods=['GET', 'POST'])
- def index():
- page = request.args.get('page', 1, type=int)
- posts = Post.query.order_by(Post.timestamp.desc()).paginate(
- page, app.config['POSTS_PER_PAGE'], False)
- next_url = url_for('index', page=posts.next_num) if posts.has_next else None
- prev_url = url_for('index', page=posts.prev_num) if posts.has_prev else None
- return render_template('index.html', title='Home', posts=posts.items,
- next_url=next_url, prev_url=prev_url)
- @app.route('/login', methods=['GET', 'POST'])
- def login():
- if current_user.is_authenticated:
- return redirect(url_for('index'))
- form = LoginForm()
- if form.validate_on_submit():
- user = User.query.filter_by(username=form.username.data).first()
- if user is None or not user.check_password(form.password.data):
- flash('Invalid username or password')
- return redirect(url_for('login'))
- login_user(user, remember=form.remember_me.data)
- next_page = request.args.get('next')
- if not next_page or url_parse(next_page).netloc != '':
- next_page = url_for('index')
- return redirect(next_page)
- return render_template('login.html', title='Sign In', form=form)
- @app.route('/logout')
- def logout():
- logout_user()
- return redirect(url_for('index'))
- @app.route('/register', methods=['GET', 'POST'])
- def register():
- if current_user.is_authenticated:
- return redirect(url_for('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('Congratulations, you are now a registered user!')
- return redirect(url_for('login'))
- return render_template('register.html', title='Register', form=form)
- @app.route('/create_post', methods=['GET', 'POST'])
- @login_required
- def create_post():
- form = PostForm()
- if form.validate_on_submit():
- post = Post(title=form.title.data, body=form.body.data, author=current_user)
- db.session.add(post)
- db.session.commit()
- flash('Your post is now live!')
- return redirect(url_for('index'))
- return render_template('create_post.html', title='Create Post', form=form)
- @app.route('/post/<int:id>', methods=['GET', 'POST'])
- def post(id):
- post = Post.query.get_or_404(id)
- form = CommentForm()
- if form.validate_on_submit():
- comment = Comment(body=form.body.data, post=post, author=current_user)
- db.session.add(comment)
- db.session.commit()
- flash('Your comment has been published.')
- return redirect(url_for('post', id=post.id))
- comments = Comment.query.filter_by(post_id=post.id).order_by(Comment.timestamp.desc()).all()
- return render_template('post.html', post=post, comments=comments, form=form)
- @app.route('/user/<username>')
- @login_required
- def user(username):
- user = User.query.filter_by(username=username).first_or_404()
- page = request.args.get('page', 1, type=int)
- posts = user.posts.order_by(Post.timestamp.desc()).paginate(
- page, app.config['POSTS_PER_PAGE'], False)
- next_url = url_for('user', username=user.username, page=posts.next_num) if posts.has_next else None
- prev_url = url_for('user', username=user.username, page=posts.prev_num) if posts.has_prev else None
- return render_template('user.html', user=user, posts=posts.items,
- next_url=next_url, prev_url=prev_url)
- if __name__ == '__main__':
- app.run(debug=True)
复制代码
base.html:
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- <title>{{ title }} - Blog</title>
- <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
- </head>
- <body>
- <header>
- <nav>
- <a href="{{ url_for('index') }}">Home</a>
- {% if current_user.is_anonymous %}
- <a href="{{ url_for('login') }}">Login</a>
- {% else %}
- <a href="{{ url_for('user', username=current_user.username) }}">Profile</a>
- <a href="{{ url_for('create_post') }}">New Post</a>
- <a href="{{ url_for('logout') }}">Logout</a>
- {% endif %}
- </nav>
- </header>
- <main>
- {% with messages = get_flashed_messages() %}
- {% if messages %}
- <div class="flash-messages">
- {% for message in messages %}
- <div class="flash">{{ message }}</div>
- {% endfor %}
- </div>
- {% endif %}
- {% endwith %}
-
- {% block content %}{% endblock %}
- </main>
- <footer>
- <p>Blog © 2023</p>
- </footer>
- </body>
- </html>
复制代码
index.html:
- {% extends "base.html" %}
- {% block content %}
- <h1>Blog Posts</h1>
-
- {% if current_user.is_authenticated %}
- <p><a href="{{ url_for('create_post') }}">Create a new post</a></p>
- {% endif %}
-
- {% for post in posts %}
- <article class="post">
- <h2><a href="{{ url_for('post', id=post.id) }}">{{ post.title }}</a></h2>
- <div class="post-meta">
- Posted by <a href="{{ url_for('user', username=post.author.username) }}">{{ post.author.username }}</a>
- on {{ post.timestamp.strftime('%Y-%m-%d') }}
- </div>
- <div class="post-body">
- {{ post.body[:200] }}{% if post.body|length > 200 %}...{% endif %}
- </div>
- </article>
- {% endfor %}
-
- {% if prev_url %}
- <a href="{{ prev_url }}">Newer posts</a>
- {% endif %}
- {% if next_url %}
- <a href="{{ next_url }}">Older posts</a>
- {% endif %}
- {% endblock %}
复制代码
post.html:
- {% extends "base.html" %}
- {% block content %}
- <article class="post">
- <h1>{{ post.title }}</h1>
- <div class="post-meta">
- Posted by <a href="{{ url_for('user', username=post.author.username) }}">{{ post.author.username }}</a>
- on {{ post.timestamp.strftime('%Y-%m-%d %H:%M') }}
- </div>
- <div class="post-body">
- {{ post.body }}
- </div>
- </article>
-
- <section class="comments">
- <h2>Comments</h2>
-
- {% if current_user.is_authenticated %}
- <form action="" method="post" class="comment-form">
- {{ form.hidden_tag() }}
- <div>
- {{ form.body.label }}<br>
- {{ form.body(cols=50, rows=4) }}
- </div>
- <div>{{ form.submit() }}</div>
- </form>
- {% endif %}
-
- {% for comment in comments %}
- <div class="comment">
- <div class="comment-meta">
- <a href="{{ url_for('user', username=comment.author.username) }}">{{ comment.author.username }}</a>
- on {{ comment.timestamp.strftime('%Y-%m-%d %H:%M') }}
- </div>
- <div class="comment-body">
- {{ comment.body }}
- </div>
- </div>
- {% endfor %}
- </section>
- {% endblock %}
复制代码
create_post.html:
- {% extends "base.html" %}
- {% block content %}
- <h1>Create Post</h1>
- <form action="" method="post">
- {{ form.hidden_tag() }}
- <div>
- {{ form.title.label }}<br>
- {{ form.title(size=50) }}
- </div>
- <div>
- {{ form.body.label }}<br>
- {{ form.body(cols=50, rows=10) }}
- </div>
- <div>{{ form.submit() }}</div>
- </form>
- {% endblock %}
复制代码
login.html:
- {% extends "base.html" %}
- {% block content %}
- <h1>Sign In</h1>
- <form action="" method="post">
- {{ form.hidden_tag() }}
- <div>
- {{ form.username.label }}<br>
- {{ form.username(size=32) }}
- </div>
- <div>
- {{ form.password.label }}<br>
- {{ form.password(size=32) }}
- </div>
- <div>
- {{ form.remember_me() }} {{ form.remember_me.label }}
- </div>
- <div>{{ form.submit() }}</div>
- </form>
- <p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>
- {% endblock %}
复制代码
register.html:
- {% extends "base.html" %}
- {% block content %}
- <h1>Register</h1>
- <form action="" method="post">
- {{ form.hidden_tag() }}
- <div>
- {{ form.username.label }}<br>
- {{ form.username(size=32) }}
- </div>
- <div>
- {{ form.email.label }}<br>
- {{ form.email(size=32) }}
- </div>
- <div>
- {{ form.password.label }}<br>
- {{ form.password(size=32) }}
- </div>
- <div>
- {{ form.password2.label }}<br>
- {{ form.password2(size=32) }}
- </div>
- <div>{{ form.submit() }}</div>
- </form>
- {% endblock %}
复制代码
user.html:
- {% extends "base.html" %}
- {% block content %}
- <h1>User: {{ user.username }}</h1>
-
- {% for post in posts %}
- <article class="post">
- <h2><a href="{{ url_for('post', id=post.id) }}">{{ post.title }}</a></h2>
- <div class="post-meta">
- Posted on {{ post.timestamp.strftime('%Y-%m-%d') }}
- </div>
- <div class="post-body">
- {{ post.body[:200] }}{% if post.body|length > 200 %}...{% endif %}
- </div>
- </article>
- {% endfor %}
-
- {% if prev_url %}
- <a href="{{ prev_url }}">Newer posts</a>
- {% endif %}
- {% if next_url %}
- <a href="{{ next_url }}">Older posts</a>
- {% endif %}
- {% endblock %}
复制代码
4.2 电子商务网站
让我们创建一个简单的电子商务网站,包括产品展示、购物车和订单功能。
- ecommerce/
- app.py
- config.py
- models.py
- forms.py
- templates/
- base.html
- index.html
- product.html
- cart.html
- checkout.html
- order_confirmation.html
- static/
- css/
- style.css
复制代码- # models.py
- from datetime import datetime
- from flask_sqlalchemy import SQLAlchemy
- from flask_login import UserMixin
- from werkzeug.security import generate_password_hash, check_password_hash
- db = SQLAlchemy()
- class User(UserMixin, db.Model):
- id = db.Column(db.Integer, primary_key=True)
- username = db.Column(db.String(64), index=True, unique=True)
- email = db.Column(db.String(120), index=True, unique=True)
- password_hash = db.Column(db.String(128))
- orders = db.relationship('Order', backref='user', lazy='dynamic')
- 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 __repr__(self):
- return f'<User {self.username}>'
- class Product(db.Model):
- id = db.Column(db.Integer, primary_key=True)
- name = db.Column(db.String(100), index=True)
- description = db.Column(db.Text)
- price = db.Column(db.Float)
- image_url = db.Column(db.String(200))
- stock = db.Column(db.Integer, default=0)
- order_items = db.relationship('OrderItem', backref='product', lazy='dynamic')
- def __repr__(self):
- return f'<Product {self.name}>'
- class Order(db.Model):
- id = db.Column(db.Integer, primary_key=True)
- user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
- created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow)
- status = db.Column(db.String(20), default='pending') # pending, paid, shipped, delivered, cancelled
- total_amount = db.Column(db.Float)
- shipping_address = db.Column(db.Text)
- payment_method = db.Column(db.String(50))
- items = db.relationship('OrderItem', backref='order', lazy='dynamic')
- def __repr__(self):
- return f'<Order {self.id}>'
- class OrderItem(db.Model):
- id = db.Column(db.Integer, primary_key=True)
- order_id = db.Column(db.Integer, db.ForeignKey('order.id'))
- product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
- quantity = db.Column(db.Integer)
- price = db.Column(db.Float) # Price at the time of purchase
- def __repr__(self):
- return f'<OrderItem {self.id}>'
复制代码
由于购物车是临时性的,我们可以使用Flask的session来存储购物车数据:
- # cart.py
- from flask import session
- from models import Product
- def get_cart():
- cart = session.get('cart', {})
- return cart
- def add_to_cart(product_id, quantity=1):
- cart = get_cart()
- product_id = str(product_id)
-
- if product_id in cart:
- cart[product_id] += quantity
- else:
- cart[product_id] = quantity
-
- session['cart'] = cart
- def remove_from_cart(product_id):
- cart = get_cart()
- product_id = str(product_id)
-
- if product_id in cart:
- del cart[product_id]
- session['cart'] = cart
- def update_cart(product_id, quantity):
- cart = get_cart()
- product_id = str(product_id)
-
- if quantity > 0:
- cart[product_id] = quantity
- else:
- if product_id in cart:
- del cart[product_id]
-
- session['cart'] = cart
- def get_cart_items():
- cart = get_cart()
- items = []
-
- for product_id, quantity in cart.items():
- product = Product.query.get(int(product_id))
- if product:
- items.append({
- 'product': product,
- 'quantity': quantity,
- 'total': product.price * quantity
- })
-
- return items
- def get_cart_total():
- items = get_cart_items()
- return sum(item['total'] for item in items)
- def clear_cart():
- session['cart'] = {}
复制代码- # forms.py
- from flask_wtf import FlaskForm
- from wtforms import StringField, PasswordField, BooleanField, SubmitField, TextAreaField, IntegerField, SelectField
- from wtforms.validators import DataRequired, Email, EqualTo, ValidationError, NumberRange
- from models import User
- class LoginForm(FlaskForm):
- username = StringField('Username', validators=[DataRequired()])
- password = PasswordField('Password', validators=[DataRequired()])
- remember_me = BooleanField('Remember Me')
- submit = SubmitField('Sign In')
- class RegistrationForm(FlaskForm):
- username = StringField('Username', validators=[DataRequired()])
- email = StringField('Email', validators=[DataRequired(), Email()])
- password = PasswordField('Password', validators=[DataRequired()])
- password2 = PasswordField('Repeat Password', validators=[DataRequired(), EqualTo('password')])
- submit = SubmitField('Register')
- def validate_username(self, username):
- user = User.query.filter_by(username=username.data).first()
- if user is not None:
- raise ValidationError('Please use a different username.')
- def validate_email(self, email):
- user = User.query.filter_by(email=email.data).first()
- if user is not None:
- raise ValidationError('Please use a different email address.')
- class CheckoutForm(FlaskForm):
- full_name = StringField('Full Name', validators=[DataRequired()])
- address = TextAreaField('Address', validators=[DataRequired()])
- city = StringField('City', validators=[DataRequired()])
- zip_code = StringField('ZIP Code', validators=[DataRequired()])
- country = StringField('Country', validators=[DataRequired()])
- payment_method = SelectField('Payment Method', choices=[
- ('credit_card', 'Credit Card'),
- ('paypal', 'PayPal'),
- ('bank_transfer', 'Bank Transfer')
- ], validators=[DataRequired()])
- submit = SubmitField('Place Order')
复制代码- # app.py
- from flask import Flask, render_template, flash, redirect, url_for, request, session
- from flask_login import LoginManager, current_user, login_user, logout_user, login_required
- from werkzeug.urls import url_parse
- from config import Config
- from models import db, User, Product, Order, OrderItem
- from forms import LoginForm, RegistrationForm, CheckoutForm
- from cart import get_cart, get_cart_items, get_cart_total, clear_cart
- app = Flask(__name__)
- app.config.from_object(Config)
- db.init_app(app)
- login_manager = LoginManager()
- login_manager.init_app(app)
- login_manager.login_view = 'login'
- @login_manager.user_loader
- def load_user(user_id):
- return User.query.get(int(user_id))
- @app.route('/')
- def index():
- products = Product.query.all()
- return render_template('index.html', products=products)
- @app.route('/product/<int:id>')
- def product(id):
- product = Product.query.get_or_404(id)
- return render_template('product.html', product=product)
- @app.route('/add_to_cart/<int:product_id>')
- def add_to_cart(product_id):
- product = Product.query.get_or_404(product_id)
- quantity = int(request.args.get('quantity', 1))
-
- if product.stock < quantity:
- flash('Not enough stock available!')
- return redirect(url_for('product', id=product_id))
-
- from cart import add_to_cart as cart_add
- cart_add(product_id, quantity)
- flash(f'{product.name} added to cart!')
- return redirect(url_for('cart'))
- @app.route('/cart')
- def cart():
- items = get_cart_items()
- total = get_cart_total()
- return render_template('cart.html', items=items, total=total)
- @app.route('/update_cart/<int:product_id>')
- def update_cart(product_id):
- quantity = int(request.args.get('quantity', 1))
- from cart import update_cart as cart_update
- cart_update(product_id, quantity)
- return redirect(url_for('cart'))
- @app.route('/remove_from_cart/<int:product_id>')
- def remove_from_cart(product_id):
- from cart import remove_from_cart as cart_remove
- cart_remove(product_id)
- return redirect(url_for('cart'))
- @app.route('/checkout', methods=['GET', 'POST'])
- @login_required
- def checkout():
- form = CheckoutForm()
- items = get_cart_items()
- total = get_cart_total()
-
- if not items:
- flash('Your cart is empty!')
- return redirect(url_for('index'))
-
- if form.validate_on_submit():
- # Create order
- order = Order(
- user_id=current_user.id,
- total_amount=total,
- shipping_address=f"{form.full_name.data}\n{form.address.data}\n{form.city.data}, {form.zip_code.data}\n{form.country.data}",
- payment_method=form.payment_method.data
- )
- db.session.add(order)
-
- # Add order items
- for item in items:
- order_item = OrderItem(
- order_id=order.id,
- product_id=item['product'].id,
- quantity=item['quantity'],
- price=item['product'].price
- )
- db.session.add(order_item)
-
- # Update product stock
- product = item['product']
- product.stock -= item['quantity']
-
- db.session.commit()
-
- # Clear cart
- clear_cart()
-
- flash('Order placed successfully!')
- return redirect(url_for('order_confirmation', order_id=order.id))
-
- return render_template('checkout.html', form=form, items=items, total=total)
- @app.route('/order_confirmation/<int:order_id>')
- @login_required
- def order_confirmation(order_id):
- order = Order.query.get_or_404(order_id)
-
- # Check if the order belongs to the current user
- if order.user_id != current_user.id:
- flash('You do not have permission to view this order.')
- return redirect(url_for('index'))
-
- return render_template('order_confirmation.html', order=order)
- @app.route('/login', methods=['GET', 'POST'])
- def login():
- if current_user.is_authenticated:
- return redirect(url_for('index'))
- form = LoginForm()
- if form.validate_on_submit():
- user = User.query.filter_by(username=form.username.data).first()
- if user is None or not user.check_password(form.password.data):
- flash('Invalid username or password')
- return redirect(url_for('login'))
- login_user(user, remember=form.remember_me.data)
- next_page = request.args.get('next')
- if not next_page or url_parse(next_page).netloc != '':
- next_page = url_for('index')
- return redirect(next_page)
- return render_template('login.html', title='Sign In', form=form)
- @app.route('/logout')
- def logout():
- logout_user()
- return redirect(url_for('index'))
- @app.route('/register', methods=['GET', 'POST'])
- def register():
- if current_user.is_authenticated:
- return redirect(url_for('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('Congratulations, you are now a registered user!')
- return redirect(url_for('login'))
- return render_template('register.html', title='Register', form=form)
- if __name__ == '__main__':
- app.run(debug=True)
复制代码
base.html:
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- <title>{{ title }} - E-commerce Store</title>
- <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
- </head>
- <body>
- <header>
- <div class="container">
- <div class="logo">
- <a href="{{ url_for('index') }}">E-commerce Store</a>
- </div>
- <nav>
- <a href="{{ url_for('index') }}">Home</a>
- {% if current_user.is_anonymous %}
- <a href="{{ url_for('login') }}">Login</a>
- <a href="{{ url_for('register') }}">Register</a>
- {% else %}
- <a href="{{ url_for('logout') }}">Logout</a>
- {% endif %}
- <a href="{{ url_for('cart') }}">Cart ({{ get_cart()|length }})</a>
- </nav>
- </div>
- </header>
- <main class="container">
- {% with messages = get_flashed_messages() %}
- {% if messages %}
- <div class="flash-messages">
- {% for message in messages %}
- <div class="flash">{{ message }}</div>
- {% endfor %}
- </div>
- {% endif %}
- {% endwith %}
-
- {% block content %}{% endblock %}
- </main>
- <footer>
- <div class="container">
- <p>© 2023 E-commerce Store. All rights reserved.</p>
- </div>
- </footer>
- </body>
- </html>
复制代码
index.html:
- {% extends "base.html" %}
- {% block content %}
- <h1>Our Products</h1>
-
- <div class="products-grid">
- {% for product in products %}
- <div class="product-card">
- {% if product.image_url %}
- <img src="{{ product.image_url }}" alt="{{ product.name }}">
- {% else %}
- <div class="product-image-placeholder">No Image</div>
- {% endif %}
- <h2>{{ product.name }}</h2>
- <p class="price">${{ "%.2f"|format(product.price) }}</p>
- <p class="stock">In stock: {{ product.stock }}</p>
- <a href="{{ url_for('product', id=product.id) }}" class="btn">View Details</a>
- </div>
- {% endfor %}
- </div>
- {% endblock %}
复制代码
product.html:
- {% extends "base.html" %}
- {% block content %}
- <div class="product-detail">
- <div class="product-image">
- {% if product.image_url %}
- <img src="{{ product.image_url }}" alt="{{ product.name }}">
- {% else %}
- <div class="product-image-placeholder">No Image</div>
- {% endif %}
- </div>
- <div class="product-info">
- <h1>{{ product.name }}</h1>
- <p class="price">${{ "%.2f"|format(product.price) }}</p>
- <p class="stock">In stock: {{ product.stock }}</p>
- <div class="description">
- <h3>Description</h3>
- <p>{{ product.description }}</p>
- </div>
-
- <form action="{{ url_for('add_to_cart', product_id=product.id) }}" method="get">
- <div class="quantity-selector">
- <label for="quantity">Quantity:</label>
- <input type="number" id="quantity" name="quantity" min="1" max="{{ product.stock }}" value="1">
- </div>
- <button type="submit" class="btn btn-primary">Add to Cart</button>
- </form>
- </div>
- </div>
- {% endblock %}
复制代码
cart.html:
- {% extends "base.html" %}
- {% block content %}
- <h1>Shopping Cart</h1>
-
- {% if items %}
- <table class="cart-table">
- <thead>
- <tr>
- <th>Product</th>
- <th>Price</th>
- <th>Quantity</th>
- <th>Total</th>
- <th>Actions</th>
- </tr>
- </thead>
- <tbody>
- {% for item in items %}
- <tr>
- <td>
- <div class="cart-item-info">
- {% if item.product.image_url %}
- <img src="{{ item.product.image_url }}" alt="{{ item.product.name }}" class="cart-item-image">
- {% endif %}
- <a href="{{ url_for('product', id=item.product.id) }}">{{ item.product.name }}</a>
- </div>
- </td>
- <td>${{ "%.2f"|format(item.product.price) }}</td>
- <td>
- <form action="{{ url_for('update_cart', product_id=item.product.id) }}" method="get" class="quantity-form">
- <input type="number" name="quantity" min="1" max="{{ item.product.stock }}" value="{{ item.quantity }}">
- <button type="submit">Update</button>
- </form>
- </td>
- <td>${{ "%.2f"|format(item.total) }}</td>
- <td>
- <a href="{{ url_for('remove_from_cart', product_id=item.product.id) }}" class="btn btn-danger">Remove</a>
- </td>
- </tr>
- {% endfor %}
- </tbody>
- <tfoot>
- <tr>
- <td colspan="3" class="cart-total-label">Total:</td>
- <td colspan="2" class="cart-total-amount">${{ "%.2f"|format(total) }}</td>
- </tr>
- </tfoot>
- </table>
-
- <div class="cart-actions">
- <a href="{{ url_for('index') }}" class="btn">Continue Shopping</a>
- {% if current_user.is_authenticated %}
- <a href="{{ url_for('checkout') }}" class="btn btn-primary">Proceed to Checkout</a>
- {% else %}
- <a href="{{ url_for('login') }}" class="btn btn-primary">Login to Checkout</a>
- {% endif %}
- </div>
- {% else %}
- <div class="empty-cart">
- <p>Your cart is empty.</p>
- <a href="{{ url_for('index') }}" class="btn">Continue Shopping</a>
- </div>
- {% endif %}
- {% endblock %}
复制代码
checkout.html:
- {% extends "base.html" %}
- {% block content %}
- <h1>Checkout</h1>
-
- <div class="checkout-container">
- <div class="checkout-form">
- <h2>Shipping Information</h2>
- <form action="" method="post">
- {{ form.hidden_tag() }}
- <div class="form-group">
- {{ form.full_name.label }}
- {{ form.full_name(class="form-control") }}
- </div>
- <div class="form-group">
- {{ form.address.label }}
- {{ form.address(class="form-control") }}
- </div>
- <div class="form-row">
- <div class="form-group">
- {{ form.city.label }}
- {{ form.city(class="form-control") }}
- </div>
- <div class="form-group">
- {{ form.zip_code.label }}
- {{ form.zip_code(class="form-control") }}
- </div>
- </div>
- <div class="form-group">
- {{ form.country.label }}
- {{ form.country(class="form-control") }}
- </div>
- <div class="form-group">
- {{ form.payment_method.label }}
- {{ form.payment_method(class="form-control") }}
- </div>
- <div class="form-group">
- {{ form.submit(class="btn btn-primary") }}
- </div>
- </form>
- </div>
-
- <div class="order-summary">
- <h2>Order Summary</h2>
- <table class="order-summary-table">
- <thead>
- <tr>
- <th>Product</th>
- <th>Quantity</th>
- <th>Price</th>
- </tr>
- </thead>
- <tbody>
- {% for item in items %}
- <tr>
- <td>{{ item.product.name }}</td>
- <td>{{ item.quantity }}</td>
- <td>${{ "%.2f"|format(item.total) }}</td>
- </tr>
- {% endfor %}
- </tbody>
- <tfoot>
- <tr>
- <td colspan="2">Total:</td>
- <td>${{ "%.2f"|format(total) }}</td>
- </tr>
- </tfoot>
- </table>
- </div>
- </div>
- {% endblock %}
复制代码
order_confirmation.html:
- {% extends "base.html" %}
- {% block content %}
- <div class="order-confirmation">
- <h1>Order Confirmation</h1>
- <div class="confirmation-message">
- <p>Thank you for your order!</p>
- <p>Your order number is: <strong>#{{ order.id }}</strong></p>
- <p>We've received your order and will begin processing it soon.</p>
- </div>
-
- <div class="order-details">
- <h2>Order Details</h2>
- <div class="order-info">
- <p><strong>Order Date:</strong> {{ order.created_at.strftime('%Y-%m-%d %H:%M') }}</p>
- <p><strong>Status:</strong> {{ order.status.title() }}</p>
- <p><strong>Payment Method:</strong> {{ order.payment_method.replace('_', ' ').title() }}</p>
- <p><strong>Total Amount:</strong> ${{ "%.2f"|format(order.total_amount) }}</p>
- </div>
-
- <div class="shipping-info">
- <h3>Shipping Address</h3>
- <p>{{ order.shipping_address|replace('\n', '<br>')|safe }}</p>
- </div>
-
- <div class="order-items">
- <h3>Items Ordered</h3>
- <table class="order-items-table">
- <thead>
- <tr>
- <th>Product</th>
- <th>Quantity</th>
- <th>Price</th>
- </tr>
- </thead>
- <tbody>
- {% for item in order.items %}
- <tr>
- <td>{{ item.product.name }}</td>
- <td>{{ item.quantity }}</td>
- <td>${{ "%.2f"|format(item.price * item.quantity) }}</td>
- </tr>
- {% endfor %}
- </tbody>
- </table>
- </div>
- </div>
-
- <div class="confirmation-actions">
- <a href="{{ url_for('index') }}" class="btn">Continue Shopping</a>
- </div>
- </div>
- {% endblock %}
复制代码- /* static/css/style.css */
- * {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
- }
- body {
- font-family: Arial, sans-serif;
- line-height: 1.6;
- color: #333;
- background-color: #f8f9fa;
- }
- .container {
- max-width: 1200px;
- margin: 0 auto;
- padding: 0 15px;
- }
- /* Header */
- header {
- background-color: #fff;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
- position: sticky;
- top: 0;
- z-index: 100;
- }
- header .container {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 15px;
- }
- .logo a {
- font-size: 24px;
- font-weight: bold;
- color: #333;
- text-decoration: none;
- }
- nav a {
- margin-left: 20px;
- color: #333;
- text-decoration: none;
- font-weight: 500;
- }
- nav a:hover {
- color: #007bff;
- }
- /* Main Content */
- main {
- padding: 30px 0;
- min-height: calc(100vh - 160px);
- }
- /* Buttons */
- .btn {
- display: inline-block;
- background-color: #f8f9fa;
- color: #333;
- border: 1px solid #ddd;
- padding: 8px 16px;
- border-radius: 4px;
- text-decoration: none;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.3s ease;
- }
- .btn:hover {
- background-color: #e9ecef;
- border-color: #adb5bd;
- }
- .btn-primary {
- background-color: #007bff;
- color: #fff;
- border-color: #007bff;
- }
- .btn-primary:hover {
- background-color: #0069d9;
- border-color: #0062cc;
- }
- .btn-danger {
- background-color: #dc3545;
- color: #fff;
- border-color: #dc3545;
- }
- .btn-danger:hover {
- background-color: #c82333;
- border-color: #bd2130;
- }
- /* Forms */
- .form-group {
- margin-bottom: 15px;
- }
- .form-row {
- display: flex;
- gap: 15px;
- }
- .form-row .form-group {
- flex: 1;
- }
- .form-control {
- display: block;
- width: 100%;
- padding: 8px 12px;
- font-size: 16px;
- line-height: 1.5;
- color: #495057;
- background-color: #fff;
- background-clip: padding-box;
- border: 1px solid #ced4da;
- border-radius: 4px;
- transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
- }
- .form-control:focus {
- color: #495057;
- background-color: #fff;
- border-color: #80bdff;
- outline: 0;
- box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
- }
- /* Flash Messages */
- .flash-messages {
- margin-bottom: 20px;
- }
- .flash {
- padding: 15px;
- margin-bottom: 10px;
- border: 1px solid transparent;
- border-radius: 4px;
- background-color: #d4edda;
- border-color: #c3e6cb;
- color: #155724;
- }
- /* Products Grid */
- .products-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
- gap: 20px;
- }
- .product-card {
- background-color: #fff;
- border-radius: 8px;
- overflow: hidden;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- transition: transform 0.3s ease, box-shadow 0.3s ease;
- }
- .product-card:hover {
- transform: translateY(-5px);
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
- }
- .product-card img {
- width: 100%;
- height: 200px;
- object-fit: cover;
- }
- .product-image-placeholder {
- width: 100%;
- height: 200px;
- background-color: #e9ecef;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #6c757d;
- }
- .product-card h2 {
- font-size: 18px;
- margin: 10px;
- }
- .product-card .price {
- font-size: 18px;
- font-weight: bold;
- color: #007bff;
- margin: 0 10px;
- }
- .product-card .stock {
- font-size: 14px;
- color: #6c757d;
- margin: 0 10px 10px;
- }
- .product-card .btn {
- margin: 0 10px 15px;
- }
- /* Product Detail */
- .product-detail {
- display: flex;
- gap: 30px;
- background-color: #fff;
- padding: 20px;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- }
- .product-image {
- flex: 1;
- }
- .product-image img {
- width: 100%;
- border-radius: 8px;
- }
- .product-info {
- flex: 1;
- }
- .product-info h1 {
- margin-bottom: 10px;
- }
- .product-info .price {
- font-size: 24px;
- font-weight: bold;
- color: #007bff;
- margin-bottom: 10px;
- }
- .product-info .stock {
- font-size: 16px;
- color: #6c757d;
- margin-bottom: 20px;
- }
- .description {
- margin-bottom: 20px;
- }
- .description h3 {
- margin-bottom: 10px;
- }
- .quantity-selector {
- display: flex;
- align-items: center;
- margin-bottom: 15px;
- }
- .quantity-selector label {
- margin-right: 10px;
- }
- .quantity-selector input {
- width: 60px;
- padding: 5px;
- border: 1px solid #ced4da;
- border-radius: 4px;
- }
- /* Cart */
- .cart-table {
- width: 100%;
- border-collapse: collapse;
- margin-bottom: 20px;
- background-color: #fff;
- border-radius: 8px;
- overflow: hidden;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- }
- .cart-table th, .cart-table td {
- padding: 12px 15px;
- text-align: left;
- border-bottom: 1px solid #dee2e6;
- }
- .cart-table th {
- background-color: #f8f9fa;
- font-weight: 600;
- }
- .cart-item-info {
- display: flex;
- align-items: center;
- }
- .cart-item-image {
- width: 50px;
- height: 50px;
- object-fit: cover;
- margin-right: 10px;
- border-radius: 4px;
- }
- .quantity-form {
- display: flex;
- align-items: center;
- }
- .quantity-form input {
- width: 50px;
- padding: 5px;
- border: 1px solid #ced4da;
- border-radius: 4px;
- margin-right: 5px;
- }
- .cart-total-label {
- text-align: right;
- font-weight: 600;
- }
- .cart-total-amount {
- font-weight: bold;
- font-size: 18px;
- color: #007bff;
- }
- .cart-actions {
- display: flex;
- justify-content: space-between;
- margin-top: 20px;
- }
- .empty-cart {
- text-align: center;
- padding: 40px;
- background-color: #fff;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- }
- .empty-cart p {
- margin-bottom: 20px;
- font-size: 18px;
- }
- /* Checkout */
- .checkout-container {
- display: flex;
- gap: 30px;
- }
- .checkout-form {
- flex: 3;
- background-color: #fff;
- padding: 20px;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- }
- .order-summary {
- flex: 2;
- background-color: #fff;
- padding: 20px;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- height: fit-content;
- }
- .order-summary-table {
- width: 100%;
- border-collapse: collapse;
- }
- .order-summary-table th, .order-summary-table td {
- padding: 10px;
- text-align: left;
- border-bottom: 1px solid #dee2e6;
- }
- .order-summary-table th {
- background-color: #f8f9fa;
- font-weight: 600;
- }
- /* Order Confirmation */
- .order-confirmation {
- background-color: #fff;
- padding: 30px;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- }
- .confirmation-message {
- text-align: center;
- margin-bottom: 30px;
- padding: 20px;
- background-color: #d4edda;
- border-radius: 8px;
- color: #155724;
- }
- .confirmation-message p {
- margin-bottom: 10px;
- font-size: 18px;
- }
- .order-details {
- margin-bottom: 30px;
- }
- .order-details h2 {
- margin-bottom: 20px;
- padding-bottom: 10px;
- border-bottom: 1px solid #dee2e6;
- }
- .order-info {
- margin-bottom: 20px;
- }
- .order-info p {
- margin-bottom: 8px;
- }
- .shipping-info {
- margin-bottom: 20px;
- }
- .shipping-info h3 {
- margin-bottom: 10px;
- }
- .order-items-table {
- width: 100%;
- border-collapse: collapse;
- }
- .order-items-table th, .order-items-table td {
- padding: 10px;
- text-align: left;
- border-bottom: 1px solid #dee2e6;
- }
- .order-items-table th {
- background-color: #f8f9fa;
- font-weight: 600;
- }
- .confirmation-actions {
- text-align: center;
- }
- /* Footer */
- footer {
- background-color: #333;
- color: #fff;
- padding: 20px 0;
- text-align: center;
- }
复制代码
5. Flask高级主题
5.1 蓝图(Blueprints)
蓝图是Flask中组织应用程序的一种方式,允许你将应用程序分割成更小、更可管理的组件。这对于大型应用程序特别有用。
让我们创建一个简单的示例,展示如何使用蓝图:
- # app.py
- from flask import Flask, render_template
- from admin import admin_bp
- from blog import blog_bp
- app = Flask(__name__)
- # 注册蓝图
- app.register_blueprint(admin_bp, url_prefix='/admin')
- app.register_blueprint(blog_bp)
- @app.route('/')
- def index():
- return render_template('index.html')
- if __name__ == '__main__':
- app.run(debug=True)
复制代码- # admin.py
- from flask import Blueprint, render_template
- admin_bp = Blueprint('admin', __name__,
- template_folder='templates',
- static_folder='static')
- @admin_bp.route('/')
- def admin_home():
- return render_template('admin/index.html')
- @admin_bp.route('/users')
- def admin_users():
- return render_template('admin/users.html')
复制代码- # blog.py
- from flask import Blueprint, render_template
- blog_bp = Blueprint('blog', __name__,
- template_folder='templates',
- static_folder='static')
- @blog_bp.route('/')
- def blog_home():
- return render_template('blog/index.html')
- @blog_bp.route('/post/<int:post_id>')
- def blog_post(post_id):
- return render_template('blog/post.html', post_id=post_id)
复制代码
5.2 应用工厂模式
应用工厂模式是一种创建Flask应用实例的方法,它允许你在不同的环境中配置应用,并且使测试更容易。
- # app/__init__.py
- from flask import Flask
- from flask_sqlalchemy import SQLAlchemy
- from flask_login import LoginManager
- from config import Config
- db = SQLAlchemy()
- login_manager = LoginManager()
- 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.main import bp as main_bp
- app.register_blueprint(main_bp)
-
- from app.admin import bp as admin_bp
- app.register_blueprint(admin_bp, url_prefix='/admin')
-
- from app.auth import bp as auth_bp
- app.register_blueprint(auth_bp, url_prefix='/auth')
-
- return app
复制代码- # config.py
- import os
- basedir = os.path.abspath(os.path.dirname(__file__))
- class Config:
- SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
- SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
- 'sqlite:///' + os.path.join(basedir, 'app.db')
- SQLALCHEMY_TRACK_MODIFICATIONS = False
复制代码- # run.py
- from app import create_app
- app = create_app()
- if __name__ == '__main__':
- app.run(debug=True)
复制代码
5.3 RESTful API与Flask-RESTful
Flask-RESTful是一个Flask扩展,用于快速构建REST API。首先安装:
- pip install flask-restful
复制代码
然后创建一个简单的API:
- from flask import Flask
- from flask_restful import reqparse, Api, Resource
- app = Flask(__name__)
- api = Api(app)
- TODOS = {
- 'todo1': {'task': 'build an API'},
- 'todo2': {'task': '?????'},
- 'todo3': {'task': 'profit!'},
- }
- def abort_if_todo_doesnt_exist(todo_id):
- if todo_id not in TODOS:
- api.abort(404, message="Todo {} doesn't exist".format(todo_id))
- parser = reqparse.RequestParser()
- parser.add_argument('task', type=str)
- # Todo
- # shows a single todo item and lets you delete a todo item
- class Todo(Resource):
- def get(self, todo_id):
- abort_if_todo_doesnt_exist(todo_id)
- return TODOS[todo_id]
- def delete(self, todo_id):
- abort_if_todo_doesnt_exist(todo_id)
- del TODOS[todo_id]
- return '', 204
- def put(self, todo_id):
- abort_if_todo_doesnt_exist(todo_id)
- args = parser.parse_args()
- task = {'task': args['task']}
- TODOS[todo_id] = task
- return task, 201
- # TodoList
- # shows a list of all todos, and lets you POST to add new tasks
- class TodoList(Resource):
- def get(self):
- return TODOS
- def post(self):
- args = parser.parse_args()
- todo_id = int(max(TODOS.keys()).lstrip('todo')) + 1
- todo_id = 'todo%i' % todo_id
- TODOS[todo_id] = {'task': args['task']}
- return TODOS[todo_id], 201
- ##
- ## Actually setup the Api resource routing here
- ##
- api.add_resource(TodoList, '/todos')
- api.add_resource(Todo, '/todos/<todo_id>')
- if __name__ == '__main__':
- app.run(debug=True)
复制代码
5.4 WebSocket与Flask-SocketIO
Flask-SocketIO为Flask应用程序添加了对WebSocket的支持。首先安装:
- pip install flask-socketio
复制代码
然后创建一个简单的聊天应用:
- from flask import Flask, render_template
- from flask_socketio import SocketIO, emit
- app = Flask(__name__)
- app.config['SECRET_KEY'] = 'your-secret-key'
- socketio = SocketIO(app)
- @app.route('/')
- def index():
- return render_template('chat.html')
- @socketio.on('connect')
- def test_connect():
- emit('my response', {'data': 'Connected'})
- @socketio.on('disconnect')
- def test_disconnect():
- print('Client disconnected')
- @socketio.on('message')
- def handle_message(message):
- print('received message: ' + message)
- emit('message', message, broadcast=True)
- if __name__ == '__main__':
- socketio.run(app, debug=True)
复制代码
对应的模板templates/chat.html:
- <!DOCTYPE html>
- <html>
- <head>
- <title>Flask-SocketIO Chat</title>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
- <style>
- body {
- font-family: Arial, sans-serif;
- margin: 0;
- padding: 0;
- }
- #messages {
- list-style-type: none;
- margin: 0;
- padding: 0;
- height: 300px;
- overflow-y: auto;
- border: 1px solid #ddd;
- padding: 10px;
- }
- #messages li {
- padding: 5px 10px;
- margin-bottom: 5px;
- border-radius: 5px;
- }
- #messages li:nth-child(odd) {
- background-color: #f9f9f9;
- }
- #message-form {
- display: flex;
- margin-top: 10px;
- }
- #message-input {
- flex: 1;
- padding: 10px;
- border: 1px solid #ddd;
- border-radius: 5px 0 0 5px;
- }
- #send-button {
- padding: 10px 20px;
- background-color: #007bff;
- color: white;
- border: none;
- border-radius: 0 5px 5px 0;
- cursor: pointer;
- }
- #send-button:hover {
- background-color: #0069d9;
- }
- </style>
- </head>
- <body>
- <h1>Flask-SocketIO Chat</h1>
- <ul id="messages"></ul>
- <form id="message-form">
- <input id="message-input" autocomplete="off" />
- <button id="send-button">Send</button>
- </form>
- <script>
- document.addEventListener('DOMContentLoaded', () => {
- const socket = io();
- const messages = document.getElementById('messages');
- const messageForm = document.getElementById('message-form');
- const messageInput = document.getElementById('message-input');
- socket.on('connect', () => {
- console.log('Connected to server');
- });
- socket.on('message', (msg) => {
- const item = document.createElement('li');
- item.textContent = msg;
- messages.appendChild(item);
- messages.scrollTop = messages.scrollHeight;
- });
- messageForm.addEventListener('submit', (e) => {
- e.preventDefault();
- if (messageInput.value) {
- socket.emit('message', messageInput.value);
- messageInput.value = '';
- }
- });
- });
- </script>
- </body>
- </html>
复制代码
6. Flask应用部署
6.1 使用Gunicorn部署
Gunicorn是一个WSGI HTTP服务器,用于UNIX系统上的Python应用。首先安装:
然后运行你的Flask应用:
- gunicorn -w 4 -b 127.0.0.1:8000 app:app
复制代码
这会启动4个工作进程,监听在8000端口。
6.2 使用uWSGI部署
uWSGI是另一个流行的WSGI服务器。首先安装:
然后创建一个uWSGI配置文件uwsgi.ini:
- [uwsgi]
- module = app:app
- master = true
- processes = 4
- socket = myapp.sock
- chmod-socket = 666
- vacuum = true
- die-on-term = true
复制代码
然后运行:
6.3 使用Docker部署
Docker可以创建一个包含你的应用及其所有依赖的容器。首先创建一个Dockerfile:
- FROM python:3.9-slim
- WORKDIR /app
- COPY requirements.txt .
- RUN pip install --no-cache-dir -r requirements.txt
- COPY . .
- EXPOSE 5000
- CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]
复制代码
然后构建并运行Docker容器:
- docker build -t my-flask-app .
- docker run -p 5000:5000 my-flask-app
复制代码
6.4 使用Nginx作为反向代理
Nginx可以作为反向代理,将请求转发到你的Flask应用。以下是一个基本的Nginx配置:
- server {
- listen 80;
- server_name example.com;
- 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;
- }
- location /static {
- alias /path/to/your/static/files;
- expires 30d;
- }
- }
复制代码
7. Flask最佳实践
7.1 项目结构
一个好的项目结构可以使你的应用更易于维护和扩展。以下是一个推荐的项目结构:
- myproject/
- app/
- __init__.py
- config.py
- extensions.py
- models/
- __init__.py
- user.py
- post.py
- routes/
- __init__.py
- main.py
- auth.py
- api.py
- templates/
- base.html
- index.html
- auth/
- login.html
- register.html
- static/
- css/
- style.css
- js/
- main.js
- utils/
- __init__.py
- decorators.py
- forms.py
- migrations/
- tests/
- __init__.py
- test_models.py
- test_routes.py
- requirements.txt
- config.py
- run.py
- .env
- .gitignore
复制代码
7.2 配置管理
使用环境变量和配置类来管理不同环境的配置:
- # config.py
- import os
- from dotenv import load_dotenv
- basedir = os.path.abspath(os.path.dirname(__file__))
- load_dotenv(os.path.join(basedir, '.env'))
- class Config:
- SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
- SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
- 'sqlite:///' + os.path.join(basedir, 'app.db')
- SQLALCHEMY_TRACK_MODIFICATIONS = False
- class DevelopmentConfig(Config):
- DEBUG = True
- class ProductionConfig(Config):
- DEBUG = False
- class TestingConfig(Config):
- TESTING = True
- SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
- config = {
- 'development': DevelopmentConfig,
- 'production': ProductionConfig,
- 'testing': TestingConfig,
- 'default': DevelopmentConfig
- }
复制代码
7.3 错误处理
使用Flask的错误处理功能来提供友好的错误页面:
- @app.errorhandler(404)
- def not_found_error(error):
- return render_template('errors/404.html'), 404
- @app.errorhandler(500)
- def internal_error(error):
- db.session.rollback()
- return render_template('errors/500.html'), 500
复制代码
7.4 日志记录
配置日志记录以便于调试和监控:
- import logging
- from logging.handlers import RotatingFileHandler
- import os
- if not app.debug:
- if not os.path.exists('logs'):
- os.mkdir('logs')
- file_handler = RotatingFileHandler('logs/myapp.log', maxBytes=10240, backupCount=10)
- file_handler.setFormatter(logging.Formatter(
- '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
- file_handler.setLevel(logging.INFO)
- app.logger.addHandler(file_handler)
- app.logger.setLevel(logging.INFO)
- app.logger.info('MyApp startup')
复制代码
7.5 安全最佳实践
• 使用HTTPS保护你的应用
• 使用CSRF保护表单
• 验证和净化用户输入
• 使用安全的密码哈希
• 设置适当的CORS策略
• 定期更新依赖项
- from flask_wtf.csrf import CSRFProtect
- from flask_talisman import Talisman
- csrf = CSRFProtect(app)
- Talisman(app, force_https=True)
复制代码
8. 总结
Flask是一个强大而灵活的Python Web框架,它提供了构建Web应用所需的基本工具,同时允许你根据需要添加扩展。通过本教程,你已经学习了从基础到高级的Flask开发技能,包括:
• Flask基础概念和路由
• 模板渲染和静态文件处理
• 数据库集成和表单处理
• 用户认证和授权
• RESTful API开发
• 实战项目示例(博客系统和电子商务网站)
• 高级主题(蓝图、应用工厂、WebSocket等)
• 应用部署和最佳实践
现在你已经具备了使用Flask开发各种Web应用的能力。继续探索Flask的丰富生态系统,不断实践和学习,你将成为一名优秀的Flask开发者。 |
|