预计所需阅读时间:10分钟

之前翻译过一篇叫《用乐高解释模型-视图-控制器(MVC)框架》的文章,自己用Flask来做网页开发也有一段时间,这里总结一下个人对于新增或修改一个功能的理解,在MVC框架下,开发流程是怎样的,调试的切入点在哪里。

一、数据库模型

新增或修改一个功能,首先要对它的数据库和数据表进行新建与修改,建立与修改ORM类model.py,让它的每个结构化字段对与业务中的数据,或者需要进行数据分析的数据进行对应,每个字段都有意义,字段类型要符合实际需要,而不简单使用整数、浮点、字符串、日期时间这几种类型,这对于一个个人博客已经足够,但实际业务是不够的。

接着进行数据库初始或迁移,如果迁移过程中出现无法识别字段改变的问题,可以参考这篇文章《解决INFO [alembic.runtime.migration] Will assume non-transactional DDL问题》进行解决。

二、表单类

然后,建立与数据库字段对应的表单类form.py。当然不需要每个字段都要用表单来填写,有些不显示的用隐藏区域段,有些是有默认值,例如:默认是当前的时间。密码要验证,所以要额外多写一个区域段。提交submit这个区域段,通常我不会写在表单类,因为如果用AJAX提交的话,它的类型就是Button不是Submit,提交按钮用默认样式比较丑,或者位置不合适,所以我喜欢在前端对表单的整体和提交按钮进行设置。

如果加入了flask_ckeditor,会有一个CKEditorField可以使用。

三、新增的功能

在后端这里要开始写控制器功能。这里建议视图函数名字、路由解析的地址名、模板名都最好一致,不然项目越来越大,功能越来越多,在前后端引用这些名字的时候,就很容易混乱。

新增功能就是在数据库能实现在数据建立一行或多行,简单来说,就是视图函数将前端提交的表单转为一个ORM对象,再提交到数据库。前端模版就要根据表单类写出来,记得添加上CSRF的值。打开浏览器看看是否为所设想那样。

在浏览器提交内容的过程中,可以会出现一些错误,那就要找开浏览器的开发人员工具。如果服务器的响应码是200,但数据库没增加记录,很大可能前端出现错误。前端出错误可以先看Javascript有没有错误,可以通过console.log()来打印关键的变量值,如果没发现问题可以看看表单渲染出来的HTML语法是怎样的,有可能JQ没有选到对应的网页元素。如果HTML没问题,可以看看有没有form.data提交到服务器,没有CSRF,提交方法是怎样,数据类型是JSON还是其它。这样基本就能排除前端的错误。

另外,表单的验证有两方要注意,在前端方面,前期开发还没写提示,自己写的表单不符合要求,提交只是原地刷新。在后端方面,如果提交的表单数据不通过,不会进入if form.validate_on_submit():里面的代码,也不添加数据记录。

如果提交后,服务器的响应码是500之类,那肯定是控制器有问题。这里检查视图函数,看在处理前端提交过来的数据有没有变成空值。最有效的解决办法是用try/except语句来找到错误。最后也基本能把后端的问题解决。

四、删除的功能

数据库有了记录后,需要有一个列表的界面来显示这些对象。所以控制器的逻辑就是把某个表符合条件的内容读取出来。通过模版语法,如:{{ user.id }}, {{ user.name }}之类显示在前端的一个表格table里。

这时就是可以新增、删除、编辑的按钮先添加到网页上。接着写删除的视图函数。逻辑很简单就是根据id来找到这个要删除的对象,然后提交到数据库。但要注意CSRF保护,后端删除时要验证是否为管理员,是否有对象的删除权限。

五、修改的功能

修改功能的表单,通常Get和Post的请求方式都要用要用到,在写模版和视图函数中都要注意。在默认Get的请求下,是将原来的数据从数据库读取传到网页前端。当前端发生修改,然后用Post请求将修改后的数据写入数据库。

有些修改表单要用用户密码验证,这种情况,在前端和后端都要加上验证的逻辑。

另外,渲染模版时可以加上特定的参数,例如render_template('admin/article/article_edit.html', form=form, category=category....)后面这些变量在前端可以用模版语法来使用,这些变量可以称之为上下文对象。这些对象可以是一般的数字、字符串,也可以是可迭代的序列,如列表、元组、字典、集合,但不能是迭代器的生成器。模版语法能迭代这些序列,迭代器和生成器是惰性的,所以模版语法无法再回到后台,读取下一值。

六、查找的功能

添加查找功能,要有查找的表单,可以自己建一个表单类,或是直接在HTML写这个表单。要注意的是,表单用的是单选、多选、下拉菜单之类的网页元素,记得要在value要有不同的值。

然后,要根据表单传来不同的值,在后台设定不同的判断条件和不同ORM查询语句。

七、分页的功能

这里涉及两方面,一个是列表的内容要分页,二是搜索的结果要分页。

用ORM查询时用分页对象来伟出相关结果即可,以下是参考代码:

@member_module.route('/address_list', methods=['get', 'post'])
def address_list():
    page = request.args.get('page', 1)
    # 分页对象
    res = UserAddress.query.filter(UserAddress.user_id == current_user.user_id).order_by(
        UserAddress.id.desc()).paginate(int(page), current_app.config['XPMALL_MANAGE_GOODS_PER_PAGE'])
    addresses = res.items  # 分页对象里面的项目
    pageList = res.iter_pages()  # 分页地址的列表
    total = res.total  # 结果项总数
    pages = res.pages  # 分页总数
    return render_template('member/address/address_list.html', addresses=addresses, pageList=pageList, total=total, pages=pages)

初学者者在将搜索表单加上分页功能,在测试时会遇到按第2个分页之后,会成显示所有记录的第2页而不是搜索结果的第2页,关于搜索结果分页的功能,可以参考这篇文章来解决:《Flask中用同一个模板去渲染搜索分页与非搜索分页》

八、不同权限用户显示的内容

Flask的模板也有面向对象中继承的概念,通常会有一个base.html模板,这是网站给所有人查看要继承的模版;还有的是给注册用的模版,涉及用户中心相关的页面,一般是继承user_base.html模版;最后一类是给管理员的模版,涉及网站后台页面,一般是继承admin_base.html模版。

如果有不同级别的管理员,首先要在数据库上做区分,增加is_admin, is_manage, is_editor之类的字段。然后,如果是管理员在目录下的__init__.py文件写入以下检测代码:

# 需要flask_login插件

# 检测管理员账户
@admin_module.before_request  # 在管理员所有的视图函数前执行这个函数
@login_required  # 需要要用户登陆
def is_admin():
    if not current_user.is_admin:
        return redirect(url_for("login"))

会员目录下的__init__.py文件则简单一些:

@member_app.before_request
@login_required  # 装饰器已完成检测用户是否登陆功能,所以函数写什么也不重要
def is_login():
    # print(session['user'])

有不同级别的管理员,最好要分开权限使用Flask_login插件来为不同视图函数加上不同权限的装饰器。

如果后台的角色要操作的界面差异明显,例如是给运营、编辑人员,可以建立另外的目录写这个角色的视图函数与模版。

另外,如果不同角色要用的上下文对象,则可以写在角色的__init__.py文件里,例如:

@member_module.context_processor
def getCart():
    '''
    计算购物车信息
    :return: 购物车商品数量总数,总金额
    '''
    cart = Cart.query.filter_by(user_id=current_user.user_id).all()
    cart_amount, cart_total = 0, 0
    if cart:
        for item in cart:
            cart_amount = cart_amount + item.amount
            cart_total = cart_total + item.goods.price * item.amount
        print(cart_amount, cart_total)
        return {'cart_amount': cart_amount, 'cart_total': cart_total}
    else:
        return {'cart_amount': cart_amount, 'cart_total': cart_total}

如果是全局都可以用的,那注册在app.py里:

def register_template_context(app):
    @app.context_processor
    def getRecomendGood():
        '''
        获取推荐商品
        :return: 推荐商品上下文对象
        '''
        recommend_goods = Goods.query.filter_by(is_recommend=1).order_by(Goods.create_time.desc()).limit(10)
        return {'recommend_goods': recommend_goods}

做网页的全栈开发技术要求还是比较高的,要掌握html、CSS、Javascrip、其它前端框架、Flask框架、Jinja2模板语法、Python语法、数据库的知识,调试时要非常细心才行,通过浏览开发工具,IDE的控制台定位到问题是在前端还是后端,是静态html、CSS部分,还是动态Javascrip、模版语法部分,是Python语法、Flask用户问题,还是数据库、ORM用法的问题。