Tornado + Bootstrap 快速搭建自己的web应用
前言
最近用 python tordado 框架, 整了一个模板页面, 用于接入与发布数据的展示,
tornado 简单易用, bootstrap 比较流行, 用起来也省事, 配合起来做些小案例非常迅速.
技术储备
-
python
基础知识, 面向对象封装,继承
-
数据库
mysql
-
框架
tornado, sqlalchemy (ORM), template
-
开发工具
pycharm, chrome
功能开发
一. 需求分析
-
每页展示 5 条统计数据, 按日期倒序排列
没有统计数据时, 要有文字提示: "暂没有对接统计信息"
-
支持按日期进行查询
查询时不显示分页情况
-
支持分页查询, 提示目前所在页码, 可跳转到任意一页
二. 页面原型
个人前端水平有限, 弄这样一个页面从无到有估计得花半天时间, 所以就找了 bootstrap 的模板, 里边有很多现成的组件可以直接使用, 然后改下布局, 调下样式, 做一个简单的页面足够.
bootstrap : https://v3.bootcss.com/components/
三. 搭建 tornado 框架
熟悉 python 的同学, 直接 easy_install 或者 pip 安装最新的 tornado, sqlalchemy, pymysql 等模块
bootstrap.js 直接到官网下载就行, 当然还需要 bootstrap 依赖的 JQuery.js
app.py
配置静态资源路径:
"static_path": os.path.join(os.path.dirname(__file__), "statics"),
配置模板路径:
"template_path": os.path.join(os.path.dirname(__file__), 'templates'),
配置运行模式:
# 通过脚本传参的方式指定
define("debug", default=1, help="debug mode: 1 to open, 2 to test env, other to production")
"debug": bool(options.debug),
开发, 调试模式, 开启单进程:
application.listen(options.port)
tornado.ioloop.IOLoop.instance().start()
生产模式, 开启多进程:
application.bind(options.port)
application.start(3) # 开启 3 个进程
tornado.ioloop.IOLoop.instance().start()
四. 连接 mysql
获取了DBSession 类, 在需要使用的地方, 创建对象[ session = DBSession() ]即可,
如果数据库操作不是特别平凡, 会话回收的时间周期可以设置的长一点, 例如: pool_recycle = 60
# coding: utf-8
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from settings.setting import MYSQL_SERVER, MYSQL_DRIVER, MYSQL_USERNAME, MYSQL_PASSWORD, DB_NAME, DB_CHARSET
# MYSQL_USERNAME = os.getenv('MYSQL_USER') or settings.MYSQL_USERNAME
# MYSQL_PASSWORD =
# 数据库
engine = create_engine("mysql+{driver}://{username}:{password}@{server}/{database}?charset={charset}"
.format(driver=MYSQL_DRIVER,
username=MYSQL_USERNAME,
password=MYSQL_PASSWORD,
server=MYSQL_SERVER,
database=DB_NAME,
charset=DB_CHARSET),
pool_size=20,
max_overflow=100,
pool_recycle=1,
echo=False)
engine.execute("SET NAMES {charset};".format(charset=DB_CHARSET))
MapBase = declarative_base(bind=engine)
DBSession = sessionmaker(bind=engine)
五. 处理类
基础类, 定义了一些 API 规范和常用的工具方法
先封装了基础类 GlobalBaseHandler , 数据库会话和获取参数的方法封装
# 全局基类方法
class GlobalBaseHandler(BaseHandler):
@property
def session(self):
if hasattr(self, "_session"):
return self._session
self._session = DBSession()
return self._session
# 关闭数据库会话
def on_finish(self):
if hasattr(self, "_session"):
self._session.close()
def prepare(self):
pass
查询, 分页业务处理类
需要注意的是: (小于第1页情况); (大于总页数的情况, 但总页数为 0 的情况).
class IllegalStats(GlobalBaseHandler):
def get(self):
"""
获取接入,发布统计数据
:return:
"""
date = self.get_argument("date")
current_page = self.get_argument("current_page") # 当前页
if date:
return self.query_by_date(date)
elif current_page:
return self.query_paginate(current_page)
# 默认返回第一页的数据
else:
return self.query_paginate()
def query_paginate(self, current_page=None):
if current_page is None:
current_page = 1
else:
current_page = int(current_page)
#
page_size = 5 # 分页条数
# 总记录数, 总页数
total_count = IllegalAccessStats.query_count(self.session)
if total_count % page_size == 0:
total_page = int(total_count / page_size)
else:
total_page = int(total_count / page_size) + 1
# 小于第一页
if current_page <= 0:
current_page = 1
# 大于最后一页
if total_page > 0:
if current_page > total_page:
current_page = total_page
else:
current_page = 1
# 当前页的数据
stats_list = IllegalAccessStats.query_paginate(self.session, current_page=current_page, page_size=page_size)
return self.render("statistics.html", stats_list=stats_list, date="", total_count=total_count, total_page=total_page, current_page=current_page)
def query_by_date(self, date):
stats_list = IllegalAccessStats.query_by_date(self.session, date=date)
#
return self.render("statistics.html", stats_list=stats_list, date=date, total_count=None, total_page=None, current_page=None)
六. 模板渲染
template, 就是在 html 里边写 python 代码, if 判断, for range 语句和python语法一模一样, 没有太大难度, 慢慢调试就出来了, 模板渲染报错提示非常详细, 很好调试,
感觉写起来贼恶心, 又是标签, 又是逻辑处理.
statistics.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- viewport视口:网页可以根据设置的宽度自动进行适配,在浏览器的内部虚拟一个容器,容器的宽度与设备的宽度相同。
width: 默认宽度与设备的宽度相同
initial-scale: 初始的缩放比,为1:1 -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>接入&发布统计信息</title>
<!-- Bootstrap -->
<link href="{{static_url('css/bootstrap.min.css')}}" rel="stylesheet">
<script src="{{static_url('js/jquery-2.1.0.min.js')}}"></script>
<script src="{{static_url('js/bootstrap.min.js')}}"></script>
<style type="text/css">
th, td {
text-align: center;
height: 50px;
vertical-align:middle;
}
</style>
</head>
<body>
<div class="container">
<h2 style="text-align: center">接入 & 发布统计信息</h2>
<br>
<br>
<div style="float: left;">
<form class="form-inline" action="/illegal/stats" method="get">
<div class="form-group">
<label for="exampleInputName1">日期</label>
<input type="text" name="date" value="{{ date }}" class="form-control" id="exampleInputName1" placeholder="yyyy-mm-dd" >
</div>
<button type="submit" class="btn btn-default" style="margin: 5px;">查询</button>
</form>
</div>
<br>
<br>
<br>
<br>
<br>
<table border="1" class="table table-bordered table-hover">
<div>
<a class="btn btn-primary" href="/illegal/stats?" style="float: left; margin: 3px;">接入</a>
<a class="btn btn-primary" href="/illegal/stats?" style="position: relative; left: 872px; margin: 3px">发布</a>
</div>
<tr class="access">
<th style="background: #f8efc0; vertical-align:middle">日期</th>
<th style="background: #4cae4c; vertical-align:middle">违法接收</th>
<th style="background: #4cae4c; vertical-align:middle">入库成功</th>
<th style="vertical-align: middle">读取成功</th>
<th style="background: #d9534f; vertical-align:middle">读取失败</th>
<th style="vertical-align: middle">下载成功</th>
<th style="background: #d9534f; vertical-align:middle">下载失败</th>
<th style="vertical-align: middle">写入成功</th>
<th style="background: #d9534f; vertical-align:middle">写入失败</th>
<th style="background: #4cae4c; vertical-align:middle">审核接收</th>
<th style="background: #4cae4c; vertical-align:middle">发布成功</th>
</tr>
{% if len(stats_list) > 0 %}
{% for stats in stats_list %}
<tr>
<td style="vertical-align: middle">{{ stats["date"] }}</td>
<td style="vertical-align: middle">{{ stats["access_received_total"] }}</td>
<td style="vertical-align: middle">{{ stats["access_inserted_total"] }}</td>
<td style="vertical-align: middle">{{ stats["read_success_total"] }}</td>
<td style="vertical-align: middle">{{ stats["read_false_total"] }}</td>
<td style="vertical-align: middle">{{ stats["download_success_total"] }}</td>
<td style="vertical-align: middle">{{ stats["download_false_total"] }}</td>
<td style="vertical-align: middle">{{ stats["write_success_total"] }}</td>
<td style="vertical-align: middle">{{ stats["write_false_total"] }}</td>
<td style="vertical-align: middle">{{ stats["publish_received_total"] }}</td>
<td style="vertical-align: middle">{{ stats["publish_send_total"] }}</td>
</tr>
{% end %}
{% else %}
<tr>
<td colspan="11" style="vertical-align: middle">暂没有对接统计信息</td>
</tr>
{% end %}
</table>
<!-- 查询的时候不显示分页内容-->
{% if total_count is not None or total_page is not None %}
<div>
<nav aria-label="Page navigation">
<ul class="pagination">
{% if current_page == 1 %}
<li class="disabled">
{% end %}
{% if current_page != 1 %}
<li>
{% end %}
<a href="/illegal/stats?current_page={{current_page - 1}}" aria-label="Previous">
<span aria-hidden="false">«</span>
</a>
</li>
{% for page in range(1, total_page + 1) %}
{% if current_page == page %}
<li class="active"><a href="/illegal/stats?current_page={{ page }}">{{ page }}</a></li>
{% end %}
{% if current_page != page %}
<li><a href="/illegal/stats?current_page={{ page }}">{{ page }}</a></li>
{% end %}
{% end %}
{% if current_page == total_page or current_page == 1 %}
<li class="disabled">
{% end %}
{% if current_page != total_page and current_page != 1 %}
<li>
{% end %}
<a href="/illegal/stats?current_page={{current_page + 1}}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
<span style="font-size: 25px;margin-left: 5px;">
共{{ total_count }}条记录,共{{ total_page }}页
</span>
</ul>
</nav>
</div>
{% end %}
</div>
</body>
</html>
需要注意的地方
-
模板渲染的时候, 需要使用
static_url()
函数获取静态资源( statics ) 的路径, 不然模板会找不到 css, js 文件, 不能正常加载页面.<link href="{{static_url('css/bootstrap.min.css')}}" rel="stylesheet">
<script src="{{static_url('js/jquery-2.1.0.min.js')}}"></script>
<script src="{{static_url('js/bootstrap.min.js')}}"></script>
-
给当前页添加激活状态
项目改进
xxx
最后给出web项目地址: https://github.com/kaichenkai/TornadoWebApp