当前位置:首页 > Python教程 > python技巧

Python Flask 实现移动端接口 —— 干货

  目前,Web 应用已形成一种趋势:业务逻辑被越来越多地移到客户端,逐渐完善为一种称为富互联网应用(RIA,rich Internet application)的架构。在 RIA 中,服务器的主要功能 (有时是唯一功能)是为客户端提供数据存取服务。在这种模式中,服务器变成了 Web 服务或应用编程接口(API,application programming interface)。 

  Flask 是开发 REST架构(RIA 采用的一种与 Web 服务通信的协议) Web 服务的理想框架,因为 Flask 天生轻量。本文将实际操作,实现一个简单的API。

 

一、项目简介

使用Flask实现一个接口(API),提供给移动端(iOS应用)调用,实现首页数据获取。同时展示了一种较为通用的项目架构及目录结构。

  • 本文客户端iOS代码不做详细说明。
  • Flask部署不做阐述,如需要,可参考之前的文章:Python Flask Web 框架入门
  • 接口功能只是最基本的实现,很多功能需要在真实项目中进行完善:包括身份验证、全量的错误处理、缓存与备份、负载与并发、复杂的数据库操作、数据库迁移、日志、版本迭代管理等等。
  • 服务端部署只是使用到Flask自带的Web服务器。
  • 客户端页面如下,首页接口返回数据包括:轮播图(两个条目)+下方三个分组(每个分组4个条目)

技术分享图片

 

 

二、环境准备

1、服务端

  • python包 :python(3.7)、pip、虚拟环境(virtualenv)、Flask、flask-sqlalchemy、pymysql
  • 其他:CentOS7 ECS服务器(本地测试也可以)、MySQL数据库、Git、

2、其他端

  • 本地开发:Mac、Pycharm、同上的python环境、Navicat(连接数据库)、Git、Postman(接口测试)
  • 客户端:xcode编写iOS客户端

3、虚拟环境和库

  • 如何创建虚拟环境不做介绍了
  • 在Pycharm中使用已经存在的虚拟环境(从Pycharm偏好设置进入)

技术分享图片

技术分享图片

  • 在Pycharm中添加库

技术分享图片

 

 

 

三、项目步骤及核心代码

 

 项目目录结构总览(请分清层次)

 技术分享图片

  • app文件夹为业务代码的存放处,包括视图+模型+静态文件,也叫做应用包。
  • static、templates、migrations、tests 本文中没有使用到,可跳过。
  • config.py 和 manage.py是启动应用和配置应用的关键。
  • requirements.txt 里面存放当前环境使用到的库,当我们将项目迁移到别的服务器(环境)时,可以通过这个文件,快速导入依赖的所有库。
pip3 freeze -l > requirements.txt  #导出
pip3 install -r requirements.txt   #导入

 

1、从 manage.py 开始

             1
            #
             启动程序
             2
            from app import create_app
 3 4""" 5development:    开发环境
 6production:     生产环境
 7testing:        测试环境
 8default:        默认环境
 910"""11# 通过传入当前的开发环境,创建应用实例,不同的开发环境配置有不同的config。这个参数也可以从环境变量中获取12 app = create_app(development)
1314if__name__ == __main__:
15# flask内部自带的web服务器,只可以在测试时使用16# 应用启动后,在9001端口监听所有地址的请求,同时根据配置文件中的DEBUG字段,设置flask是否开启debug17     app.run(host=0.0.0.0, port=9001, debug=app.config[DEBUG])

(1)每个flask项目,必须有一个应用实例。这里把实例的创建,推迟到了init中定义的create_app方法(工厂函数)。这样做,可以动态修改配置,给脚本配置应用“留出时间”,还能够创建多个应用,单元测试时也很有用。

(2)关于debug:在这个模式下,开发服务器默认会加载两个便利的工具:重载器和调试器。

  • 启用重载器后,Flask 会监视项目中的所有源码文件,发现变动时自动重启服务器。在开 发过程中运行启动重载器的服务器特别方便,因为每次修改并保存源码文件后,服务器都 会自动重启,让改动生效。
  • 调试器是一个基于 Web 的工具,当应用抛出未处理的异常时,它会出现在浏览器中。此时,Web 浏览器变成一个交互式栈跟踪。(本文中,没有用到调试器)

(3)from app import create_app ,会去app模块中,找去__init__.py ,将其中的对应内容引用进来。

 

2、app模块中 __init__.py  

            from flask_sqlalchemy import SQLAlchemy
from flask import Flask
from config import config

# 创建数据库
db = SQLAlchemy()

def create_app(config_name):

    # 初始化
    app = Flask(__name__)

    # 导致指定的配置对象:创建app时,传入环境的名称    app.config.from_object(config[config_name])

    # 初始化扩展(数据库)    db.init_app(app)

    # 创建数据库表    create_tables(app)
# 注册所有蓝本    regist_blueprints(app)

    return app

def regist_blueprints(app):

    # 导入蓝本对象# 方式一from app.api import api

    # 方式二:这样,就不用在app/api/__init__.py(创建蓝本时)里面的最下方单独引入各个视图模块了# from app.api.views import api# from app.api.errors import api# 注册api蓝本,url_prefix为所有路由默认加上的前缀
    app.register_blueprint(api, url_prefix=/api)

def create_tables(app):
    """
    根据模型,创建表格(可以有两种写法)
    1、模型必须在create_all方法之前导入,模型类声明后会注册到db.Model.metadata.tables属性中
    不导入模型模块,就不会执行模型中的代码,也就无法完成注册。
    2、但是,如果db是在模型模块中创建的,同时在此处 from app.models import db 引用db,则就实现了
    模型和数据库的绑定,不需要再单独导入模型模块了。
    """from app.models import Video
    db.create_all(app=app)

(1)创建应用实例,并且导入config.py文件,来配置app。

(2)创建数据库实例,然后一定要在create_app中初始化db.init_app(就是和app关联起来)。

(3)创建数据库表:先创建模型类(在models.py中),然后通过ORM(flask_sqlalchemy)映射为数据库中的表。如上面代码注释所说,一定注意导入模型的时机。

(4)注册蓝本,此处我们使用的蓝本名称是 api,蓝本实例的创建在api模块的__init_.py 中。

(5)关于蓝本的补充:

  • 将视图方法模块化,既当大量的视图函数放在一个文件中,很明显是不合适的,最好的方案是根据功能将路由合理的划分到不同的文件中。
  • 转换成应用工厂函数的操作(通过create_app创建应用实例)让定义路由变复杂了,现在应用在运行时创建,只有调用create_app() 之后才能使用 app.route 装饰器,这时定义路由就太晚了。使用蓝本,在蓝本中定义的路由处于休眠状态,直到蓝本注册到应用上之后,它们才真正成为应用的一部分。

 

3、api蓝本模块中的 __init__.py

            from flask import Blueprint

# 两个参数分别指定蓝本的名字、蓝本所在的包或模块
api = Blueprint(api, __name__)

"""
 导入路由模块、错误处理模块,将其和蓝本关联起来

 1、应用的路由保存在包里的 views.py 和 errors.py 模块中
 2、导入这两个模块就能把路由与蓝本关联起来
 3、注意,这些模块在 app/__init__.py 脚本的末尾导入,原因是:
    为了避免循环导入依赖,因为在 app/views.py 中还要导入api蓝本,所以除非循环引用出现在定义 api 之后,否则会致使导入出错。

"""from app.api import views, error

 

4、配置文件 config.py

             1
            #
             配置环境的基类
             2
            class
             Config(object):

             3
             4
            #
             每次请求结束后,自动提交数据库中的变动,该字段在flask-sqlalchemy 2.0之后已经被删除了(有bug)
             5     SQLALCHEMY_COMMIT_ON_TEARDOWN = True
 6 7# 2.0之后新加字段,flask-sqlalchemy 将会追踪对象的修改并且发送信号。 8# 这需要额外的内存,如果不必要的可以禁用它。 9# 注意,如果不手动赋值,可能在服务器控制台出现警告10     SQLALCHEMY_TRACK_MODIFICATIONS = False
1112# 数据库操作时是否显示原始SQL语句,一般都是打开的,因为后台要日志13     SQLALCHEMY_ECHO = True
141516# 开发环境的配置17class DevelopmentConfig(Config):
18"""19    配置文件中的所有的账号密码等敏感信息,应该避免出现在代码中,可以采用从环境变量中引用的方式,比如:
20    username = os.environ.get(‘MYSQL_USER_NAME‘)
21    password = os.environ.get(‘MYSQL_USER_PASSWORD‘)
2223    本文为了便于理解,将用户信息直接写入了代码里
2425"""26     DEBUG = True
27# 数据库URI28     SQLALCHEMY_DATABASE_URI = mysql+pymysql://root:123456@172.17.180.2/cleven_development2930# 也可如下来写,比较清晰31# SQLALCHEMY_DATABASE_URI = "mysql+pymysql://{username}:{password}@{hostname}/{databasename}".format(username="xxxx", password="123456", hostname="172.17.180.2", databasename="cleven_development")323334# 测试环境的配置35class TestingConfig(Config):
3637     TESTING = True
38     SQLALCHEMY_DATABASE_URI = mysql+pymysql://root:123456@172.17.180.3:3306/cleven_test394041"""42    测试环境也可以使用sqlite,默认指定为一个内存中的数据库,因为测试运行结束后无需保留任何数据
43    也可使用  ‘sqlite://‘ + os.path.join(basedir, ‘data.sqlite‘) ,指定完整默认数据库路径
44"""45# import os46# basedir = os.path.abspath(os.path.dirname(__file__))47# SQLALCHEMY_DATABASE_URI = os.environ.get(‘TEST_DATABASE_URL‘) or ‘sqlite://‘ 484950# 生产环境的配置51class ProductionConfig(Config):
5253     SQLALCHEMY_DATABASE_URI = mysql+pymysql://root:123456@172.17.180.4:3306/cleven_production545556# 初始化app实例时对应的开发环境声明57 config = {
58development: DevelopmentConfig,
59production: ProductionConfig,
60testing: TestingConfig,
61default: DevelopmentConfig
62 }

(1)给配置文件设置一个基类,让不同的配置环境,继承自他。

(2)关于 flask-sqlalchemy 的一些配置选项列表,不在这里展开了介绍了。

(3)配置文件中可以写入其他各种配置信息,比如以后使用到的 redis、MongoDB,甚至一些业务代码中使用到的配置相关的“常量”也可以定义在这里(注意代码的整洁)。

 

 

5、模型文件 models.py 

             1
            from app import db
 2from flask import abort
 3 4class Video(db.Model):
 5""" 6    视频 Model
 7""" 8__tablename__ = videos 9# 主键10     id = db.Column(db.Integer, primary_key=True)
11# 视频id12     vid = db.Column(db.String(50))
13# 封面图片14     coverUrl = db.Column(db.Text)
15# 详情描述16     desc = db.Column(db.Text)
17# 概要18     synopsis = db.Column(db.Text)
19# 标题20     title = db.Column(db.String(100))
21# 发布时间22     updateTime = db.Column(db.Integer)
23# 主题24     theme = db.Column(db.String(10))
25# 是否已删除?(逻辑)26     isDelete = db.Column(db.Boolean, default=False)
2728def to_json(self):
29"""30        完成Video数据模型到JSON格式化的序列化字典转换
31"""32         json_blog = {
33id: self.vid,
34coverUrl: self.coverUrl,
35desc: self.desc,
36synopsis: self.synopsis,
37title: self.title,
38updateTime: self.updateTime
39        }
40return json_video

(1)本文中使用的是“视频”模型,相应表的字段已经声明

(2)关于 flask-sqlalchemy 的模型属性类型 

技术分享图片

(3)常用 SQLAlchemy 列选项

技术分享图片

(4)补充:常用 SQLAlchemy 关系选项(本文并没有使用到,可以跳过)

  此处可参阅:flask-sqlalchemy用法详解

技术分享图片

 

6、业务的核心视图函数 views.py

             1
            from flask import make_response, jsonify
 2from app.api import api
 3from app.models import getHomepageData
 4 5 @api.route(/v1.0/homePage/, methods=[GET, POST])
 6def homepage():
 7""" 8     上面 /v1.0/homePage/ 定义的url最后带上"/":
 9     1、如果接收到的请求url没有带"/",则会自动补上,同时响应视图函数
10     2、如果/v1.0/homePage/这条路由的结尾没有带"/",则接收到的请求里也不能以"/"结尾,否则无法响应
11"""12     response = jsonify(code=200,
13                        msg="success",
14                        data=getHomepageData())
1516return response
17# 也可以使用 make_response 生成指定状态码的响应18# return make_response(response, 200)19

(1)这个视图,包含一个路由:获取ios应用首页的数据。

(2)getHomepageData 方法是在models.py中定义的一个函数,用来查询首页数据。

 

 

7、在models.py里添加查询函数

            from app import db
from flask import abort

class Video(db.Model):
    """
    视频 Model
    """__tablename__ = videos# 主键
    id = db.Column(db.Integer, primary_key=True)
    # 视频id
    vid = db.Column(db.String(50))
    # 封面图片
    coverUrl = db.Column(db.Text)
    # 详情描述
    desc = db.Column(db.Text)
    # 概要
    synopsis = db.Column(db.Text)
    # 标题
    title = db.Column(db.String(100))
    # 发布时间
    updateTime = db.Column(db.Integer)
    # 主题
    theme = db.Column(db.String(10))
    # 是否已删除?(逻辑)
    isDelete = db.Column(db.Boolean, default=False)

    def to_json(self):
        """
        完成Video数据模型到JSON格式化的序列化字典转换
        """
        json_blog = {
            id: self.vid,
            coverUrl: self.coverUrl,
            desc: self.desc,
            synopsis: self.synopsis,
            title: self.title,
            updateTime: self.updateTime
        }
        return json_blog


def getHomepageData():

    result = {}
    # 获取banner
    banners = Video.query.filter_by(theme=banner)
    result[banner] = [banner.to_json() for banner in banners]
    # 获取homepage
    first = Video.query.filter_by(theme=hot).all()
    second = Video.query.filter_by(theme=dramatic).all()
    third = Video.query.filter_by(theme=idol).all()
    if len(first) and len(second) and len(third):
        homepage = [{Hot Broadcast: [item.to_json() for item in first]},
                    {Dramatic Theater: [item.to_json() for item in second]},
                    {Idol Theatre: [item.to_json() for item in third]}]
        result[homepage] = homepage
        return result
    else:
        abort(404)

(1)上面使用到了flask_sqlalchemy的数据库查询方法,模型类.query即可查询模型对应的表。关于查询的其他常用操作符,只做简单介绍:

      技术分享图片

(2)abort(404)将请求阻断,并响应flask的errorhandler,在errors.py中实现了errorhandler装饰器装饰的响应函数。回顾一下,errors.py模块,也是在蓝本api中注册过的,所以可以响应abort抛出的错误。

(3)在下面运行和测试的时候会给出一个完整的json,可做参考。

 

 

8、错误处理模块 errors.py

            from flask import jsonify
from . import api

# 使用errorhandler装饰器,只有蓝本才能触发处理程序
# 要想触发全局的错误处理程序,要用app_errorhandler
@api.app_errorhandler(404)
def page_not_found(e):
    """这个handler可以catch住所有abort(404)以及找不到对应router的处理请求"""return jsonify({error: 没有找到您想要的资源, code: 404, data: ‘‘})


@api.app_errorhandler(500)
def internal_server_error(e):
    """这个handler可以catch住所有的abort(500)和raise exeception."""return jsonify({error: 服务器内部错误, code: 500, data: ‘‘})

 

          

 

四、运行与测试

现在服务端的代码都写完了,关于iOS端,代码很简单,就是一个tableView+SDCycleScrollView+AFN网路请求,不沾代码了。下面开始测试。

1、在本地,Git提交代码,服务端同步代码,并且在虚拟环境中安装好所有包:pip3 install -r requirements.txt。

2、启动应用:python3 manage.py ,如下,成功。

技术分享图片

 

3、启动成功之后,应该在数据库(cleven_development)中创建出了videos这张表,我们用Navicat连接数据库,并添加一些测试数据:

图片用的是公司项目的资源,打个码~,大家可以随便找点图片,放到自己的服务器上进行测试

技术分享图片

 

4、postman或者浏览器先测试一下 : http://服务器地址:9001/api/v1.0/homePage/,得到数据应该是

技术分享图片 技术分享图片
                  1
                {

                  2     code = 200;
  3     data =     {
  4         banner =         (
  5                        {
  6                 coverUrl = "http://xxxxx.comcomicApi/v1.0/fileserver/getfile/DM/publicimg/fuyao.jpg";
  7                 desc = "U8d85U7ea7U65e0U654cU597dU770bU7684U4e0dU884c";
  8                 id = D20171117092809862;
  9                 synopsis = "U8d2bU7620U7684U53e4U53bfU57ceU5373U5c06U6380U8d77U4e00U573aU8840U96e8U8165U98ce";
 10                 title = "U7261U4e39U4ed9U5b50U4e4bU7687U5e1dU8bcfU66f0";
 11                 updateTime = 1550122242716;
 12            },
 13                        {
 14                 coverUrl = "http://xxxxx.comcomicApi/v1.0/fileserver/getfile/DM/publicimg/muhouzhiwang.jpg";
 15                 desc = "U73b0U4ee3U793eU4f1aU771fU5b9eU5199U7167Uff0cU7cbeU5f69U65e0U4e0eU4f26U6bd4";
 16                 id = 20181130164518024;
 17                 synopsis = "U59d0U5f1fU604bU73b0U5b9eU7248";
 18                 title = "U7f8eU5bb9U9488";
 19                 updateTime = 1550122242716;
 20            }
 21        );
 22         homepage =         (
 23                        {
 24"Hot Broadcast" =                 (
 25                                        {
 26                         coverUrl = "http://xxxxx.comcomicApi/v1.0/fileserver/getfile/DM/publicimg/zhengyangmenxiaxiaonvren.jpg";
 27                         desc = "<null>";
 28                         id = 20181017153841718;
 29                         synopsis = "<null>";
 30                         title = "U6b63U9633U95e8U4e0bU5c0fU5973U4eba";
 31                         updateTime = 1553853355;
 32                    },
 33                                        {
 34                         coverUrl = "http://xxxxx.comcomicApi/v1.0/fileserver/getfile/DM/publicimg/simeiren.jpg";
 35                         desc = "<null>";
 36                         id = D20171117093709878;
 37                         synopsis = "<null>";
 38                         title = "U601dU7f8eU4eba";
 39                         updateTime = 1553853355;
 40                    },
 41                                        {
 42                         coverUrl = "http://xxxxx.comcomicApi/v1.0/fileserver/getfile/DM/publicimg/jiangye.jpg";
 43                         desc = "<null>";
 44                         id = 20181031171606549;
 45                         synopsis = "<null>";
 46                         title = "U5c06U591c";
 47                         updateTime = 1553853355;
 48                    },
 49                                        {
 50                         coverUrl = "http://xxxxx.comcomicApi/v1.0/fileserver/getfile/DM/publicimg/aishangnizhiyuwo.jpg";
 51                         desc = "<null>";
 52                         id = 20180628144552415;
 53                         synopsis = "<null>";
 54                         title = "U730eU6bd2U4eba";
 55                         updateTime = 1553853355;
 56                    }
 57                );
 58            },
 59                        {
 60"Dramatic Theater" =                 (
 61                                        {
 62                         coverUrl = "http://xxxxx.comcomicApi/v1.0/fileserver/getfile/DM/publicimg/nanfangyouqiaomu.jpg";
 63                         desc = "<null>";
 64                         id = D20171117092809831;
 65                         synopsis = "<null>";
 66                         title = "U5357U65b9U6709U4e54U6728";
 67                         updateTime = 1553853356;
 68                    },
 69                                        {
 70                         coverUrl = "http://xxxxx.comcomicApi/v1.0/fileserver/getfile/DM/publicimg/zuihaodeyujian.jpg";
 71                         desc = "<null>";
 72                         id = 20180329103639147;
 73                         synopsis = "<null>";
 74                         title = "U6700U597dU7684U9047U89c1";
 75                         updateTime = 1553853356;
 76                    },
 77                                        {
 78                         coverUrl = "http://xxxxx.comcomicApi/v1.0/fileserver/getfile/DM/publicimg/zhaoyao.jpg";
 79                         desc = "<null>";
 80                         id = 20190118091609760;
 81                         synopsis = "<null>";
 82                         title = "U62dbU6447";
 83                         updateTime = 1553853356;
 84                    },
 85                                        {
 86                         coverUrl = "http://xxxxx.comcomicApi/v1.0/fileserver/getfile/DM/publicimg/nihewodeqingchengshiguang.jpg";
 87                         desc = "<null>";
 88                         id = 20181107131541789;
 89                         synopsis = "<null>";
 90                         title = "U4f60U548cU6211U7684U503eU57ceU65f6U5149";
 91                         updateTime = 1553853356;
 92                    }
 93                );
 94            },
 95                        {
 96"Idol Theatre" =                 (
 97                                        {
 98                         coverUrl = "http://xxxxx.comcomicApi/v1.0/fileserver/getfile/DM/publicimg/langmanxingxing.jpg";
 99                         desc = "<null>";
100                         id = 20190123094947961;
101                         synopsis = "<null>";
102                         title = "U6d6aU6f2bU661fU661f";
103                         updateTime = 1553853357;
104                    },
105                                        {
106                         coverUrl = "http://xxxxx.comcomicApi/v1.0/fileserver/getfile/DM/publicimg/wodetiyulao.jpg";
107                         desc = "<null>";
108                         id = 20180124165920835;
109                         synopsis = "<null>";
110                         title = "U6211U7684U4f53U80b2U8001U5e08";
111                         updateTime = 1553853357;
112                    },
113                                        {
114                         coverUrl = "http://xxxxx.comcomicApi/v1.0/fileserver/getfile/DM/publicimg/aidesudi.jpg";
115                         desc = "<null>";
116                         id = 20180709103825926;
117                         synopsis = "<null>";
118                         title = "U7231U7684U901fU9012";
119                         updateTime = 1553853357;
120                    },
121                                        {
122                         coverUrl = "http://xxxxx.comcomicApi/v1.0/fileserver/getfile/DM/publicimg/aishangnizhiyuwo.jpg";
123                         desc = "<null>";
124                         id = 20180905132122384;
125                         synopsis = "<null>";
126                         title = "U7231U4e0aU4f60U6cbbU6108U6211";
127                         updateTime = 1553853357;
128                    }
129                );
130            }
131        );
132    };
133     msg = success;
134 }
json数据

里面有一些小问题需要处理,比如<null>这种情况(iOS这边对返回的空对象会解析成NSNull对象,打印出来就是<null>,理论上后端不应该把空对象返回给移动端),咱们就不单独处理了。

5、xcode打开app,应该可以拿到数据并展示了,good ~ 

 

 

五、总结

   算是完成了一个简单的移动端应用和Python服务端的通信。当然,里面还有很多问题需要优化,我们也没有加上服务器分发以及uWSGI等部署,同时数据库也就一张表,没有出现连表查询、关系存储等等,所以,只能算是一个双端通信的模型demo,用作大家交流探讨。

  后面如果有时间,把demo整理一下,打包上来。

原文:https://www.cnblogs.com/cleven/p/10979068.html


【说明】本文章由站长整理发布,文章内容不代表本站观点,如文中有侵权行为,请与本站客服联系(QQ:254677821)!

相关教程推荐

其他课程推荐