美文网首页Python小哥哥
Python Web开发: 教你如何解放路由管理

Python Web开发: 教你如何解放路由管理

作者: 我爱学python | 来源:发表于2019-03-15 14:59 被阅读0次

    1. 痛点

    随着业务的飞速发展,API接口越来越多,路由管理文件从几十号变成几百上千行,且每次上新服务,需要在修改路由文件代码,带来一定的风险。

    image

    2. 解决方案

    • 既然路由文件随着业务的扩展越来越庞大,那就去掉路由文件。
    • 制定对应规则,路由通过API文件名根据一定的规则对应类名,然后自动导入对应实现类,注册到Web框架中。

    2.1 制定规则

    下面这套规则只是其中一种方案,可以针对项目情况制定对应的规则,然后实现相关代码,但是整体思路基本一样。

    1. 代码目录结构,列一下简单的项目文件目录,下面以flask框架为例:
    image

    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

    image

    hello_world_python.py

    image
    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开发: 教你如何解放路由管理

        本文链接:https://www.haomeiwen.com/subject/ikalmqtx.html