引言html
目前,Web 應用已造成一種趨勢:業務邏輯被愈來愈多地移到客戶端,逐漸完善爲一種稱爲富互聯網應用(RIA,rich Internet application)的架構。在 RIA 中,服務器的主要功能 (有時是惟一功能)是爲客戶端提供數據存取服務。在這種模式中,服務器變成了 Web 服務或應用編程接口(API,application programming interface)。 python
Flask 是開發 REST架構(RIA 採用的一種與 Web 服務通訊的協議) Web 服務的理想框架,由於 Flask 天生輕量。本文將實際操做,實現一個簡單的API。mysql
1、項目簡介ios
使用Flask實現一個接口(API),提供給移動端(iOS應用)調用,實現首頁數據獲取。同時展現了一種較爲通用的項目架構及目錄結構。web
2、環境準備redis
一、服務端sql
二、其餘端數據庫
三、虛擬環境和庫編程
3、項目步驟及核心代碼json
項目目錄結構總覽(請分清層次)
(1)app文件夾爲業務代碼的存放處,包括視圖+模型+靜態文件,也叫作應用包。
(2)static、templates、migrations、tests 本文中沒有使用到,可跳過。
(3)config.py 和 manage.py是啓動應用和配置應用的關鍵。
(4)requirements.txt 裏面存放當前環境使用到的庫,當咱們將項目遷移到別的服務器(環境)時,能夠經過這個文件,快速導入依賴的全部庫。
pip3 freeze -l > requirements.txt #導出 pip3 install -r requirements.txt #導入
1 # 啓動程序 2 from app import create_app 3 4 """ 5 development: 開發環境 6 production: 生產環境 7 testing: 測試環境 8 default: 默認環境 9 10 """ 11 # 經過傳入當前的開發環境,建立應用實例,不一樣的開發環境配置有不一樣的config。這個參數也能夠從環境變量中獲取 12 app = create_app('development') 13 14 if __name__ == '__main__': 15 # flask內部自帶的web服務器,只能夠在測試時使用 16 # 應用啓動後,在9001端口監聽全部地址的請求,同時根據配置文件中的DEBUG字段,設置flask是否開啓debug 17 app.run(host='0.0.0.0', port=9001, debug=app.config['DEBUG'])
(1)每一個flask項目,必須有一個應用實例。這裏把實例的建立,推遲到了init中定義的create_app方法(工廠函數)。這樣作,能夠動態修改配置,給腳本配置應用「留出時間」,還可以建立多個應用,單元測試時也頗有用。
(2)關於debug:在這個模式下,開發服務器默認會加載兩個便利的工具:重載器和調試器。
(3)from app import create_app ,會去app模塊中,找去__init__.py ,將其中的對應內容引用進來。
② 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): """ 根據模型,建立表格(能夠有兩種寫法) 一、模型必須在create_all方法以前導入,模型類聲明後會註冊到db.Model.metadata.tables屬性中 不導入模型模塊,就不會執行模型中的代碼,也就沒法完成註冊。 二、可是,若是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 裝飾器,這時定義路由就太晚了。使用藍本,在藍本中定義的路由處於休眠狀態,直到藍本註冊到應用上以後,它們才真正成爲應用的一部分。
③ api藍本模塊中的 __init__.py
from flask import Blueprint # 兩個參數分別指定藍本的名字、藍本所在的包或模塊 api = Blueprint('api', __name__) """ 導入路由模塊、錯誤處理模塊,將其和藍本關聯起來 一、應用的路由保存在包裏的 views.py 和 errors.py 模塊中 二、導入這兩個模塊就能把路由與藍本關聯起來 三、注意,這些模塊在 app/__init__.py 腳本的末尾導入,緣由是: 爲了不循環導入依賴,由於在 app/views.py 中還要導入api藍本,因此除非循環引用出如今定義 api 以後,不然會導致導入出錯。 """ from app.api import views, error
④ 配置文件 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 11 12 # 數據庫操做時是否顯示原始SQL語句,通常都是打開的,由於後臺要日誌 13 SQLALCHEMY_ECHO = True 14 15 16 # 開發環境的配置 17 class DevelopmentConfig(Config): 18 """ 19 配置文件中的全部的帳號密碼等敏感信息,應該避免出如今代碼中,能夠採用從環境變量中引用的方式,好比: 20 username = os.environ.get('MYSQL_USER_NAME') 21 password = os.environ.get('MYSQL_USER_PASSWORD') 22 23 本文爲了便於理解,將用戶信息直接寫入了代碼裏 24 25 """ 26 DEBUG = True 27 # 數據庫URI 28 SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:123456@172.17.180.2/cleven_development' 29 30 # 也可以下來寫,比較清晰 31 # SQLALCHEMY_DATABASE_URI = "mysql+pymysql://{username}:{password}@{hostname}/{databasename}".format(username="xxxx", password="123456", hostname="172.17.180.2", databasename="cleven_development") 32 33 34 # 測試環境的配置 35 class TestingConfig(Config): 36 37 TESTING = True 38 SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:123456@172.17.180.3:3306/cleven_test' 39 40 41 """ 42 測試環境也可使用sqlite,默認指定爲一個內存中的數據庫,由於測試運行結束後無需保留任何數據 43 也可以使用 'sqlite://' + os.path.join(basedir, 'data.sqlite') ,指定完整默認數據庫路徑 44 """ 45 # import os 46 # basedir = os.path.abspath(os.path.dirname(__file__)) 47 # SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or 'sqlite://' 48 49 50 # 生產環境的配置 51 class ProductionConfig(Config): 52 53 SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:123456@172.17.180.4:3306/cleven_production' 54 55 56 # 初始化app實例時對應的開發環境聲明 57 config = { 58 'development': DevelopmentConfig, 59 'production': ProductionConfig, 60 'testing': TestingConfig, 61 'default': DevelopmentConfig 62 }
(1)給配置文件設置一個基類,讓不一樣的配置環境,繼承自他。
(2)關於 flask-sqlalchemy 的一些配置選項列表,不在這裏展開了介紹了。
(3)配置文件中能夠寫入其餘各類配置信息,好比之後使用到的 redis、MongoDB,甚至一些業務代碼中使用到的配置相關的「常量」也能夠定義在這裏(注意代碼的整潔)。
⑤ 模型文件 models.py
1 from app import db 2 from flask import abort 3 4 class Video(db.Model): 5 """ 6 視頻 Model 7 """ 8 __tablename__ = 'videos' 9 # 主鍵 10 id = db.Column(db.Integer, primary_key=True) 11 # 視頻id 12 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) 27 28 def to_json(self): 29 """ 30 完成Video數據模型到JSON格式化的序列化字典轉換 31 """ 32 json_blog = { 33 'id': self.vid, 34 'coverUrl': self.coverUrl, 35 'desc': self.desc, 36 'synopsis': self.synopsis, 37 'title': self.title, 38 'updateTime': self.updateTime 39 } 40 return json_video
(1)本文中使用的是「視頻」模型,相應表的字段已經聲明
(2)關於 flask-sqlalchemy 的模型屬性類型
(3)經常使用 SQLAlchemy 列選項
(4)補充:經常使用 SQLAlchemy 關係選項(本文並無使用到,能夠跳過)
此處可參閱:flask-sqlalchemy用法詳解
⑥ 業務的核心視圖函數 views.py
1 from flask import make_response, jsonify 2 from app.api import api 3 from app.models import getHomepageData 4 5 @api.route('/v1.0/homePage/', methods=['GET', 'POST']) 6 def homepage(): 7 """ 8 上面 /v1.0/homePage/ 定義的url最後帶上"/": 9 一、若是接收到的請求url沒有帶"/",則會自動補上,同時響應視圖函數 10 二、若是/v1.0/homePage/這條路由的結尾沒有帶"/",則接收到的請求裏也不能以"/"結尾,不然沒法響應 11 """ 12 response = jsonify(code=200, 13 msg="success", 14 data=getHomepageData()) 15 16 return response 17 # 也可使用 make_response 生成指定狀態碼的響應 18 # return make_response(response, 200) 19
(1)這個視圖,包含一個路由:獲取ios應用首頁的數據。
(2)getHomepageData 方法是在models.py中定義的一個函數,用來查詢首頁數據。
⑦ 在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,可作參考。
⑧ 錯誤處理模塊 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': ''})
4、運行與測試
如今服務端的代碼都寫完了,關於iOS端,代碼很簡單,就是一個tableView+SDCycleScrollView+AFN網路請求,不沾代碼了。下面開始測試。
一、在本地,導出全部使用的庫:pip3 freeze -l > requirements.txt,而後Git提交代碼,服務端同步代碼,而且在虛擬環境中安裝好全部包:pip3 install -r requirements.txt。
二、啓動應用:python3 manage.py ,以下,成功。
三、啓動成功以後,應該在數據庫(cleven_development)中建立出了videos這張表,咱們用Navicat鏈接數據庫,並添加一些測試數據:
圖片用的是公司項目的資源,打個碼~,你們能夠隨便找點圖片,放到本身的服務器上進行測試
四、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 = "\U8d85\U7ea7\U65e0\U654c\U597d\U770b\U7684\U4e0d\U884c"; 8 id = D20171117092809862; 9 synopsis = "\U8d2b\U7620\U7684\U53e4\U53bf\U57ce\U5373\U5c06\U6380\U8d77\U4e00\U573a\U8840\U96e8\U8165\U98ce"; 10 title = "\U7261\U4e39\U4ed9\U5b50\U4e4b\U7687\U5e1d\U8bcf\U66f0"; 11 updateTime = 1550122242716; 12 }, 13 { 14 coverUrl = "http://xxxxx.comcomicApi/v1.0/fileserver/getfile/DM/publicimg/muhouzhiwang.jpg"; 15 desc = "\U73b0\U4ee3\U793e\U4f1a\U771f\U5b9e\U5199\U7167\Uff0c\U7cbe\U5f69\U65e0\U4e0e\U4f26\U6bd4"; 16 id = 20181130164518024; 17 synopsis = "\U59d0\U5f1f\U604b\U73b0\U5b9e\U7248"; 18 title = "\U7f8e\U5bb9\U9488"; 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 = "\U6b63\U9633\U95e8\U4e0b\U5c0f\U5973\U4eba"; 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 = "\U601d\U7f8e\U4eba"; 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 = "\U5c06\U591c"; 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 = "\U730e\U6bd2\U4eba"; 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 = "\U5357\U65b9\U6709\U4e54\U6728"; 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 = "\U6700\U597d\U7684\U9047\U89c1"; 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 = "\U62db\U6447"; 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 = "\U4f60\U548c\U6211\U7684\U503e\U57ce\U65f6\U5149"; 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 = "\U6d6a\U6f2b\U661f\U661f"; 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 = "\U6211\U7684\U4f53\U80b2\U8001\U5e08"; 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 = "\U7231\U7684\U901f\U9012"; 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 = "\U7231\U4e0a\U4f60\U6cbb\U6108\U6211"; 127 updateTime = 1553853357; 128 } 129 ); 130 } 131 ); 132 }; 133 msg = success; 134 }
裏面有一些小問題須要處理,好比<null>這種狀況(iOS這邊對返回的空對象會解析成NSNull對象,打印出來就是<null>,理論上後端不該該把空對象返回給移動端),我們就不單獨處理了。
五、xcode打開app,應該能夠拿到數據並展現了,good ~
5、總結
算是完成了一個簡單的移動端應用和Python服務端的通訊。固然,裏面還有不少問題須要優化,咱們也沒有加上服務器分發以及uWSGI等部署,同時數據庫也就一張表,沒有出現連表查詢、關係存儲等等,因此,只能算是一個雙端通訊的模型demo,用做你們交流探討。
開發移動端API和其餘web應用相比,在設計思想和細節上仍是有不少不一樣的。服務端沒法全量掌控業務代碼,客戶端也是獨立開發,服務端必須考慮到客戶端設備性能、網絡狀態、平臺兼容、統一的數據結構、穩定的訪問、文檔的提供、友好的用戶體驗、規範的版本管理等等問題。雖然看上去,服務端只是給客戶端手機提供了想要的「資源」,可是,穩定性和規範化,比通常應用要求的還要高不少,換個角度說,爲移動端開發API,要求有較高的「容錯性」設計。
後面若是有時間,把demo整理一下,打包上來。