• Tornado + Bootstrap 快速搭建自己的web应用


    前言

    最近用 python tordado 框架, 整了一个模板页面, 用于接入与发布数据的展示,

    tornado 简单易用, bootstrap 比较流行, 用起来也省事, 配合起来做些小案例非常迅速.


    技术储备

    1. python

      基础知识, 面向对象封装,继承

    2. 数据库

      mysql

    3. 框架

      tornado, sqlalchemy (ORM), template

    4. 开发工具

      pycharm, chrome


    功能开发

    一. 需求分析

    1. 每页展示 5 条统计数据, 按日期倒序排列

      没有统计数据时, 要有文字提示: "暂没有对接统计信息"

    2. 支持按日期进行查询

      查询时不显示分页情况

    3. 支持分页查询, 提示目前所在页码, 可跳转到任意一页


    二. 页面原型

    个人前端水平有限, 弄这样一个页面从无到有估计得花半天时间, 所以就找了 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视口:网页可以根据设置的宽度自动进行适配,在浏览器的内部虚拟一个容器,容器的宽度与设备的宽度相同。
         默认宽度与设备的宽度相同
        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">&laquo;</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">&raquo;</span>
                            </a>
                        </li>
                        <span style="font-size: 25px;margin-left: 5px;">
                                共{{ total_count }}条记录,共{{ total_page }}页
                        </span>
                    </ul>
                </nav>
            </div>
        {% end %}
    </div>
    </body>
    </html>
    

    需要注意的地方

    1. 模板渲染的时候, 需要使用 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>


    1. 给当前页添加激活状态


    项目改进

    xxx


    最后给出web项目地址: https://github.com/kaichenkai/TornadoWebApp


    ending ~

    每天都要遇到更好的自己.
  • 相关阅读:
    使用scp进行远程数据传输时避免输入密码(scp without password)
    scons用户指南翻译(附gcc/g++参数详解)
    StringIO 模块用于在内存缓冲区中读写数据
    注意变换的顺序
    为什么static成员必须在类外初始化
    Visual Studio中删除所有空行
    rendering order of skybox
    解决VS中注释乱码的问题
    一道数论题目
    First Chance Exception
  • 原文地址:https://www.cnblogs.com/kaichenkai/p/11999880.html
Copyright © 2020-2023  润新知