1. 痛點
隨着業務的飛速發展,API接口越來越多,路由管理文件從幾十號變成幾百上千行,且每次上新服務,需要在修改路由文件代碼,帶來一定的風險。
2. 解決方案
- 既然路由文件隨着業務的擴展越來越龐大,那就去掉路由文件。
-
制定對應規則,路由通過API文件名根據一定的規則對應類名,然後自動導入對應實現類,註冊到Web框架中。
2.1 制定規則
下面這套規則只是其中一種方案,可以針對項目情況制定對應的規則,然後實現相關代碼,但是整體思路基本一樣。
-
代碼目錄結構,列一下簡單的項目文件目錄,下面以flask框架爲例:
app.py是啓動文件。
resources是API接口代碼文件夾。
services是爲API接口服務的函數封裝文件夾。
如果項目還有依賴文件,也可以單獨再建其他文件夾。 -
項目的API接口代碼均放在resources文件夾下,且此文件夾只能寫接口API服務代碼。
-
接口名稱命名以_連接單詞,而對應文件裏的類名文件名稱的單詞,不過換成是駝峯寫法。
- 類的導入則通過文件名對應到類名,實現自動映射註冊到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
hello_world_python.py
-
路由入口文件會自動映射,映射規則爲:
前綴 / 項目文件夾[...] / 文件名其中 前綴爲整個項目的路由前綴,可以定義,也可以不定義,比如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
- 關於resources裏目錄結構,代碼裏是可以允許N層,但建議不要超過3層, 不易管理。
-
2.2 代碼實現
python很多框架的啓動和路由管理都很類似,所以這套規則適合很多框架,測試過程中有包括flask, tornado, sanic, japronto。 以前年代久遠的web.py也是支持的。
完整代碼地址:
https://github.com/CrystalSkyZ/PyAutoApiRoute
-
實現下劃線命名 轉 駝峯命名 函數,代碼演示:
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
-
實現根據字符串導入模塊函數, 代碼演示:
-
通過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種方法都有測試。
-
-
檢索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'>)
]
元組第一個元素則是路由,第二個元素是對應的實現類。
總結:
至此,通過制定一定規則,解放路由管理文件方案完成。 歡迎各位一起討論其餘比較好的方案,更多方案討論可以關注微信公衆號: 天澄的技術筆記
。