作为一个编程入门新手,Flask是我接触到的第一个Web框架。想要深入学习,就从《FlaskWeb开发:基于Python的Web应用开发实战》这本书入手,本书由于是翻译过来的中文版,理解起来不是很顺畅。但是对着代码理解也是能应对的,学到 第七章:大型程序结构 这章节的时候,发现难度有所提升,网上能参考的完整实例没有,于是根据自己的理解记下来。
程序结构图:
README
(1)本程序是基于Flask微型Web框架开发,使用Jinja2模版引擎 (2)页面展示了一个文本框和一个按钮,输入文本框点击按钮提交,文本框为空无法提交(输入文本框的数据为一个模拟用户); (3)当在文本框中输入新用户提交,欢迎词和文本框中输入老用户提交不一致; (4)文本框输入新用户提交后,将新用户保存至SQLite数据库,并使用异步发送邮件至管理员邮箱; (5)页面刷新,浏览器不会再次提示:是否提交 项目结构 flasky # 程序根目录 ├── app # 核心模块目录 │ ├── email.py # 邮件发送模版 │ ├── __init__.py │ ├── main # 蓝图模块目录 │ │ ├── errors.py # 错误处理模块 │ │ ├── forms.py # 页面表单模块 │ │ ├── __init__.py │ │ └── views.py # 正常处理模块 │ ├── models.py # 对象关系映射模块 │ ├── static # 页面静态资源目录 │ │ └── favicon.ico # 页面收藏夹图标 │ └── templates # 默认存放页面模版目录 │ ├── 404.html │ ├── base.html │ ├── index.html │ ├── mail # 邮件模块目录 │ │ ├── new_user.html │ │ └── new_user.txt │ └── user.html ├── config.py # 程序配置文件 ├── data-dev.sqlite # 程序数据库文件 ├── manage.py # 程序管理启动文件 ├── migrations # 数据库迁移目录 │ ├── alembic.ini │ ├── env.py │ ├── README │ ├── script.py.mako │ └── versions ├── requirements.txt # 所有依赖包文件 └── tests # 测试文件目录 ├── __init__.py └── test_basics.py
程序代码总汇
"/"
# -*- coding: utf-8 -*- # Author: hkey import os basedir = os.path.abspath(os.path.dirname(__file__)) class Config(object): # 所有配置类的父类,通用的配置写在这里 SECRET_KEY = os.environ.get(\'SECRET_KEY\') or \'hard to guess string\' SQLALCHEMY_COMMIT_ON_TEARDOWN = True SQLALCHEMY_TRACK_MODIFICATIONS = True FLASKY_MAIL_SUBJECT_PREFIX = \'[Flasky]\' FLASKY_MAIL_SENDER = \'Flasky Admin <xxx@126.com>\' FLASKY_ADMIN = \'xxx@qq.com\' @staticmethod def init_app(app): # 静态方法作为配置的统一接口,暂时为空 pass class DevelopmentConfig(Config): # 开发环境配置类 DEBUG = True MAIL_SERVER = \'smtp.126.com\' MAIL_PORT = 465 MAIL_USE_SSL = True MAIL_USERNAME = \'xxx@126.com\' MAIL_PASSWORD = \'xxxxxx\' SQLALCHEMY_DATABASE_URI = \ \'sqlite:///\' + os.path.join(basedir, \'data-dev.sqlite\') class TestingConfig(Config): # 测试环境配置类 TESTING = True SQLALCHEMY_DATABASE_URI = \ \'sqlite:///\' + os.path.join(basedir, \'data-test.sqlite\') class ProductionConfig(Config): # 生产环境配置类 SQLALCHEMY_DATABASE_URI = \ \'sqlite:///\' + os.path.join(basedir, \'data.sqlite\') config = { # config字典注册了不同的配置,默认配置为开发环境,本例使用开发环境 \'development\': DevelopmentConfig, \'testing\': TestingConfig, \'production\': ProductionConfig, \'default\': DevelopmentConfig }
# -*- coding: utf-8 -*- # Author: hkey import os from app import create_app, db from app.models import User, Role from flask_script import Manager, Shell from flask_migrate import Migrate, MigrateCommand app = create_app(os.getenv(\'FLASK_CONFIG\') or \'default\') manager = Manager(app) migrate = Migrate(app, db) def make_shell_context(): return dict(app=app, db=db, User=User, Role=Role) manager.add_command(\'shell\', Shell(make_context=make_shell_context)) manager.add_command(\'db\', MigrateCommand) @manager.command def test(): import unittest tests = unittest.TestLoader().discover(\'tests\') unittest.TextTestRunner(verbosity=2).run(tests) if __name__ == \'__main__\': manager.run()
"/app"
# -*- coding: utf-8 -*- # Author: hkey from flask import Flask, render_template from flask_bootstrap import Bootstrap from flask_sqlalchemy import SQLAlchemy from flask_mail import Mail from config import config # 由于尚未初始化所需的程序实例,所以没有初始化扩展,创建扩展类时没有向构造函数传入参数。 bootstrap = Bootstrap() mail = Mail() db = SQLAlchemy() def create_app(config_name): \'\'\'工厂函数\'\'\' app = Flask(__name__) app.config.from_object(config[config_name]) config[config_name].init_app(app) # 通过config.py统一接口 bootstrap.init_app(app) # 该init_app是bootstrap实例的方法调用,与上面毫无关系 mail.init_app(app) # 同上 db.init_app(app) # 同上 # 附加路由和自定义错误页面,将蓝本注册到工厂函数 from .main import main as main_blueprint app.register_blueprint(main_blueprint) return app
# -*- coding: utf-8 -*- # Author: hkey from threading import Thread from flask import render_template, current_app from flask_mail import Message from . import mail def send_async_mail(app, msg): \'\'\'创建邮件发送函数\'\'\' with app.app_context(): mail.send(msg) def send_mail(to, subject, template, **kwargs): app = current_app._get_current_object() if app.config[\'FLASKY_ADMIN\']: msg = Message(app.config[\'FLASKY_MAIL_SUBJECT_PREFIX\'] + subject, sender=app.config[\'FLASKY_MAIL_SENDER\'], recipients=[to]) msg.body = render_template(template + \'.txt\', **kwargs) msg.html = render_template(template + \'.html\', **kwargs) thr = Thread(target=send_async_mail, args=(app, msg)) thr.start() # 通过创建子线程实现异步发送邮件 return thr
# -*- coding: utf-8 -*- # Author: hkey # 对象关系映射类 from . import db class Role(db.Model): __tablename__ = \'roles\' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), unique=True, index=True) users = db.relationship(\'User\', backref=\'role\', lazy=\'dynamic\') def __repr__(self): return \'<Role %r>\' % self.name class User(db.Model): __tablename__ = \'users\' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), unique=True, index=True) role_id = db.Column(db.Integer, db.ForeignKey(\'roles.id\')) def __repr__(self): return \'<User %r>\' % self.username
\'\'/app/main"
# -*- coding: utf-8 -*- # Author: hkey from flask import Blueprint # 定义蓝本 main = Blueprint(\'main\', __name__) from . import views, errors
# -*- coding: utf-8 -*- # Author: hkey from flask import render_template from . import main @main.app_errorhandler(404) # 路由装饰器由蓝本提供,这里要调用 app_errorhandler 而不是 errorhandler def page_not_found(e): return render_template(\'404.html\'), 404 @main.app_errorhandler(500) def internal_server_error(e): return render_template(\'500.html\'), 500
# -*- coding: utf-8 -*- # Author: hkey from flask_wtf import FlaskForm from wtforms import StringField, SubmitField from wtforms.validators import Required class NameForm(FlaskForm): \'\'\'通过 flask-wtf 定义表单类\'\'\' name = StringField(\'What is your name ?\', validators=[Required()]) # 文本框 submit = SubmitField(\'Submit\') # 按钮
# -*- coding: utf-8 -*- # Author: hkey from flask import render_template, session, redirect, url_for, current_app from . import main from .forms import NameForm from .. import db from ..models import User from ..email import send_mail @main.route(\'/\', methods=[\'GET\', \'POST\']) def index(): form = NameForm() if form.validate_on_submit(): user = User.query.filter_by(username=form.name.data).first() # 查询数据库是否有该用户 if user is None: # 如果没有该用户,就保存到数据库中 user = User(username=form.name.data) db.session.add(user) session[\'known\'] = False # 通过session保存 known为False,通过web渲染需要 if current_app.config[\'FLASKY_ADMIN\']: # 如果配置变量有flasky管理员就发送邮件 # 异步发送邮件 send_mail(current_app.config[\'FLASKY_ADMIN\'], \'New User\', \'mail/new_user\', user=user) else: session[\'known\'] = True session[\'name\'] = form.name.data form.name.data = \'\' return redirect(url_for(\'.index\')) # 通过redirect避免用户刷新重复提交 return render_template(\'index.html\', form=form, name=session.get(\'name\'), known=session.get(\'known\', False))
"/app/main/templates" 页面
<!DOCTYPE html> {% extends "bootstrap/base.html" %} {% block title %}Flasky{% endblock %} {% block head %} {{ super() }} <link rel="shortcut icon" href="{{ url_for(\'static\', filename = \'favicon.ico\')}}" type="image/x-icon"> <link rel="icon" href="{{ url_for(\'static\', filename = \'favicon.ico\')}}" type="image/x-icon"> {% endblock %} {% block navbar %} <div class="navbar navbar-inverse" role="navigation"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="/">Flasky</a> </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li><a href="/">Home</a></li> </ul> </div> </div> </div> {% endblock %} {% block content %} <div class="container"> {% for message in get_flashed_messages() %} <div class="alert alert-warning"> <button type="button" class="close" data-dismiss="alert">×</button> {{ message }} </div> {% endfor %} {% block page_content %}{% endblock %} </div> {% endblock %}
<!DOCTYPE html> {% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} {% block title %}Flasky{% endblock %} {% block page_content %} <div class="page-header"> <h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1> {% if not known %} <p>Pleased to meet you!</p> {% else %} <p>Happy to see you again!</p> {% endif %} </div> {{ wtf.quick_form(form) }} {% endblock %}
<!DOCTYPE html> {% extends "base.html" %} {% block title %}Flasky - Page Not Found{% endblock %} {% block page_content %} <div class="page-header"> <h1>Not Found!</h1> </div> {% endblock %}
"/app/main/templates/mail" 邮件模版
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> User <b>{{ user.username }}</b> has joined. </head> <body> </body> </html>
User {{ user.username }} has joined.
"/app/main/static/favicon.ico" 静态 icon 图片文件
创建需求文件
程序中必须包含一个 requirements.txt 文件,用于记录所有依赖包及其精确的版本号。如果要在另一台电脑上重新生成虚拟环境,这个文件的重要性就体现出来了,例如部署程序时使用的电脑。
(venv) E:\flasky>pip3 freeze > requirements.txt
创建数据库
(venv) E:\flasky>python manage.py shell >>> db.create_all() >>> exit()
生成数据库迁移文件
(venv) E:\flasky>python manage.py db init Creating directory E:\flasky\migrations ... done Creating directory E:\flasky\migrations\versions ... done Generating E:\flasky\migrations\alembic.ini ... done Generating E:\flasky\migrations\env.py ... done Generating E:\flasky\migrations\README ... done Generating E:\flasky\migrations\script.py.mako ... done Please edit configuration/connection/logging settings in \'E:\\flasky\\migrations\\alembic.ini\' before proceeding. (venv) E:\flasky>python manage.py db migrate -m "initial migration" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.env] No changes in schema detected. (venv) E:\flasky>python manage.py db upgrade INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL.
运行测试
(venv) E:\flasky>python manage.py test test_app_exists (test_basics.BasicsTestCase) 确保程序实例存在 ... ok test_app_is_testing (test_basics.BasicsTestCase) 确保程序在测试中运行 ... ok ---------------------------------------------------------------------- Ran 2 tests in 2.232s OK
启动程序
(venv) E:\flasky>python manage.py runserver * Serving Flask app "app" (lazy loading) * Environment: production WARNING: Do not use the development server in a production environment. Use a production WSGI server instead. * Debug mode: on * Restarting with stat * Debugger is active! * Debugger PIN: 138-639-525 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
浏览器输入 http://127.0.0.1:5000
输入用户名并提交:
程序会异步发送邮件,程序控制台会打印发送日志。已收到邮件: