Python Web開發: 教你如何解放路由管理

天澄發表於2019-02-18

1. 痛點

隨著業務的飛速發展,API介面越來越多,路由管理檔案從幾十號變成幾百上千行,且每次上新服務,需要在修改路由檔案程式碼,帶來一定的風險。

Python Web開發:  教你如何解放路由管理

2. 解決方案

  • 既然路由檔案隨著業務的擴充套件越來越龐大,那就去掉路由檔案。
  • 制定對應規則,路由通過API檔名根據一定的規則對應類名,然後自動匯入對應實現類,註冊到Web框架中。

2.1 制定規則

下面這套規則只是其中一種方案,可以針對專案情況制定對應的規則,然後實現相關程式碼,但是整體思路基本一樣。

  1. 程式碼目錄結構,列一下簡單的專案檔案目錄,下面以flask框架為例:

Python Web開發:  教你如何解放路由管理
app.py是啟動檔案。 resources是API介面程式碼資料夾。 services是為API介面服務的函式封裝資料夾。 如果專案還有依賴檔案,也可以單獨再建其他資料夾。

  1. 專案的API介面程式碼均放在resources資料夾下,且此資料夾只能寫介面API服務程式碼。

  2. 介面名稱命名以_連線單詞,而對應檔案裡的類名檔名稱的單詞,不過換成是駝峰寫法。

  3. 類的匯入則通過檔名對應到類名,實現自動對映註冊到web框架中。

規則舉例如下: 如上圖,resources下有一個hello_world介面,還有一個ab專案資料夾,ab下面還有一個hello_world_python介面以及子專案資料夾testab,testab下面也有一個hello_world_python.

  • 介面檔案的檔名命名規範: 檔名命名均為小寫,多個單詞之間使用'_'隔開,比如hello_world.py 命名正確,helloWorld.py命名錯誤。

  • 介面檔案裡的介面類Class命名是以檔名字轉為駝峰格式,且首字母大寫。比如hello_world.py 對應的介面類是 HelloWorld 舉例: hello_world.py

    Python Web開發:  教你如何解放路由管理
    hello_world_python.py

    Python Web開發:  教你如何解放路由管理

  1. 路由入口檔案會自動對映,對映規則為: 字首 / 專案資料夾[...] / 檔名

    其中 字首為整個專案的路由字首,可以定義,也可以不定義,比如api-ab專案,可以定義整個專案的路由字首為 ab/ resource下面專案資料夾如果有,則會自動拼接,如果沒有,則不會讀取。 舉例: 字首為空,上圖resources中的三個介面對應的路由為:

    hello_world.py ==>  /hello_world
    ab/hello_world_python.py ==> /ab/hello_world_python
    ab/testab/hello_world_python.py ==> /ab/testab/hello_world_python
    複製程式碼

    字首為ab/,上圖resources中的三個介面對應的路由為:

    hello_world.py ==> ab/hello_world
    ab/hello_world_python.py ==> ab/ab/hello_world_python
    ab/testab/hello_world_python.py ==> ab/ab/testab/hello_world_python
    複製程式碼
  2. 關於resources裡目錄結構,程式碼裡是可以允許N層,但建議不要超過3層, 不易管理。

2.2 程式碼實現

python很多框架的啟動和路由管理都很類似,所以這套規則適合很多框架,測試過程中有包括flask, tornado, sanic, japronto。 以前年代久遠的web.py也是支援的。

完整程式碼地址: github.com/CrystalSkyZ…

  1. 實現下劃線命名 轉 駝峰命名 函式,程式碼演示:

    def underline_to_hump(underline_str):
    '''
    下劃線形式字串轉成駝峰形式,首字母大寫
    '''
    sub = re.sub(r'(_\w)', lambda x: x.group(1)[1].upper(), underline_str)
    if len(sub) > 1:
        return sub[0].upper() + sub[1:]
    return sub
    複製程式碼
  2. 實現根據字串匯入模組函式, 程式碼演示:

    • 通過python內建函式__import__函式實現載入類
    def import_object(name):
    """Imports an object by name.
    import_object('x') is equivalent to 'import x'.
    import_object('x.y.z') is equivalent to 'from x.y import z'.
    """
    if not isinstance(name, str):
        name = name.encode('utf-8')
    if name.count('.') == 0:
        return __import__(name, None, None)
    
    parts = name.split('.')
    obj = __import__('.'.join(parts[:-1]), None, None, [parts[-1]], 0)
    try:
        return getattr(obj, parts[-1])
    except AttributeError:
        raise ImportError("No module named %s" % parts[-1])
    複製程式碼
    • 通過importlib模組實現
    importlib.import_module(name)
    複製程式碼

    上面2種方法都可以,github上程式碼裡2種方法都有測試。

  3. 檢索resources資料夾,生成路由對映,並匯入對應實現類, 程式碼演示如下:

    def route(route_file_path,
          resources_name="resources",
          route_prefix="",
          existing_route=None):
          
    route_list = []
    
        def get_route_tuple(file_name, route_pre, resource_module_name):
            """
            :param file_name: API file name
            :param route_pre: route prefix
            :param resource_module_name: resource module
            """
            nonlocal route_list
            nonlocal existing_route
            route_endpoint = file_name.split(".py")[0]
            #module = importlib.import_module('{}.{}'.format(
            #    resource_module_name, route_endpoint))
            module = import_object('{}.{}'.format(
                resource_module_name, route_endpoint))
            route_class = underline_to_hump(route_endpoint)
            real_route_endpoint = r'/{}{}'.format(route_pre, route_endpoint)
            if existing_route and isinstance(existing_route, dict):
                if real_route_endpoint in existing_route:
                    real_route_endpoint = existing_route[real_route_endpoint]
            route_list.append((real_route_endpoint, getattr(module, route_class)))
    
        def check_file_right(file_name):
            if file_name.startswith("_"):
                return False
            if not file_name.endswith(".py"):
                return False
            if file_name.startswith("."):
                return False
            return True
    
        def recursive_find_route(route_path, sub_resource, route_pre=""):
            nonlocal route_prefix
            nonlocal resources_name
            file_list = os.listdir(route_path)
            if config.DEBUG:
                print("FileList:", file_list)
            for file_item in file_list:
                if file_item.startswith("_"):
                    continue
                if file_item.startswith("."):
                    continue
                if os.path.isdir(route_path + "/{}".format(file_item)):
                    recursive_find_route(route_path + "/{}".format(file_item), sub_resource + ".{}".format(file_item), "{}{}/".format(route_pre, file_item))
                    continue
                if not check_file_right(file_item):
                    continue
                get_route_tuple(file_item, route_prefix + route_pre, sub_resource)
    
    recursive_find_route(route_file_path, resources_name)
    if config.DEBUG:
        print("RouteList:", route_list)
    
    return route_list
    複製程式碼
    • get_route_tuple函式作用是通過字串匯入類,並將路由和類以元組的方式新增到陣列中。
    • check_file_right函式作用是過濾資料夾中不合法的檔案。
    • recursive_find_route函式採用遞迴查詢resources中的檔案。
    • existing_route引數是將已經線上存在的路由替換新規則生成的路由,這樣舊專案也是可以優化使用這套規則。

3. 應用到專案中

以flask框架為例,其餘框架請看github中的程式碼演示。 app.py 中程式碼

app = Flask(__name__)
api = Api(app)
# APi route and processing functions
exist_route = {"/flask/hello_world": "/hello_world"}
route_path = "./resources"
route_list = route(
    route_path,
    resources_name="resources",
    route_prefix="flask/",
    existing_route=exist_route)

for item in route_list:
    api.add_resource(item[1], item[0])

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=int(parse_args.port), debug=config.DEBUG)
複製程式碼

執行app.py之後,路由列印如下:

RouteList: [
('/hello_world', <class'resources.hello_world.HelloWorld'>),\   ('/flask/ab/testab/hello_world_python_test', <class 'resources.ab.testab.hello_world_python_test.HelloWorldPythonTest'>), \
('/flask/ab/hello_world_python', <class 'resources.ab.hello_world_python.HelloWorldPython'>)
]
複製程式碼

元組第一個元素則是路由,第二個元素是對應的實現類。


總結: 至此,通過制定一定規則,解放路由管理檔案方案完成。

更多文章請關注公眾號 『天澄技術雜談』

Python Web開發:  教你如何解放路由管理

相關文章