Python Flask 實現移動端應用接口(API)

引言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

  • 本文客戶端iOS代碼不作詳細說明。
  • Flask部署不作闡述,如須要,可參考以前的文章:Python Flask Web 框架入門
  • 接口功能只是最基本的實現,不少功能須要在真實項目中進行完善:包括身份驗證、全量的錯誤處理、緩存與備份、負載與併發、複雜的數據庫操做、數據庫遷移、日誌、版本迭代管理等等。
  • 服務端部署只是使用到Flask自帶的Web服務器。
  • 客戶端頁面以下,首頁接口返回數據包括:輪播圖(兩個條目)+下方三個分組(每一個分組4個條目)

    

 

 

2、環境準備redis


 

  一、服務端sql

  • python包 :python(3.7)、pip、虛擬環境(virtualenv)、Flask、flask-sqlalchemy、pymysql
  • 其餘:CentOS7 ECS服務器(本地測試也能夠)、MySQL數據庫、Git、

  二、其餘端數據庫

  • 本地開發:Mac、Pycharm、同上的python環境、Navicat(鏈接數據庫)、Git、Postman(接口測試)
  • 客戶端:xcode編寫iOS客戶端

  三、虛擬環境和庫編程

  • 如何建立虛擬環境不作介紹了
  • 在Pycharm中使用已經存在的虛擬環境(從Pycharm偏好設置進入)

     

     

  • 在Pycharm中添加庫

     

 

 

 

3、項目步驟及核心代碼json


   

 項目目錄結構總覽(請分清層次)

  • 使用tree命令查看

     

 

  • Pycharm中查看

      

  (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   #導入

 

 從 manage.py 開始   

 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:在這個模式下,開發服務器默認會加載兩個便利的工具:重載器調試器

  • 啓用重載器後,Flask 會監視項目中的全部源碼文件,發現變更時自動重啓服務器。在開 發過程當中運行啓動重載器的服務器特別方便,由於每次修改並保存源碼文件後,服務器都 會自動重啓,讓改動生效。
  • 調試器是一個基於 Web 的工具,當應用拋出未處理的異常時,它會出如今瀏覽器中。此時,Web 瀏覽器變成一個交互式棧跟蹤。(本文中,沒有用到調試器)

(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 }
json數據

  裏面有一些小問題須要處理,好比<null>這種狀況(iOS這邊對返回的空對象會解析成NSNull對象,打印出來就是<null>,理論上後端不該該把空對象返回給移動端),我們就不單獨處理了。

 五、xcode打開app,應該能夠拿到數據並展現了,good ~ 

 

 

5、總結 


 

    算是完成了一個簡單的移動端應用和Python服務端的通訊。固然,裏面還有不少問題須要優化,咱們也沒有加上服務器分發以及uWSGI等部署,同時數據庫也就一張表,沒有出現連表查詢、關係存儲等等,因此,只能算是一個雙端通訊的模型demo,用做你們交流探討。

  開發移動端API和其餘web應用相比,在設計思想和細節上仍是有不少不一樣的。服務端沒法全量掌控業務代碼,客戶端也是獨立開發,服務端必須考慮到客戶端設備性能、網絡狀態、平臺兼容、統一的數據結構、穩定的訪問、文檔的提供、友好的用戶體驗、規範的版本管理等等問題。雖然看上去,服務端只是給客戶端手機提供了想要的「資源」,可是,穩定性和規範化,比通常應用要求的還要高不少,換個角度說,爲移動端開發API,要求有較高的「容錯性」設計。

  後面若是有時間,把demo整理一下,打包上來。

相關文章
相關標籤/搜索