流程一览
前言
最近在看flask的课程,根据视频的要求做了一个flask的小项目:代码统计系统。简单的完善了一下。
不得不说虽然只是一个小项目,但是也是有完整的目录结构的,所以就不讲解代码了。
编译环境及工具,库:
- pycharm,anaconda,mysql,redis
- sqlalchemy,wtforms,redis,redis-py,flask,pipreqs,flask-migrate,flask-script,flask-sqlalchemy,flask-session
mysql驱动的话用这个mysql-connector-python,不用pymysql
# 先 pip install mysql-connector-python
class BaseConfig(object):
# ###flask-sqlalchemy的配置###
SQLALCHEMY_DATABASE_URI = "mysql+mysqlconnector://root:password@127.0.0.1:3306/你的数据库名?charset=utf8"
1.实现效果
先上目录结构:
1. 首页
2. 登录
3. 注册
4. 用户界面
柱状图有序显示用户的总代码提交量
5. 代码详情
折线图根据日期有序显示代码提交趋势
6. 上传页面
上传代码压缩包给服务器,得到里面的代码写的总行数
2.一些讲解
用的mysql做数据保存,用sqlalchemy库来做orm,不用原生sql,并且使用flask-sqlalchemy库更贴合flask来进行开发。使用wtfoems库来做登陆和注册表单,更简洁高效。使用pipreqs库来合成项目的requirements,方便的显示所使用的第三方库。使用flask-script和flask-migrate,因flask-migrate依赖于flask-script,其参数migrate生成数据库更改命令,upgrate进行数据库更改,能更简洁高效的对数据库进行更改。
flask因其特性,不可避免的会使用大量的第三方库,合理的使用资源才可提高效率。
以下是项目的打包,echarts的js包。(免费)
3.遇见的问题和解决(要点)
1. echarts的使用和下载 码云
因为echarts的js包是在github上的,下载很慢还会中断,所以我帮你们下载下来了。我用的是码云的办法,在网上可以搜到。使用的话有说明文档,解决基本功能还是可以的。
2. 文件解压问题 byteio
这是一个很重要的问题,因为之前的方法没用了。
# 保存解压后的文件(原压缩文件不保存)
# obj.stream是flask上传后取到的文件流,有open()功能
target_path = "保存路径"
shutil._unpack_zipfile(obj.stream, target_path)
如果你直接把stream放进去的话,会报错:
AttributeError: 'SpooledTemporaryFile' object has no attribute 'seekable'
可能flask返回的并非真正的文件对象,所以没有’seekable’参数,改为如下就可以了:
from io import BytesIO
temp = BytesIO(obj.stream.read())
target_path = "保存路径"
shutil._unpack_zipfile(temp, target_path)
3. wtforms自定义validate
为了检查用户名是否别人已经注册过了,而且wtforms没有对比的方法,就自己自定义,如下:
class RegisterForm(Form):
name = simple.StringField(
label='用户名:',
validators=[
validators.DataRequired(message='用户名不可为空!'),
validators.Length(min=2, max=12, message='用户名长度必须大于%(min)d且小于%(max)d'),
],
render_kw={'placeholder': '请输入用户名:', 'id': 'name'}
)
@staticmethod
def validate_name(self, filed): # validate是必须的前缀,后面的name是上面对应组件的对象名
name = db.session.query(models.Users.name).filter(models.Users.name == filed.data).first()
db.session.remove()
if name:
raise ValidationError("用户名:%s 已存在!" % name)
4. html的table的滑动条
html的table很难看,而且如果限制显示区域就有上下和左右的滑动条,添加如下属性则隐藏:
table{
width: 60%;
margin: 10px auto;
border-collapse: collapse;/*border-collapse:collapse合并内外边距(去除表格单元格默认的2个像素内外边距*/
background-color: rgba(0,0,0,0);
table-layout: fixed;
}
/*table可滚动start*/
tbody{
display: block;
height: 250px;
overflow-y: auto;
overflow-x: hidden !important;
-webkit-overflow-scrolling: touch; /*为了滚动顺畅*/
}
table tbody::-webkit-scrollbar {
display: none; /*隐藏滚动条*/
}
thead, tbody tr{
display: table;
width: 100%;
table-layout: fixed;
}
table thead{
width: 100%;
}
/*table可滚动end*/
虽然是隐藏了,但是当数据多的时候也没有下拉滚动条,只能滚动鼠标轮,所以最好的是当数据多的超出了table范围的时候自动显示下拉滚动,未超出时就隐藏。
5. flask的handler的错误处理
flak处理错误的时候不能在蓝图里面,必须在全局app上,如下:
def create_app():
app = Flask(__name__)
app.config.from_object('settings.ProConfig')
app.debug = True
@app.errorhandler(404)
def page_not_found(error):
return render_template("ac_404.html", error=error), 404
app.register_blueprint(account)
app.register_blueprint(administration)
# Session(app)
db.init_app(app)
return app
还有就是处理 413错误(上传文件过大) 的时候,没有返回,上传文件过大,无法处理,只能提醒,且会断开链接。
6. pipreqs好用
python的pipreqs库挺好用的,会自动生成所引用的第三方库,不过我觉得还是有一些缺陷所在,但是基本问题不大!
7. mysql的连表子查询
比如查询一个人在另一张表里的所有代码总数和。
subqry = db.session.query(db.func.sum(models.CodeMes.code_line)).filter(models.Users.id == models.CodeMes.user_id).correlate(
models.Users).as_scalar() # 生成mysql语句不查询
index_result = db.session.query(models.Users, subqry).order_by(subqry.desc()).all()
db.session.remove()
8. 闪现的使用或者g,jinjia模板不能在{{}}里调用字符串的[0]
原本我以为flask的闪现很鸡肋,后来用到了发现还是有一点用的,场景:比如我注册成功了,跳转到登陆界面,那么界面下应该“显示注册成功,请登录”吧,但是flask的跳转函数
return redirect("login")
是没有可以传参的。但是从注册成功到跳转登陆界面只是一次请求,所以flask的g对象可以完成这个,因为g就是只存在一次请求中就消失的。不过用闪现可能简单点,就先用它了。
flash("注册成功,请登录!", 'success')
success = ''
if get_flashed_messages(category_filter=['success']):
success = get_flashed_messages(category_filter=['success'])[0]
# 因为jinjia模板不能在html的{{}}里调用字符串的[0],而get_flashed_messages()函数返回的是列表,所以就在后端的处理里直接取[0]了。
9. html的textarea的运用
因为在注册或登陆时如果你的信息不符合要求,应该要有返回错误的,那么如何接受以及放在合适的位置就很重要了。而textarea就勉强符合要求:
<textarea id="error1" cols="30" rows="3" readonly disabled>{% for field in form %}{{field.errors[0]}}{% endfor %}{{user_pwd_error}}
</textarea>
#error1{
position: absolute;
top: 230px;
right: 30px;
resize: none;
color: red;
background-color: rgba(0,0,0,0);
border: none;
}
设为只读,控制长宽和位置
10. Flask的自定义扩展 (2020-02-17更新)
很多时候我们会遇见如下情况:Flask的全局处理路由放到创建app的函数里,这样既不美观,也不方便,有其他办法吗?放到函数外面然后在内部调用?
from flask import Flask
# from flask_session import Session
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy() # 必须在蓝图之前,不然蓝图不能调用
from exts.auth import Auth
from .views.account import account
from .views.administration import administration
from .models import *
def create_app():
app = Flask(__name__)
app.config.from_object('settings.ProConfig')
app.debug = True
Auth(app, [administration]) # 因为对于蓝图的处理,所以这个地方必须放到蓝图注册之前。因为蓝图在注册的时候会对所有的蓝图函数进行添加处理。
app.register_blueprint(account)
app.register_blueprint(administration)
@app.errorhandler(404)
def xxx():
print('xxx')
return 'xxx'
# Session(app)
db.init_app(app)
return app
这时就要涉及一个知识点了,就是python的装饰器是如何实现的!
# 方法一
@app.errorhandler(404)
def xxx():
print('xxx')
return 'xxx'
# 方法二
def page_not_found(self, error):
return render_template("ac_404.html", error=error), 404
app.errorhandler(404)(self.page_not_found)
基于如此我们就可以再开一个目录exts来存放全局处理的路由和函数了。
from flask import render_template, session, redirect
class Auth(object):
def __init__(self, app=None, bluep_list=None):
self.app = app
if bluep_list is None:
bluep_list = []
self.bluep_list = bluep_list
if app is not None:
self.init_app(app)
def init_app(self, app):
app.auth_manager = self
for bluep in self.bluep_list:
bluep.before_request(self.check_login)
app.errorhandler(404)(self.page_not_found)
# 这个函数可以处理一些值在全部的视图里直接使用,而不需要render_template()来导入值。
app.context_processor(self.context_processor)
def check_login(self):
if not session.get('user_info'):
return redirect("/login")
def context_processor(self):
user = session.get('user_info') # 返回的变量可视图中全局使用
return dict(current_user=user)
def page_not_found(self, error):
return render_template("ac_404.html", error=error), 404
def login_session(self, login_result):
session['user_info'] = {'id': login_result[0], 'nikename': login_result[1]}
def logout_session(self):
if 'user_info' in session:
session.pop('user_info')
如上的做法还处理了一个问题,当你的蓝图很多的时候,你还需要在很多的蓝图里使用before_request装饰器来验证用户是否登录,这是否也不符合程序的简洁性。所以我们使用外部扩展来处理,在Auth类里传入蓝图对象列表,然后for循环来添加before_request函数。
这里还有一个小细节:因为对于蓝图的处理,所以外部的扩展处理 Auth(app, [administration]) 必须放到蓝图注册之前。因为蓝图在注册的时候会对所有的蓝图函数进行添加处理。
为什么又要把登录,注销的处理也放到这里面嘞,因为可以对session里的值进行统一处理,不会分散多处。
最后因能力有限,暂时就先这样,后面如果js和mysql学的好的话,还可以更丰富,特别时js感觉在前端里是真的强大!
后期可优化:
- 限制每日提交一次
- 使用正则来约束用户名和密码的格式
- ajax传数据