# 2.6 多模块拆分的误区

## 1.将试图函数都放在一个文件中有哪些不足：

1.代码太长，不利于维护 2.从业务模型抽象的角度，不应该把他们都放在一个文件中。关于书籍相关的API就应该放在书籍模型的视图函数文件中，跟用户相关的API就应该放在用户模型相关的文件中 3.入口文件的意义比较独特，会启动web服务器以及做很多初始化的操作，就算要放在一个文件也不应该业务的操作放在入口文件中来

## 2.尝试拆分模块

思路，将试图函数抽离到单独的包中，然后在新的试图文件中引入flask.py来导入app核心对象。为了新的试图文件中的路由可以成功注册，再在flask.py中引入刚刚抽离出的试图模块

修改后的fisher.py

```python
from flask import Flask

# 为了可以注册book.py中的路由
from app.web import book

app = Flask(__name__)

app.config.from_object("config")

if __name__ == "__main__":
    app.run(host=app.config["HOST"], debug=app.config["DEBUG"], port=app.config["PORT"])
```

新增的book.py

```python
from flask import jsonify

from helper import is_isbn_or_key
from yushu_book import YuShuBook

# 为了让book.py模块可以使用app对象
from fisher import app

@app.route("/book/search/<q>/<page>")
def search(q, page):
    """
    搜索书籍路由
    :param q: 关键字 OR isbn
    :param page: 页码
    """
    isbn_or_key = is_isbn_or_key(q)
    if isbn_or_key == 'isbn':
        result = YuShuBook.search_by_isbn(q)
    else:
        result = YuShuBook.search_by_key(q)

    return jsonify(result)
```

但是这样做并不是正确的做法，结果表明，这样修改以后，访问search api会404

为了知道为什么这样做不行，我们需要先刨铣一下Flask路由机制的原理

## 3.Flask路由机制

![flask路由机制](https://upload-images.jianshu.io/upload_images/7220971-2a3df701a7d9f426.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240) flask的基本思想是内部会维护一个字典。每一个url都会对应一个视图函数，但是不仅仅是这样。每一个url还会对应一个endpoint端点。用于反向构建URL（后面会讲解)

flask的路由注册`app_url_rule(url=,view_func=,endpoint=)`会接受三个参数，前两个我们都知道了，第三个就是上面说的endpoint。他的默认值是view\_func的名称。当然，`app.route('url',endpoint=)`也可以传入

flask route的部分源码

```python
    # 注册路由的装饰器
    def route(self, rule, **options):
        def decorator(f):
            endpoint = options.pop('endpoint', None)
            # 装饰器内部也是调用了add_url_rule
            self.add_url_rule(rule, endpoint, f, **options)
            return f
        return decorator

    # 注册路由
    @setupmethod
    def add_url_rule(self, rule, endpoint=None, view_func=None,
                     provide_automatic_options=None, **options):

        # 如果endpoint传入的None，则使用视图函数名作为endpoint
        if endpoint is None:
            endpoint = _endpoint_from_view_func(view_func)
        options['endpoint'] = endpoint
        methods = options.pop('methods', None)

        # 默认的method是GET请求
        if methods is None:
            methods = getattr(view_func, 'methods', None) or ('GET',)
        if isinstance(methods, string_types):
            raise TypeError('Allowed methods have to be iterables of strings, '
                            'for example: @app.route(..., methods=["POST"])')
        methods = set(item.upper() for item in methods)

        ...
        ...
        ...

        rule = self.url_rule_class(rule, methods=methods, **options)
        rule.provide_automatic_options = provide_automatic_options

        # 将url->endpoint 的规则维护到url_map
        self.url_map.add(rule)
        if view_func is not None:
            old_func = self.view_functions.get(endpoint)
            if old_func is not None and old_func != view_func:
                raise AssertionError('View function mapping is overwriting an '
                                     'existing endpoint function: %s' % endpoint)
            # 记录endpoint 所指向的view_func
            self.view_functions[endpoint] = view_func
```

![image.png](https://upload-images.jianshu.io/upload_images/7220971-dfc98e1cb1b47c32.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240)

通过端点调试可以发现，Flask内部由url\_map 维护一个url->endpoint 的指向。由view\_functions 记录 endpoint所指向视图函数的函数，这样请求进入到Flask内部，才能通过Url找到对应的视图函数

## 4. 循环引入流程分析

从上面的断点调试中发现，我们的url\_maph和view\_functions中都已经维护了相关的信息。但是为什么还是会出现404的情况，这是因为fisher.py和book.py出现了循环引入的情况。

下面看下fisher.py和book.py的具体流程图 ![image.png](https://upload-images.jianshu.io/upload_images/7220971-3e9532c37fe07b0c.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240)

> 图中有两种颜色的线：红色的线是fisher主执行文件被执行之后的执行路径；蓝色的线是book模块被导入之后循环导入的执行路径。 1.主流程开始之后，首先到达导入book的语句。然后进入book模块中执行 2.book模块开始之后，首先到达导入fisher的语句（循环导入），这个时候主流程暂时结束，重新执行fisher中的代码 3.这时候又回到fisher中的导入book的语句，由于book已经被导入一次，所以不会再次导入，进入if语句，这个时候的**name**是book导入fisher时候的name:fisher，不是主流程**main**，所以if语句条件为false。蓝色线执行终止，重新回到2. book导入fisher的语句。 4.继续向下执行book 中app.route注册路由的语句。然后book执行完，回到fisher主流程执行中。 5.到达if语句，这个时候**name**为main。执行run方法，启动服务

回答流程图中的两个问题： 问题1：因为都是由fisher引入book，一个模块只会引入另一个模块一次。所以只执行了一次book 问题2：由于一次是主流程执行fisher文件；一次是由book模块导入 fisher。

## 5.找不到视图函数的最终解释和证明

### 整个流程中，出现了两次核心app对象的初始化，注册路由是在蓝色流程中初始化的app注册的。但是启动服务是红色流程中的app启动的

### book中注册路由所使用的app对象，是他自己所导入fisher模块的app对象（蓝色流程中），而不是红色主流程中所实例化的app对象

下面来加入一些日志出数验证我们的结论。我们在app实例化，启动，注册路由是哪个地方加入日志信息，来观察一下

book.py

```python
print("id为"+str(id(app))+"的app注册路由")


@app.route("/book/search/<q>/<page>")
def search(q, page):
    """
    搜索书籍路由
    :param q: 关键字 OR isbn
    :param page: 页码
    """
    isbn_or_key = is_isbn_or_key(q)
    if isbn_or_key == 'isbn':
        result = YuShuBook.search_by_isbn(q)
    else:
        result = YuShuBook.search_by_key(q)

    return jsonify(result)
```

fisher.py

```python
app = Flask(__name__)
print("id为"+str(id(app))+"的app实例化")

app.config.from_object("config")

# 为了可以注册book.py中的路由
from app.web import book

if __name__ == "__main__":
    print("id为" + str(id(app)) + "的app启动")
    app.run(host=app.config["HOST"], debug=app.config["DEBUG"], port=app.config["PORT"])
```

执行结果

```
pydev debugger: process 63816 is connecting

id为4350444824的app实例化
id为4355159656的app实例化
id为4355159656的app注册路由
id为4350444824的app启动
 * Debugger is active!
 * Debugger PIN: 176-669-651
```

可以看到注册路由的app，和启动服务的app不是同一个app。并且最后启动的app是最先实例化的app，也就是红色主流程的app；而注册路由的app是后实例化的app，也就是由book导入fisher模块的蓝色流程的app


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://flask-yushu.gitbook.io/yushu/2.-sou-suo-shu-ji-lu-you-bian-xie/2.6-jiang-shi-tu-han-shu-chai-fen-dao-dan-du-de-mo-kuai-zhong.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
