背景
上一篇帖子我们使用 Flask 创建了最基本的 web 服务。使用 bootstrap 对页面进行装点,使用 JQuery Ajax 实现了在页面上实时显示 log 的功能。趁着周末,我继续开始学习更多的东西以满足这个 web 服务的需求。
模板继承
之前我们有了首页,有显示 log 的页面。之后我们还需要查看配置详情和创建新配置的页面。 那么我们会在页面中有很多重复的东西。例如我们所有的页面都有导航页,所有页面都要加载 JQuery。我们希望写页面的时候可以像写 python 一样可以使用对象的继承功能以减少重复的代码。所以我们用 Flask 的模板继承功能来写页面。现在我们创建一个 base.html。 代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 引入 Bootstrap -->
<link href="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<!-- HTML5 Shim 和 Respond.js 用于让 IE8 支持 HTML5元素和媒体查询 -->
<!-- 注意: 如果通过 file:// 引入 Respond.js 文件,则该文件无法起效果 -->
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<script type=text/javascript src="{{ url_for('static', filename='jquery.js') }}"></script>
<script type=text/javascript>
$SCRIPT_ROOT = {{ request.script_root|tojson|safe }};
</script>
</head>
<body>
<div class="container">
<div class="row clearfix">
<div class="col-md-12 column">
<ul class="nav nav-tabs">
<li>
<a href="{{ url_for('index') }}">当前环境</a>
</li>
<li>
<a href="{{ url_for('create_config') }}">创建环境</a>
</li>
<li class="disabled">
<a href="#">其他</a>
</li>
</ul>
</div>
</div>
{% block content %}{% endblock %}
</div>
</body>
</html>
我们看上面的代码,这是我们所有页面的父页面。 它很普通,跟其他页面貌似是一样的。但我们看到代码的底部,有这样一段的代码:
{% block content %}{% endblock %}
这段代码的意思是告诉继承它的子页面。子页面所有的内容将会添加到这里来。所以我们再看看子页面怎么写的。如下:
{% extends "base.html" %}
{% block content %}
<div class="row clearfix">
{% for config in configs %}
<div class="col-md-4 column">
<h2>
{{ config.name_prefix }}
</h2>
<p>
<a class="btn" href="{{ url_for('detail', name=config.name_prefix) }}">查看详情 >></a>
<a class="btn" href="javascript:if(confirm('确实重新部署环境么?此动作会覆盖现有环境'))location='/restart/{{ config.name_prefix }}'" >重新启动 >></a>
</p>
<p>
<a class="btn" href="{{ url_for('log', name=config.name_prefix) }}">日志</a>
</p>
</div>
{% endfor %}
</div>
{% endblock %}
在子页面中,我们开头使用 extends "base.html" 来继承父页面,在结束时使用 endblock。 结合父页面的定义,我们可以知道子页面的所有内容都显示在父页面的定义的 block 中。所以我们就可以看到如下的效果图:
这样子我们所有的页面都拥有了上面的导航栏。
url_for
接下来我们再聊一个简单的代码复用功能。url_for 方法, 这个方法的作用是根据函数名称找到正确的路由。 什么意思呢, 好记的我们的路由方法是如何定义的么?
@app.route('/')
@app.route('/index', methods=['GET', 'POST'])
def index():
这就是我们的路由方法,解释器决定了我们通过什么 url 来访问这个方法。但是我们在开发过程中每一个页面跳转或者重定向都使用这种固定的路径就会有问题,假如如果我修改了一下 url 的路径,那么所有其他引用这个路径的页面和方法都要做修改。 这样就很不爽, 所以我们可以使用 url_for(function_name) 的方式来获取正确的 url。 参数是路由方法的名称。 例如我们在其他方法里这样重定向首页。
return redirect(url_for('index'))
这段代码就是一个重定向的功能。我们处理完请求后,重定向为函数名称为 index 的路由上。不管定义路由的 URL 怎么变,只要路由的函数名称 (也就是 index 这个函数名) 没有变化就能获取到正确的 url。 下面看看在页面中如何引用:
<a class="btn" href="{{ url_for('detail', name=config.name_prefix) }}">查看详情 >></a>
在页面上的使用方式也是一样的。 另外如果想给 URL 传递参数,后面可以跟参数名=值得方式传递。
Flask-WTF
OK,我们对之前的代码做了一些小的优化。现在我们来继续完成 web 服务的功能。我们现在能展示所有配置名称,重启环境,查看 log。 我们离能凑合用 的状态还差了查看环境配置详情和创建环境配置的功能。 这两个功能实现起来很相似,首先我们需要一个表单来填写我们的配置项。 所幸我们有 Flask-WTF 这个扩展模块来帮我们做一些事情,省去了很多工作。 闲话不多说,我们通过 pip 安装这个模块后。需要配置一些东西。还记得我们一开始在 init.py 里初始化 Flask 的配置么,现在我们要多加一行。
# 如果想要使用Flask-WTF的表单,需要一个config文件
app.config.from_object('config')
然后我们再创建一个 config.py 文件。
CSRF_ENABLED = False
SECRET_KEY = 'you-will-never-guess'
WTF_CSRF_ENABLED = False
这些都是一些安全设置,如果不设置为 False 的话就需要在每一个表单中添加一个 hidden 的安全选项,我觉得麻烦。所以都禁用了。这样我们就对 Flask-WTF 做好了配置。 现在我们来创建一个表单吧。 首先我们创建一个 forms.py
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired
class ConfigForm(FlaskForm):
# base
name_prefix = StringField(u'环境名称', validators=[DataRequired()])
package_name = StringField(u'包名称', validators=[DataRequired()])
env_name = StringField(u'配置管理文件名称', validators=[DataRequired()])
# branch
prophet_app = StringField(u'prophet_app分支', validators=[DataRequired()])
online_app = StringField(u'online_app分支', validators=[DataRequired()])
prophet_ce = StringField(u'prophet_ce分支', validators=[DataRequired()])
lamma = StringField(u'lamma分支名称', validators=[DataRequired()])
# pma
pma_cpu = StringField(u'pma_CPU', validators=[DataRequired()])
pma_memory = StringField(u'pma内存限制', validators=[DataRequired()])
pma_disk = StringField(u'pma硬盘限制', validators=[DataRequired()])
# ip
prophet_ip = StringField(u'先知_ip', validators=[DataRequired()])
pma1_ip = StringField(u'pma1_ip', validators=[DataRequired()])
pma2_ip = StringField(u'pma2_ip', validators=[DataRequired()])
我们把表单抽象成了一个对象来表示。继承 Flask-WTF 的 FlaskForm 类。可以看到我们把表单元素都用做一个类的属性来定义。 编写这些表单元素的时候我们都有一些设置。例如 StringField 是定义这个属性为一个字符串输入字段,其实在页面上就是一个 input 标签。 里面我们用了两个参数,首先第一个参数为字符串,表示为表单元素的 label 属性,意思是我们自定义的名称。 第二个参数是一个验证器的列表,Flask-WTF 向我们提供了多种验证方式,它会自动帮我们验证页面的表单元素是否符合要求。 这里我使用的是一个 DataRequired,意思是表单不能为空。之后我们会在页面上看到,如果我们这个表单没有填写,Flask-WTF 会自动报错。 好了,现在我们定义好了一个表单。我们需要在路由方法中使用它并渲染到模板页面中。
@app.route('/config/detail/<name>')
def detail(name):
config = filter(lambda x: x.name_prefix == name, list_all_config())[0]
form = forms.ConfigForm()
form.name_prefix.data = config.name_prefix
form.package_name.data = config.package_name
form.env_name.data = config.env_name
form.prophet_app.data = config.branch_info['prophet-app']
form.prophet_ce.data = config.branch_info['prophet-ce']
form.online_app.data = config.branch_info['online-app']
form.lamma.data = config.branch_info['lamma']
form.prophet_ip.data = config.prophet_ip
form.pma1_ip.data = config.pma1_ip
form.pma2_ip.data = config.pma2_ip
form.pma_disk.data = config.pma_disk
form.pma_memory.data = config.pma_memory
form.pma_cpu.data = config.pma_cpu
return render_template('detail.html', form=form)
这是我们查看一个环境配置的路由方法,首先我们通过 fiter 方法把我们需要的环境配置筛选出来,然后我们挨个的给表单元素的 data 属性赋值。 这里介绍一下 FlaskForm。 这个类的每一个属性都是一个表单元素。每一个属性本身又有各种属性。 例如我们常用的 label(我们之前定义的表单名称),data(这个表单元素的值), name(属性本身的名称)。同样它还是一个可迭代的类,也就是说你可以使用 for 循环来遍历每一个表单元素。 这样就很方便我们操作了。好了,现在我们给每个表单元素都进行了赋值,然后渲染到了 detail.html 上。现在我们看看这个页面怎么定义吧
{% extends "base.html" %}
{% block content %}
{% if form.errors !={} %}
<div class="container">
<div class="row clearfix">
<div class="col-md-12 column">
<blockquote>
<p>
<font color="red">{{ form.errors }}</font>
</p>
</blockquote>
</div>
</div>
</div>
{% endif %}
<div class="container">
<div class="row clearfix">
<div class="col-md-12 column">
<form role="form" method="post" action="{{ url_for('save_config') }}">
<div class="form-group" hidden="true">
<label>{{ form.name_prefix.label }} {{ form.name_prefix(size=20) }} </label>
</div>
{% for item in form %}
{% if item.name != form.name_prefix.name %}
<div class="form-group">
<label>{{ item.label }} {{ item(size=20) }} </label>
</div>
{% endif %}
{% endfor %}
可以看到,我们通过 for 循环,在页面遍历了每一个表单元素 (由于我不希望用户更改环境名称,所以降这个字段设置为了 hidden)。 现在我们看看效果图。
可以看到我们不仅拥有了所有的表单元素,每个元素的默认值也都显示了出来(通过 label 属性展示)。不过我们留意到页面最上面有一个判断 form.errors 是否为空的代码块。这是给 Flask-WFT 做表单验证准备的,它会显示表单验证的错误信息。 举个例子,现在我们通过填写表单并提交求情的方式。来保存我们对环境配置的更改,我们写一个路由方法来处理这个请求:
@app.route('/config/save', methods=['POST'])
def save_config():
form = forms.ConfigForm()
if form.validate_on_submit():
update_config(form)
else:
return render_template('detail.html', form=form)
return redirect(url_for('index'))
我们首先引入眼帘的就是 form.validate_on_submit() 方法。 这是一个很好用的方法, 它已经帮我们从 request 中取出了 form,并判断它是否验证通过以及是否是提交状态。 一个 form 的流程就是,先判断是不是 submit,也就是说这个请求是不是通过提交表单提交的。然后判断 validate,好记的我们定义表单的时候使用的 validater 么? 它就是判断现在提交的表单符不符合当初定义的规则。 我之前在定义的时候要求所有表单都不能为空。 所以如果有表单为空,这个方法就会返回 False。 所以上面这个路由方法的逻辑就是先判断表单提交是否合法,如果合法就更新配置,如果不合法就重新渲染表单页面并输出错误信息。我们就可以通过在页面上使用 form.errors 来展示错误信息。 来看一下效果图。
由于 validate_on_submit 方法的特性既判断表单是不是提交又检验表单提交是否合法。所以其实我们的渲染表单和处理表单的请求是可以放在一个方法里写的。例如我为创建环境写的路由方法:
@app.route('/config/create', methods=['GET', 'POST'])
def create_config():
# config = filter(lambda x: x.name_prefix == 'template', list_all_config())[0]
form = forms.ConfigForm()
if form.validate_on_submit():
config_create(form)
return redirect(url_for('index'))
else:
return render_template('create.html', form=form)
解释上面的逻辑:如果是表单提交请求并且验证合法,创建环境并重定向到首页。 如果不是表单请求或者验证不合法,就会重新渲染页面。
总结
好了今天就先到这吧。 进度不快, Flask 的官方文档写的不是很细,踩了一些坑。现在这个 web 服务基本就是可用状态了,我们有环境的增删查改,部署环境,查看日志。虽然我预想的还需要很多功能。但是现在这个样子基本可以凑合使用。之后有时间再慢慢完善吧。
更多手把手教程请加入我的星球, 我最近正在更新手把手教你测试人工智能系列: