个人博客Demo: link.
GitHub项目完整链接:link
回顾上一节主要讲了以下5个方面内容:
- 项目组织结构
- 编写配置文件 settings.py
- 拓展实例化 extensions.py
- 定义数据库模型 models.py
- 编写生成虚拟数据的函数 fakes.py
2.2.1 构造文件__init__.py
- 程序实例在工厂函数中创建。工厂函数一般在程序包的构造文件__init__.py中创建,一般命名为create_app()
- 在之前提到过蓝图就像是模子,而工厂函数(create_app)就是用这个模子(blueprint)加上其他的材料(配置文件,程序上下文等)生产出对应的可直接运行的程序实例
- 蓝图和工厂函数结合使用,使得测试和部署更加方便,因为我们不用将加载的配置写死在某处,而直接在不同的地方创建不同的程序实例
由于我们还没有开始编写程序的视图函数,所以在这里先在bluemaps文件夹下的admin,blog和login文件中创建对应蓝图实例,以blog中创建蓝图实例示例代码如下:
from flask import Blueprint # 导入Blueprint类
# 实例化Blueprint类,第一参数为蓝图名称,第二参数为包或模块名称,此处用特殊变量__name__代替
blog_bm = Blueprint('blog', __name__)
@blog_bm
-
在__init__.py文件中定义create_app()工厂函数进行程序实例注册,由于很多处理函数都要注册到程序上,为避免将工厂函数弄得太长,我们根据类别将代码分离成多个函数,这些函数都接收程序实例为参数
-
需要注册的处理函数有:
- 注册日志处理器
- 注册拓展实例化
- 注册蓝图(定义url前缀)
- 注册自定义shell命令(生成虚拟数据命令,初始化数据库命令等)
- 注册shell上下文处理函数
- 注册模板上下文处理函数
如何理解程序的上下文:插入一条轮子哥在知乎的回答
- 每一段程序都有很多外部变量。只有像Add这种简单的函数才是没有外部变量的。一旦你的一段程序有了外部变量,这段程序就不完整,不能独立运行。你为了使他们运行,就要给所有的外部变量一个一个写一些值进去。这些值的集合就叫上下文。
- 个人理解 :工厂函数将这些外部变量以函数形式装在一个"容器"里,能够让模板,shell命令在运行时附带上这些变量,使得程序正常运行
代码如下:
import os
import click
from flask import Flask, request
import logging
import pymysql
from Blog.bluemaps.admin import admin_bm
from Blog.bluemaps.blog import blog_bm
from Blog.bluemaps.login import login_bm
from Blog.extensions import db, ckeditor, moment, bootstrap, login, csrf
from Blog.settings import config
from Blog.models import Admin, Category, Post, Comment
from Blog.fakes import fake_admin, fake_post, fake_category, fake_comment
from flask_login import current_user
from logging.handlers import RotatingFileHandler
basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) # 根目录
pymysql.install_as_MySQLdb()
# 定义工厂函数
def create_app(config_name=None):
if config_name == None:
config_name = os.getenv('FLASK_CONFIG', 'development') # 设置配置名
app = Flask('Blog')
app.config.from_object(config[config_name]) # 从settings.py中的配置字典获取配置
# 注册工厂函数
register_template_context(app)
register_commands(app)
register_shell_context(app)
register_extensions(app)
register_bluemap(app)
register_logging(app)
return app
# 日志管理
def register_logging(app):
# 生成logs文件夹
folder = os.path.exists(os.path.join(basedir, 'logs'))
if not folder:
os.makedirs(os.path.join(basedir, 'logs'))
class RequestFormatter(logging.Formatter):
def format(self, record):
record.url = request.url
record.remote_addr = request.remote_addr
return super(RequestFormatter, self).format(record)
request_formatter = RequestFormatter(
'[%(asctime)s] %(remote_addr)s requested %(url)s\n'
'%(levelname)s in %(module)s: %(message)s'
)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler = RotatingFileHandler(os.path.join(basedir, 'logs/MyBlog.log'),
maxBytes=10 * 1024 * 1024, backupCount=10)
file_handler.setFormatter(formatter)
file_handler.setLevel(logging.INFO)
if not app.debug:
app.logger.addHandler(file_handler)
# 组织蓝本
def register_bluemap(app):
app.register_blueprint(blog_bm)
app.register_blueprint(login_bm, url_prefix='/login')
app.register_blueprint(admin_bm, url_prefix='/admin')
# 模板上下文
def register_template_context(app):
@app.context_processor
def make_template_context():
admin = Admin.query.first()
categories = Category.query.order_by(Category.name).all()
posts = Post.query.order_by(Post.timestamp).all()
if current_user.is_authenticated:
unread_comments = Comment.query.filter_by(reviewed=False).count()
else:
unread_comments = None
return dict(
admin=admin,
categories=categories,
posts=posts,
unread_comments=unread_comments
)
# 调用init_app()方法,传入程序实例,完成拓展初始化
def register_extensions(app):
bootstrap.init_app(app)
db.init_app(app)
moment.init_app(app)
ckeditor.init_app(app)
login.init_app(app)
csrf.init_app(app)
return app
# shell上下文
def register_shell_context(app):
@app.shell_context_processor
def make_shell_context():
return dict(db=db)
# shell命令
def register_commands(app):
@app.cli.command() # 添加命令行接口
@click.option('--drop', is_flag=True, help='重新创建数据库表') # 使用click提供的option装饰器添加自定义数量支持
def initdb(drop): # 初始化数据库,传入drop参数
"""初始化数据库"""
if drop:
click.confirm('该操作将删除原有数据库,确定删除?', abort=True)
db.drop_all()
click.echo('删除数据库')
db.create_all()
click.echo('已重置数据库')
@app.cli.command()
@click.option('--username', prompt=True, help='登录用户名')
@click.option('--password', prompt=True, hide_input=True, # hide_input隐藏输入内容
confirmation_prompt=True, help='登录密码') # confirmation_prompt设置二次确认输入
def init(username, password): # 博客初始化,传入用户名和密码
click.echo('配置数据库中...')
db.create_all()
admin = Admin.query.first() # 从数据库中查找管理员记录
if admin is not None: # 如果数据库中已经有管理员记录就更新用户名和密码
click.echo('管理员已存在,更新中...')
admin.username = username
admin.set_password(password) # 调用Admin模型类中的set_password()方法,生成password
else: # 没有管理员记录则创建新的管理员记录
click.echo('新建管理员账户...')
admin = Admin(
username=username,
blog_title='你的博客',
name='你的名字',
about='Anything about you.'
)
admin.set_password(password)
db.session.add(admin) # 将新创建对象添加到数据库会话
category = Category.query.first()
if category is None: # 如果没有分类则创建默认分类
click.echo('创建默认分类...')
category = Category(name='Default')
db.session.add(category)
db.session.commit() # 调用session.commit(),将改动提交到数据库
click.echo('完成.')
@app.cli.command()
@click.option('--category', default=5, help='分类数量,默认值为5个')
@click.option('--post', default=25, help='文章数量,默认值为25篇')
@click.option('--comment', default=150, help='评论数量,默认值为150个')
def forge(category, post, comment):
db.drop_all()
db.create_all()
click.echo('生成管理员...')
fake_admin()
click.echo('生成 %d 个分类...' % category)
fake_category(category)
click.echo('生成 %d 篇文章...' % post)
fake_post(post)
click.echo('生成 %d 个评论...' % comment)
fake_comment(comment)
click.echo('数据生成完毕!')
工厂函数对程序进行加工处理后程序就可以初步运行了,在开始运行之前再做以下工作
在蓝图文件夹下blog.py文件中添加以下代码
@blog_bm.route('/') # 设置路由
def index(): # 定义主页视图函数
return "跑起来啦!"
创建一个.flaskenv文件,在文件中写入两行代码
- FLASK_APP=Blog --指定程序实例名称
- FLASK_ENV=development --将运行环境设置为development(默认production)
在项目根目录下打开命令行窗口,输入以下代码运行程序
- pipenv shell —— 运行虚拟环境
- flask run ——运行flask程序
如下图:
网页运行结果如图(因为没有渲染模板所以只有之前视图函数返回的文字):
2.2.2 定义博客表单 forms.py
表单用于获取用户输入,博客定义的表单类有:
- 登录表单:用户名,密码,记住我等字段
- 评论表单:游客名,评论内容
- 设置表单:博客标题,关于,管理员昵称
- 新增/编辑文章表单:文章标题,内容,所属分类采用下拉列表,定义__init__方法
- 新增/编辑分类表单:分类名,定义验证器验证分类名不能重复
表单较为简单,代码如下:
from flask_wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField, TextAreaField, SelectField, ValidationError
from wtforms.validators import DataRequired, Length
from flask_ckeditor import CKEditorField
from Blog.models import Category
# 登录表单
class LoginForm(Form):
username = StringField('用户名', validators=[DataRequired()])
password = PasswordField('密码', validators=[DataRequired(), Length(1, 32)])
remember = BooleanField('记住我')
submit = SubmitField('登录')
# 评论表单
class CommentForm(Form):
name = StringField('昵称', validators=[DataRequired()])
comment = TextAreaField('评论内容', validators=[DataRequired(), Length(1, 256)])
submit = SubmitField('发布评论')
# 设置表单
class SettingForm(Form):
blog_title = StringField('博客名称', validators=[DataRequired()])
name = StringField('昵称', validators=[DataRequired(), Length(1, 20)])
about = TextAreaField('关于', validators=[DataRequired()])
submit = SubmitField('更新')
# 新增文章
class PostForm(Form):
title = StringField('标题', validators=[DataRequired()])
category = SelectField('分类', coerce=int, default=1)
body = CKEditorField('正文', validators=[DataRequired()])
submit = SubmitField('提交')
# 文章表单中分类下拉列表的选项必须是包含两个元素元组的列表,元组分别包含选项值和选项标签,
# 使用分类id作为选项值,分类名称作为选项标签,通过迭代Category.query.order_by(Category.name).all()返回分类记录实现
def __init__(self, *args, **kwargs):
super(PostForm, self).__init__(*args, **kwargs)
self.category.choices = [(category.id, category.name)
for category in Category.query.order_by(Category.name).all()]
# 新增分类
class CategoryForm(Form):
name = StringField('分类名', validators=[DataRequired()])
submit = SubmitField('提交')
# 表单自定义行内验证器,用于验证分类名重复
def validate_name(self, field):
if Category.query.filter_by(name=field.data).first():
raise ValidationError('分类名已存在')
2.2.3 辅助函数文件 helpers.py
- 辅助函数文件主要包括判断链接是否安全,防止url被篡改以及返回重定向回上一页面(如登录后返回上一页面),详情参考代码注释
from flask import redirect, request, url_for, current_app
try:
from urlparse import urlparse, urljoin
except ImportError:
from urllib.parse import urlparse, urljoin
# 判断安全链接,防止形成开放重定向漏洞
def is_safe_url(target):
# request.host_url获取程序内主机URL
ref_url = urlparse(request.host_url)
# urljoin()函数将目标URL转换为绝对URL
test_url = urlparse(urljoin(request.host_url, target))
# 最后对目标URL的URL模式和主机地址进行验证,确保只返回属于程序内部的URL
return test_url.scheme in ('http', 'https') and ref_url.netloc == test_url.netloc
# 重定向回上一页面
def redirect_back(default='blog.index', **kwargs):
# referrer用来记录发源地址的HTTP首部字段,即访问来源
# 在url中手动加入包含当前url的查询参数,一般命名next
for target in request.args.get('next'), request.referrer: # 先从next获取url,没有再从referrer获取
if not target: # 如果没有url
continue
if is_safe_url(target): # 判断url是否安全
return redirect(target)
return redirect(url_for(default, **kwargs)) # 未获取到任何上一页面url时,就返回到默认页面
# 给允许上传的文件加上“.”
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in current_app.config['BLOG_ALLOWED_IMAGE_EXTENSIONS']
小结:
工厂函数部分需要好好消化,涉及到很多不容易理解的概念,代码也较多且复杂
后面将开始编写视图函数和模板