用盡洪荒之力學(xué)習(xí)Flask源碼

[TOC]
一直想做源碼閱讀這件事,總感覺難度太高時(shí)間太少,可望不可見。最近正好時(shí)間充裕,決定試試做一下,并記錄一下學(xué)習(xí)心得。
首先說明一下,本文研究的Flask版本是0.12。
首先做個(gè)小示例,在pycharm新建flask項(xiàng)目"flask_source"后,默認(rèn)創(chuàng)建項(xiàng)目入口"flask_source.py"文件。
運(yùn)行該文件,在瀏覽器上訪問 http://127.0.0.1:5000/上可以看到“hello,world"內(nèi)容。這是flask_source.py源碼:

#源碼樣例-1
from flask import Flask

app = Flask(__name__)


@app.route('/')
def hello_world():
    return 'Hello World!'


if __name__ == '__main__':
    app.run()

本篇博文的目標(biāo):閱讀flask源碼了解flask服務(wù)器啟動(dòng)后,用戶訪問http://127.0.0.1:5000/后瀏覽“Hello World"這個(gè)過程Flask的工作原理及代碼框架。

WSGI

WSGI,全稱 Web Server Gateway Interface,或者 Python Web Server Gateway Interface ,是基于 Python 定義的 Web 服務(wù)器和 Web 應(yīng)用程序或框架之間的一種簡單而通用的接口。WSGI接口的作用是確保HTTP請求能夠轉(zhuǎn)化成python應(yīng)用的一個(gè)功能調(diào)用,這也就是Gateway的意義所在,網(wǎng)關(guān)的作用就是在協(xié)議之前進(jìn)行轉(zhuǎn)換
WSGI接口中有一個(gè)非常明確的標(biāo)準(zhǔn),每個(gè)Python Web應(yīng)用必須是可調(diào)用callable的對象且返回一個(gè)iterator,并實(shí)現(xiàn)了app(environ, start_response) 的接口,server 會(huì)調(diào)用 application,并傳給它兩個(gè)參數(shù):environ 包含了請求的所有信息,start_response 是 application 處理完之后需要調(diào)用的函數(shù),參數(shù)是狀態(tài)碼、響應(yīng)頭部還有錯(cuò)誤信息。引用代碼示例:

#源碼樣例-2
# 1. 可調(diào)用對象是一個(gè)函數(shù)
def application(environ, start_response):

  response_body = 'The request method was %s' % environ['REQUEST_METHOD']

  # HTTP response code and message
  status = '200 OK'

  # 應(yīng)答的頭部是一個(gè)列表,每對鍵值都必須是一個(gè) tuple。
  response_headers = [('Content-Type', 'text/plain'),
                      ('Content-Length', str(len(response_body)))]

  # 調(diào)用服務(wù)器程序提供的 start_response,填入兩個(gè)參數(shù)
  start_response(status, response_headers)

  # 返回必須是 iterable
  return [response_body]

#2. 可調(diào)用對象是一個(gè)類實(shí)例
class AppClass:
    """這里的可調(diào)用對象就是 AppClass 的實(shí)例,使用方法類似于: 
        app = AppClass()
        for result in app(environ, start_response):
            do_somthing(result)
    """

    def __init__(self):
        pass

    def __call__(self, environ, start_response):
        status = '200 OK'
        response_headers = [('Content-type', 'text/plain')]
        self.start(status, response_headers)
        yield "Hello world!\n"
The Python WSGI server-application interface

如上圖所示,F(xiàn)lask框架包含了與WSGI Server通信部分和Application本身。Flask Server本身也包含了一個(gè)簡單的WSGI Server(這也是為什么運(yùn)行flask_source.py可以在瀏覽器訪問的原因)用以開發(fā)測試使用。在實(shí)際的生產(chǎn)部署中,我們將使用apache、nginx+Gunicorn等方式進(jìn)行部署,以適應(yīng)性能要求。

app.run()

下圖是服務(wù)器啟動(dòng)和處理請求的流程圖,本節(jié)從分析這個(gè)圖開始:


flask的核心組件有兩個(gè)Jinjia2和werkzeug。
Jinjia2是一個(gè)基于python實(shí)現(xiàn)的模板引擎,提供對于HTML的頁面解釋,當(dāng)然它的功能非常豐富,可以結(jié)合過濾器、集成、變量、流程邏輯支持等作出非常簡單又很酷炫的的web出來。Flask類實(shí)例運(yùn)行會(huì)創(chuàng)造一個(gè)Jinjia的環(huán)境。
在本文使用的樣例中,我們是直接返回"Hello, world"字符串生成響應(yīng),因此本文將不詳細(xì)介紹Jinjia2引擎,但不否認(rèn)Jinjia2對于Flask非常重要也非常有用,值得重點(diǎn)學(xué)習(xí)。不過在源碼學(xué)習(xí)中重點(diǎn)看的是werkzeug。

werkzeug

werkzeug是基于python實(shí)現(xiàn)的WSGI的工具組件庫,提供對HTTP請求和響應(yīng)的支持,包括HTTP對象封裝、緩存、cookie以及文件上傳等等,并且werkzeug提供了強(qiáng)大的URL路由功能。具體應(yīng)用到Flask中:

  1. Flask使用werkzeug庫中的Request類和Response類來處理HTTP請求和響應(yīng)
  2. Flask應(yīng)用使用werkzeug庫中的Map類和Rule類來處理URL的模式匹配,每一個(gè)URL模式對應(yīng)一個(gè)Rule實(shí)例,這些Rule實(shí)例最終會(huì)作為參數(shù)傳遞給Map類構(gòu)造包含所有URL模式的一個(gè)“地圖”。
  3. Flask使用SharedDataMiddleware來對靜態(tài)內(nèi)容的訪問支持,也即是static目錄下的資源可以被外部,

Flask的示例運(yùn)行時(shí)將與werkzeug進(jìn)行大量交互:

#源碼樣例-3
def run(self, host=None, port=None, debug=None, **options):
        from werkzeug.serving import run_simple
        if host is None:
            host = '127.0.0.1'
        if port is None:
            server_name = self.config['SERVER_NAME']
            if server_name and ':' in server_name:
                port = int(server_name.rsplit(':', 1)[1])
            else:
                port = 5000
        if debug is not None:
            self.debug = bool(debug)
        options.setdefault('use_reloader', self.debug)
        options.setdefault('use_debugger', self.debug)
        try:
            run_simple(host, port, self, **options)
        finally:
            # reset the first request information if the development server
            # reset normally.  This makes it possible to restart the server
            # without reloader and that stuff from an interactive shell.
            self._got_first_request = False

排除設(shè)置host、port、debug模式這些參數(shù)操作以外,我們重點(diǎn)關(guān)注第一句函數(shù)from werkzeug.serving import run_simple
基于wekzeug,可以迅速啟動(dòng)一個(gè)WSGI應(yīng)用,官方文檔 上有詳細(xì)的說明,感興趣的同學(xué)可以自行研究。我們繼續(xù)分析Flask如何與wekzeug調(diào)用。

Flask調(diào)用run_simple共傳入5個(gè)參數(shù),分別是host=127.0.0.1, port=5001,self=app,use_reloader=False,use_debugger=False。按照上述代碼默認(rèn)啟動(dòng)的話,在run_simple函數(shù)中,我們執(zhí)行了以下的代碼:

#源碼樣例-4
def inner():
        try:
            fd = int(os.environ['WERKZEUG_SERVER_FD'])
        except (LookupError, ValueError):
            fd = None
        srv = make_server(hostname, port, application, threaded,
                          processes, request_handler,
                          passthrough_errors, ssl_context,
                          fd=fd)
        if fd is None:
            log_startup(srv.socket)
        srv.serve_forever()

上述的代碼主要的工作是啟動(dòng)WSGI server并監(jiān)聽指定的端口。
WSGI server啟動(dòng)之后,如果收到新的請求,它的監(jiān)聽在serving.py的run_wsgi中,執(zhí)行的代碼如下:

#源碼樣例-5
def execute(app):
    application_iter = app(environ, start_response)
    try:
        for data in application_iter:
            write(data)
        if not headers_sent:
            write(b'')
    finally:
        if hasattr(application_iter, 'close'):
            application_iter.close()
        application_iter = None

還記得上面介紹WSGI的內(nèi)容時(shí)候強(qiáng)調(diào)的python web實(shí)現(xiàn)時(shí)需要實(shí)現(xiàn)的一個(gè)WSGI標(biāo)準(zhǔn)接口,特別是源碼樣例-2中的第二個(gè)參考樣例實(shí)現(xiàn),F(xiàn)lask的實(shí)現(xiàn)與之類似,當(dāng)服務(wù)器(gunicorn/uwsgi...)接收到HTTP請求時(shí),它通過werkzeug再execute函數(shù)中通過application_iter = app(environ, start_response)調(diào)用了Flask應(yīng)用實(shí)例app(在run_simple中傳進(jìn)去的),實(shí)際上調(diào)用的是Flask類的call方法,因此Flask處理HTTP請求的流程將從call開始,代碼如下:

#源碼樣例-6
def __call__(self, environ, start_response):
        """Shortcut for :attr:`wsgi_app`."""
        return self.wsgi_app(environ, start_response)

我們來看一下wsgi_app這個(gè)函數(shù)做了什么工作:

#源碼樣例-7
def wsgi_app(self, environ, start_response):
    ctx = self.request_context(environ)
    ctx.push()
    error = None
    try:
        try:
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.handle_exception(e)
        return response(environ, start_response)
    finally:
        if self.should_ignore_error(error):
            error = None
        ctx.auto_pop(error)

在Flask的源碼注釋中,開發(fā)者顯著地標(biāo)明"The actual WSGI application.",這個(gè)函數(shù)的工作流程包括:

  1. ctx = self.request_context(environ)創(chuàng)建請求上下文,并把它推送到棧中,在“上下文”章節(jié)我們會(huì)介紹其數(shù)據(jù)結(jié)構(gòu)。
  2. response = self.full_dispatch_request()處理請求,通過flask的路由尋找對應(yīng)的視圖函數(shù)進(jìn)行處理,會(huì)在下一章介紹這個(gè)函數(shù)
  3. 通過try...except封裝處理步驟2的處理函數(shù),如果有問題,拋出500錯(cuò)誤。
  4. ctx.auto_pop(error)當(dāng)前請求退棧。

@app.route('/')

Flask路由的作用是用戶的HTTP請求對應(yīng)的URL能找到相應(yīng)的函數(shù)進(jìn)行處理。
@app.route('/')通過裝飾器的方式為對應(yīng)的視圖函數(shù)指定URL,可以一對多,即一個(gè)函數(shù)對應(yīng)多個(gè)URL。
Flask路由的實(shí)現(xiàn)時(shí)基于werkzeug的URL Routing功能,因此在分析Flask的源碼之前,首先學(xué)習(xí)一下werkzeug是如何處理路由的。
werkzeug有兩類數(shù)據(jù)結(jié)構(gòu):MapRule:

  • Map,主要作用是提供ImmutableDict來存儲URL的Rule實(shí)體。
  • Rule,代表著URL與endpoint一對一匹配的模式規(guī)則。
    舉例說明如下,假設(shè)在werkzeug中設(shè)置了如下的路由,當(dāng)用戶訪問http://myblog.com/,werkzeug會(huì)啟用別名為blog/index的函數(shù)來處理用戶請求。
#源碼樣例-8
from werkzeug.routing import Map, Rule, NotFound, RequestRedirect

url_map = Map([
    Rule('/', endpoint='blog/index'),
    Rule('/<int:year>/', endpoint='blog/archive'),
    Rule('/<int:year>/<int:month>/', endpoint='blog/archive'),
    Rule('/<int:year>/<int:month>/<int:day>/', endpoint='blog/archive'),
    Rule('/<int:year>/<int:month>/<int:day>/<slug>',
        endpoint='blog/show_post'),
    Rule('/about', endpoint='blog/about_me'),
    Rule('/feeds/', endpoint='blog/feeds'),
    Rule('/feeds/<feed_name>.rss', endpoint='blog/show_feed')
])

def application(environ, start_response):
    urls = url_map.bind_to_environ(environ)
    try:
        endpoint, args = urls.match()
    except HTTPException, e:
        return e(environ, start_response)
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return ['Rule points to %r with arguments %r' % (endpoint, args)]

更多關(guān)于werkzeug路由的細(xì)節(jié)可以看官方文檔
在上面的示例中,werkzeug完成了url與endpoint的匹配,endpoint與視圖函數(shù)的匹配將由Flask來完成,F(xiàn)lask通過裝飾器的方式來包裝app.route,實(shí)際工作函數(shù)是add_url_rule,其工作流程如下:

  1. 處理endpoint和構(gòu)建methods,methods默認(rèn)是GET和OPTIONS,即默認(rèn)處理的HTTP請求是GET/OPTIONS方式;
  2. self.url_map.add(rule) 更新url_map,本質(zhì)是更新werkzeug的url_map
  3. self.view_functions[endpoint] = view_func 更新view_functions,更新endpoint和視圖函數(shù)的匹配,兩者必須一一匹配,否則報(bào)錯(cuò)AssertionError。
#源碼樣例-9
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
    if endpoint is None:
        endpoint = _endpoint_from_view_func(view_func)
    options['endpoint'] = endpoint
    methods = options.pop('methods', None)
    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)
    required_methods = set(getattr(view_func, 'required_methods', ()))

    provide_automatic_options = getattr(view_func,
                                        'provide_automatic_options', None)

    if provide_automatic_options is None:
        if 'OPTIONS' not in methods:
            provide_automatic_options = True
            required_methods.add('OPTIONS')
        else:
            provide_automatic_options = False

    methods |= required_methods

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

    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)
        self.view_functions[endpoint] = view_func

設(shè)置好了Flask的路由之后,接下來再看看在上一章節(jié)中當(dāng)用戶請求進(jìn)來后是如何匹配請求和視圖函數(shù)的。
用戶請求進(jìn)來后,F(xiàn)lask類的wsgi_app函數(shù)進(jìn)行處理,其調(diào)用了full_dispatch_request函數(shù)進(jìn)行處理:

#源碼樣例-10
def full_dispatch_request(self):
    self.try_trigger_before_first_request_functions()
    try:
        request_started.send(self)
        rv = self.preprocess_request()
        if rv is None:
            rv = self.dispatch_request()
    except Exception as e:
        rv = self.handle_user_exception(e)
    return self.finalize_request(rv)

講一下這個(gè)處理的邏輯:

  1. self.try_trigger_before_first_request_functions()觸發(fā)第一次請求之前需要處理的函數(shù),只會(huì)執(zhí)行一次。
  2. self.preprocess_request()觸發(fā)用戶設(shè)置的在請求處理之前需要執(zhí)行的函數(shù),這個(gè)可以通過@app.before_request來設(shè)置,使用的樣例可以看我之前寫的博文中的示例-11
  3. rv = self.dispatch_request() 核心的處理函數(shù),包括了路由的匹配,下面會(huì)展開來講
  4. rv = self.handle_user_exception(e) 處理異常
  5. return self.finalize_request(rv),將返回的結(jié)果轉(zhuǎn)換成Response對象并返回。
    接下來我們看dispatch_request函數(shù),源碼樣例-11:
#源碼樣例-11
def dispatch_request(self):
    req = _request_ctx_stack.top.request
    if req.routing_exception is not None:
        self.raise_routing_exception(req)
    rule = req.url_rule
    if getattr(rule, 'provide_automatic_options', False) \
            and req.method == 'OPTIONS':
        return self.make_default_options_response()
    return self.view_functions[rule.endpoint](**req.view_args)

處理的邏輯如下:

  1. req = _request_ctx_stack.top.request獲得請求對象,并檢查有效性。
  2. 對于請求的方法進(jìn)行判斷,如果HTTP請求時(shí)OPTIONS類型且用戶未設(shè)置provide_automatic_options=False,則進(jìn)入默認(rèn)的OPTIONS請求回應(yīng),否則請求endpoint匹配的函數(shù)執(zhí)行,并返回內(nèi)容。
    在上述的處理邏輯中,F(xiàn)lask從請求上下文中獲得匹配的rule,這是如何實(shí)現(xiàn)的呢,請看下一節(jié)“上下文”。

Context

對象 上下文類型 說明
current_app AppContext 當(dāng)前的應(yīng)用對象
g AppContext 處理請求時(shí)用作臨時(shí)存儲的對象,當(dāng)前請求結(jié)束時(shí)被銷毀
request RequestContext 請求對象,封裝了HTTP請求的額內(nèi)容
session RequestContext 用于存儲請求之間需要共享的數(shù)據(jù)

純粹的上下文Context理解可以參見知乎的這篇文章,可以認(rèn)為上下文就是程序的工作環(huán)境。
Flask的上下文較多,用途也不一致,具體包括:

對象 上下文類型 說明
current_app AppContext 當(dāng)前的應(yīng)用對象
g AppContext 處理請求時(shí)用作臨時(shí)存儲的對象,當(dāng)前請求結(jié)束時(shí)被銷毀
request RequestContext 請求對象,封裝了HTTP請求的額內(nèi)容
session RequestContext 用于存儲請求之間需要共享的數(shù)據(jù)

引用博文Flask 的 Context 機(jī)制:

App Context 代表了“應(yīng)用級別的上下文”,比如配置文件中的數(shù)據(jù)庫連接信息;Request Context 代表了“請求級別的上下文”,比如當(dāng)前訪問的 URL。這兩種上下文對象的類定義在 flask.ctx 中,它們的用法是推入 flask.globals 中創(chuàng)建的 _app_ctx_stack 和 _request_ctx_stack 這兩個(gè)單例 Local Stack 中。因?yàn)?Local Stack 的狀態(tài)是線程隔離的,而 Web 應(yīng)用中每個(gè)線程(或 Greenlet)同時(shí)只處理一個(gè)請求,所以 App Context 對象和 Request Context 對象也是請求間隔離的。

在深入分析上下文源碼之前,需要特別介紹一下Local、LocalProxy和LocalStack。這是由werkzeug的locals模塊提供的數(shù)據(jù)結(jié)構(gòu):

Local

#源碼樣例-12
class Local(object):
    __slots__ = ('__storage__', '__ident_func__')

    def __init__(self):
        object.__setattr__(self, '__storage__', {})
        object.__setattr__(self, '__ident_func__', get_ident)

   def __getattr__(self, name):
        try:
            return self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}

Local維護(hù)了兩個(gè)對象:1.stroage,字典;2.idente_func, 調(diào)用的是thread的get_indent方法,從_thread內(nèi)置模塊導(dǎo)入,得到的線程號。
注意,這里的stroage的數(shù)據(jù)組織形式是:storage ={ident1:{name1:value1},ident2:{name2:value2},ident3:{name3:value3}}所以取值時(shí)候getattr通過self.__storage__[self.__ident_func__()][name]獲得。
這種設(shè)計(jì)確保了Local類實(shí)現(xiàn)了類似 threading.local 的效果——多線程或者多協(xié)程情況下全局變量的相互隔離。

LocalStack

一種基于棧的數(shù)據(jù)結(jié)構(gòu),其本質(zhì)是維護(hù)了一個(gè)Locals對象的代碼示例如下:

#源碼樣例-13
class LocalStack(object):
    def __init__(self):
        self._local = Local()

   def push(self, obj):
        rv = getattr(self._local, 'stack', None)
        if rv is None:
            self._local.stack = rv = []
        rv.append(obj)
        return rv

    def pop(self):
        stack = getattr(self._local, 'stack', None)
        if stack is None:
            return None
        elif len(stack) == 1:
            release_local(self._local)
            return stack[-1]
        else:
            return stack.pop()

    @property
    def top(self):
        """The topmost item on the stack.  If the stack is empty,
        `None` is returned.
        """
        try:
            return self._local.stack[-1]
        except (AttributeError, IndexError):
            return None
>>> ls = LocalStack()
>>> ls.push(42)
>>> ls.top
42
>>> ls.push(23)
>>> ls.top
23
>>> ls.pop()
23
>>> ls.top
42

LocalProxy

典型的代理模式實(shí)現(xiàn),在構(gòu)造時(shí)接受一個(gè)callable參數(shù),這個(gè)參數(shù)被調(diào)用后返回對象是一個(gè)Thread Local的對象,對一個(gè)LocalProxy對象的所有操作,包括屬性訪問、方法調(diào)用都會(huì)轉(zhuǎn)發(fā)到Callable參數(shù)返回的對象上。LocalProxy 的一個(gè)使用場景是 LocalStack 的 call 方法。比如 my_local_stack 是一個(gè) LocalStack 實(shí)例,那么 my_local_stack() 能返回一個(gè) LocalProxy 對象,這個(gè)對象始終指向 my_local_stack 的棧頂元素。如果棧頂元素不存在,訪問這個(gè) LocalProxy 的時(shí)候會(huì)拋出 RuntimeError。
LocalProxy的初始函數(shù):

#源碼樣例-14
def __init__(self, local, name=None):
    object.__setattr__(self, '_LocalProxy__local', local)
    object.__setattr__(self, '__name__', name)

LocalProxy與LocalStack可以完美地結(jié)合起來,首先我們注意LocalStack的call方法:

#源碼樣例-15
def __call__(self):
    def _lookup():
        rv = self.top
        if rv is None:
            raise RuntimeError('object unbound')
        return rv
    return LocalProxy(_lookup)

假設(shè)創(chuàng)建一個(gè)LocalStack實(shí)例:

#源碼樣例-16
_response_local = LocalStack()
response = _response_local()

然后,response就成了一個(gè)LocalProxy對象,能操作LocalStack的棧頂元素,該對象有兩個(gè)元素:_LocalProxy__local(等于_lookup函數(shù))和name(等于None)。

這種設(shè)計(jì)簡直碉堡了!!!!

回到Flask的上下文處理流程,這里引用Flask的核心機(jī)制!關(guān)于請求處理流程和上下文的一張圖進(jìn)行說明:

Context Create

#源碼樣例-17
def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)

def _lookup_app_object(name):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return getattr(top, name)

def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app

# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))

從源碼可以了解到以下內(nèi)容:
*. Flask維護(hù)的request全局變量_request_ctx_stack 和app全局變量_app_ctx_stack 均為LocalStack結(jié)構(gòu),這兩個(gè)全局變量均是Thread local的棧結(jié)構(gòu)
*. request、session每次都是調(diào)用_request_ctx_stack棧頭部的數(shù)據(jù)來獲取和保存里面的請求上下文信息。

為什么需要LocalProxy對象,而不是直接引用LocalStack的值?引用flask 源碼解析:上下文的介紹:

這是因?yàn)?flask 希望在測試或者開發(fā)的時(shí)候,允許多 app 、多 request 的情況。而 LocalProxy 也是因?yàn)檫@個(gè)才引入進(jìn)來的!我們拿 current_app = LocalProxy(_find_app) 來舉例子。每次使用 current_app 的時(shí)候,他都會(huì)調(diào)用 _find_app 函數(shù),然后對得到的變量進(jìn)行操作。如果直接使用 current_app = _find_app() 有什么區(qū)別呢?區(qū)別就在于,我們導(dǎo)入進(jìn)來之后,current_app 就不會(huì)再變化了。如果有多 app 的情況,就會(huì)出現(xiàn)錯(cuò)誤。

原文示例代碼:

#源碼樣例-18
from flask import current_app

app = create_app()
admin_app = create_admin_app()

def do_something():
    with app.app_context():
        work_on(current_app)
        with admin_app.app_context():
            work_on(current_app)

我的理解是:Flask考慮了一些極端的情況出現(xiàn),例如兩個(gè)Flask APP通過WSGI的中間件組成一個(gè)應(yīng)用,兩個(gè)APP同時(shí)運(yùn)行的情況,因此需要?jiǎng)討B(tài)的更新當(dāng)前的應(yīng)用上下文,而_app_ctx_stack每次都指向棧的頭元素,并且更新頭元素(如果存在刪除再創(chuàng)建)來確保當(dāng)前運(yùn)行的上下文(包括請求上下文和應(yīng)用上下文)的準(zhǔn)確

Stack push

在本文第二章節(jié)介紹Flask運(yùn)行流程的內(nèi)容時(shí),我們介紹了wsig_app函數(shù),這個(gè)函數(shù)是處理用戶的HTTP請求的,其中有兩句ctx = self.request_context(environ)ctx.push()兩句。
本質(zhì)上實(shí)例了一個(gè)RequestContext,通過WSGI server傳過來的environ來構(gòu)建一個(gè)請求的上下文。源碼:

#源碼樣例-19
class RequestContext(object):
    def __init__(self, app, environ, request=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = app.create_url_adapter(self.request)
        self.flashes = None
        self.session = None      
        self.preserved = False
        self._preserved_exc = None
        self._after_request_functions = []
        self.match_request()

    def push(self):
        top = _request_ctx_stack.top
        if top is not None and top.preserved:
            top.pop(top._preserved_exc)
        app_ctx = _app_ctx_stack.top
        if app_ctx is None or app_ctx.app != self.app:
            app_ctx = self.app.app_context()
            app_ctx.push()
            self._implicit_app_ctx_stack.append(app_ctx)
        else:
            self._implicit_app_ctx_stack.append(None)

        if hasattr(sys, 'exc_clear'):
            sys.exc_clear()

        _request_ctx_stack.push(self)

        self.session = self.app.open_session(self.request)
        if self.session is None:
            self.session = self.app.make_null_session()

Flask的上下文入棧的操作在RequestContext類的push函數(shù):

  1. 清空_request_ctx_stack棧;
  2. 確保當(dāng)前的Flask實(shí)例推入_app_ctx_stack棧;
  3. 根據(jù)WSGI服務(wù)器傳入的environ構(gòu)建了request(在init函數(shù)完成),將該request推入_request_ctx_stack棧;
  4. 創(chuàng)建session對象。

Stack pop

wsig_app函數(shù)在完成上一小節(jié)上下文入棧之后進(jìn)行請求分發(fā),進(jìn)行路由匹配尋找視圖函數(shù)處理請求,并生成響應(yīng),此時(shí)用戶可以在應(yīng)用程序中import上下文對象作為全局變量進(jìn)行訪問:

from flask import request,session,request,g

請求完成后,同樣在源碼樣例-7wsgi_app函數(shù)中可以看到上下文出棧的操作ctx.auto_pop(error),auto_pop函數(shù)只彈出請求上下文,應(yīng)用上下文仍然存在以應(yīng)對下次的HTTP請求。至此,上下文的管理和操作機(jī)制介紹完畢。

Request

接下來繼續(xù)學(xué)習(xí)Flask的請求對象。Flask是基于WSGI服務(wù)器werkzeug傳來的environ參數(shù)來構(gòu)建請求對象的,檢查發(fā)現(xiàn)environ傳入的是一個(gè)字典,在本文的經(jīng)典訪問樣例(返回“Hello, World")中,傳入的environ包含的信息包括

"wsgi.multiprocess":"False"
"SERVER_SOFTWARE":"Werkzeug/0.11.15"
"SCRIPT_NAME":""
"REQUEST_METHOD":"GET"
"PATH_INFO":"/favicon.ico"
"SERVER_PROTOCOL":"HTTP/1.1"
"QUERY_STRING":""
"werkzeug.server.shutdown":"<function shutdown_server at 0x0000000003F4FAC8>"
"CONTENT_LENGTH":""
"HTTP_USER_AGENT":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0"
"HTTP_CONNECTION":"keep-alive"
"SERVER_NAME":"127.0.0.1"
"REMOTE_PORT":"12788"
"wsgi.url_scheme":"http"
"SERVER_PORT":"5000"
"wsgi.input":"<socket._fileobject object at 0x0000000003E18408>"
"HTTP_HOST":"127.0.0.1:5000"
"wsgi.multithread":"False"
"HTTP_ACCEPT":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
"wsgi.version":"(1, 0)"
"wsgi.run_once":"False"
"wsgi.errors":"<open file '<stderr>', mode 'w' at 0x0000000001DD2150>"
"REMOTE_ADDR":"127.0.0.1"
"HTTP_ACCEPT_LANGUAGE":"zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3"
"CONTENT_TYPE":""
"HTTP_ACCEPT_ENCODING":"gzip, deflate"

Flask需要將WSGI server傳進(jìn)來的上述的字典改造成request對象,它是通過調(diào)用werkzeug.wrappers.Request類來進(jìn)行構(gòu)建。Request沒有構(gòu)造方法,且Request繼承了多個(gè)類,在

#源碼樣例-20
class Request(BaseRequest, AcceptMixin, ETagRequestMixin,
              UserAgentMixin, AuthorizationMixin,
              CommonRequestDescriptorsMixin):

    """Full featured request object implementing the following mixins:

    - :class:`AcceptMixin` for accept header parsing
    - :class:`ETagRequestMixin` for etag and cache control handling
    - :class:`UserAgentMixin` for user agent introspection
    - :class:`AuthorizationMixin` for http auth handling
    - :class:`CommonRequestDescriptorsMixin` for common headers
    """

這里有多重繼承,有多個(gè)類負(fù)責(zé)處理request的不同內(nèi)容,python的多重繼承按照從下往上,從左往右的入棧出棧順序進(jìn)行繼承,且看構(gòu)造方法的參數(shù)匹配。在Request的匹配中只有BaseRequest具有構(gòu)造函數(shù),其他類只有功能函數(shù),這種設(shè)計(jì)模式很特別,但是跟傳統(tǒng)的設(shè)計(jì)模式不太一樣,傳統(tǒng)的設(shè)計(jì)模式要求是多用組合少用繼承多用拓展少用修改,這種利用多重繼承來達(dá)到類功能組合的設(shè)計(jì)模式稱為Python的mixin模式,感覺的同學(xué)可以看看Python mixin模式,接下來重點(diǎn)關(guān)注BaseRequest。
底層的Request功能均由werkzeug來實(shí)現(xiàn),這邊不再一一贅述。

Response

在本文的源碼樣例-1中,訪問URL地址“http://127.0.0.1” 后,查看返回的response,除了正文文本"Hello, world"外,我們還可以得到一些額外的信息,通過Chrome調(diào)試工具可以看到:

以上的信息都是通過flask服務(wù)器返回,因此,在視圖函數(shù)返回“Hello,World”的響應(yīng)后,F(xiàn)lask對響應(yīng)做了進(jìn)一步的包裝。本章節(jié)分析一下Flask如何封裝響應(yīng)信息。
在本文的源碼樣例-10中用戶的請求由full_dispatch_request函數(shù)進(jìn)行處理,其調(diào)用了視圖函數(shù)index()返回得到rv='Hello, World',接下來調(diào)用了finalize_request函數(shù)進(jìn)行封裝,得到其源碼如下:

#源碼樣例-21
def finalize_request(self, rv, from_error_handler=False):
    response = self.make_response(rv)
    try:
        response = self.process_response(response)
        request_finished.send(self, response=response)
    except Exception:
        if not from_error_handler:
            raise
        self.logger.exception('Request finalizing failed with an '
                              'error while handling an error')
    return response

def make_response(self, rv):
    status_or_headers = headers = None
    if isinstance(rv, tuple):
        rv, status_or_headers, headers = rv + (None,) * (3 - len(rv))

    if rv is None:
        raise ValueError('View function did not return a response')

    if isinstance(status_or_headers, (dict, list)):
        headers, status_or_headers = status_or_headers, None

    if not isinstance(rv, self.response_class):
        if isinstance(rv, (text_type, bytes, bytearray)):
            rv = self.response_class(rv, headers=headers,
                                    status=status_or_headers)
            headers = status_or_headers = None
        else:
            rv = self.response_class.force_type(rv, request.environ)

    if status_or_headers is not None:
        if isinstance(status_or_headers, string_types):
            rv.status = status_or_headers
        else:
            rv.status_code = status_or_headers
    if headers:
        rv.headers.extend(headers)

    return rv

def process_response(self, response):
    ctx = _request_ctx_stack.top
    bp = ctx.request.blueprint
    funcs = ctx._after_request_functions
    if bp is not None and bp in self.after_request_funcs:
        funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
    if None in self.after_request_funcs:
        funcs = chain(funcs, reversed(self.after_request_funcs[None]))
    for handler in funcs:
        response = handler(response)
    if not self.session_interface.is_null_session(ctx.session):
        self.save_session(ctx.session, response)
    return response

返回信息的封裝順序如下:

  1. response = self.make_response(rv):根據(jù)視圖函數(shù)返回值生成response對象。
  2. response = self.process_response(response):在response發(fā)送給WSGI服務(wù)器錢對于repsonse進(jìn)行后續(xù)處理,并執(zhí)行當(dāng)前請求的后續(xù)hooks函數(shù)。
  3. request_finished.send(self, response=response) 向特定的訂閱者發(fā)送響應(yīng)信息。關(guān)于Flask的信號機(jī)制可以學(xué)習(xí)一下這篇博文,這里不再展開詳細(xì)說明。

make_response該函數(shù)可以根據(jù)不同的輸入得到不同的輸出,即參數(shù)rv的類型是多樣化的,包括:

  • str/unicode,如源碼樣例-1所示直接返回str后,將其設(shè)置為body主題后,調(diào)用response_class生成其他響應(yīng)信息,例如狀態(tài)碼、headers信息。
  • tuple 通過構(gòu)建status_or_headers和headers來進(jìn)行解析。在源碼樣例-1可以修改為返回return make_response(('hello,world!', 202, None)),得到返回碼也就是202,即可以在視圖函數(shù)中定義返回狀態(tài)碼和返回頭信息。
  • WSGI方法:這個(gè)用法沒有找到示例,不常見。
  • response類實(shí)例。視圖函數(shù)可以直接通過調(diào)用make_response接口,該接口可以提供給用戶在視圖函數(shù)中設(shè)計(jì)自定制的響應(yīng),可以參見我之前寫的博文lask自帶的常用組件介紹, 相較于tuple類型,response能更加豐富和方便地訂制響應(yīng)。

process_response處理了兩個(gè)邏輯:

  1. 將用戶定義的after_this_request方法進(jìn)行執(zhí)行,同時(shí)檢查了是否在blueprint中定義了after_requestafter_app_request,如果存在,將其放在執(zhí)行序列;
  2. 保存sesseion。

上述的源碼是flask對于response包裝的第一層外殼,去除這個(gè)殼子可以看到,flask實(shí)際上調(diào)用了Response類對于傳入的參數(shù)進(jìn)行包裝,其源碼如下:

#源碼樣例-21
class Response(ResponseBase):
    """The response object that is used by default in Flask.  Works like the
    response object from Werkzeug but is set to have an HTML mimetype by
    default.  Quite often you don't have to create this object yourself because
    :meth:`~flask.Flask.make_response` will take care of that for you.

    If you want to replace the response object used you can subclass this and
    set :attr:`~flask.Flask.response_class` to your subclass.
    """
    default_mimetype = 'text/html'

嗯,基本上 沒啥內(nèi)容,就是繼承了werkzeug.wrappers:Response,注意上面的類注釋,作者明確建議使用flask自帶的make_response接口來定義response對象,而不是重新實(shí)現(xiàn)它。werkzeug實(shí)現(xiàn)Response的代碼參見教程 這里就不再展開分析了。

Config

Flask配置導(dǎo)入對于其他項(xiàng)目的配置導(dǎo)入有很好的借鑒意義,所以我這里還是作為一個(gè)單獨(dú)的章節(jié)進(jìn)行源碼學(xué)習(xí)。Flask常用的四種方式進(jìn)行項(xiàng)目參數(shù)的配置,分別是:

#Type1: 直接配置參數(shù)
app.config['SECRET_KEY'] = 'YOUCANNOTGUESSME'

#Type2: 從環(huán)境變量中獲得配置文件名并導(dǎo)入配置參數(shù)
export MyAppConfig=/path/to/settings.cfg #linux
set MyAppConfig=d:\settings.cfg#不能立即生效,不建議windows下通過這種方式獲得環(huán)境變量。
app.config.from_envvar('MyAppConfig')

#Type3: 從對象中獲得配置
class Config(object):
    DEBUG = False
    TESTING = False
    DATABASE_URI = 'sqlite://:memory:'
class ProductionConfig(Config):
    DATABASE_URI = 'mysql://user@localhost/foo'
app.config.from_object(ProductionConfig)
print app.config.get('DATABASE_URI')

#Type4: 從文件中獲得配置參數(shù)
# default_config.py
HOST = 'localhost'
PORT = 5000
DEBUG = True
# flask中使用
app.config.from_pyfile('default_config.py')

Flask已經(jīng)默認(rèn)自帶的配置包括:
['JSON_AS_ASCII', 'USE_X_SENDFILE', 'SESSION_COOKIE_PATH', 'SESSION_COOKIE_DOMAIN', 'SESSION_COOKIE_NAME', 'SESSION_REFRESH_EACH_REQUEST', 'LOGGER_HANDLER_POLICY', 'LOGGER_NAME', 'DEBUG', 'SECRET_KEY', 'EXPLAIN_TEMPLATE_LOADING', 'MAX_CONTENT_LENGTH', 'APPLICATION_ROOT', 'SERVER_NAME', 'PREFERRED_URL_SCHEME', 'JSONIFY_PRETTYPRINT_REGULAR', 'TESTING', 'PERMANENT_SESSION_LIFETIME', 'PROPAGATE_EXCEPTIONS', 'TEMPLATES_AUTO_RELOAD', 'TRAP_BAD_REQUEST_ERRORS', 'JSON_SORT_KEYS', 'JSONIFY_MIMETYPE', 'SESSION_COOKIE_HTTPONLY', 'SEND_FILE_MAX_AGE_DEFAULT', 'PRESERVE_CONTEXT_ON_EXCEPTION', 'SESSION_COOKIE_SECURE', 'TRAP_HTTP_EXCEPTIONS']
其中關(guān)于debug這個(gè)參數(shù)要特別的進(jìn)行說明,當(dāng)我們設(shè)置為app.config["DEBUG"]=True時(shí)候,flask服務(wù)啟動(dòng)后進(jìn)入調(diào)試模式,在調(diào)試模式下服務(wù)器的內(nèi)部錯(cuò)誤會(huì)展示到web前臺,舉例說明:

app.config["DEBUG"]=True

@app.route('/')
def hello_world():
    a=3/0
    return 'Hello World!'

打開頁面我們會(huì)看到


除了顯示錯(cuò)誤信息以外,F(xiàn)lask還支持從web中提供console進(jìn)行調(diào)試(需要輸入pin碼),破解pin碼很簡單,這意味著用戶可以對部署服務(wù)器執(zhí)行任意的代碼,所以如果Flask發(fā)布到生產(chǎn)環(huán)境,必須確保DEBUG=False
嗯,有空再寫一篇關(guān)于Flask的安全篇。另外,關(guān)于如何配置Flask參數(shù)讓網(wǎng)站更加安全,可以參考這篇博文,寫的很好。
接下來繼續(xù)研究Flask源碼中關(guān)于配置的部分。可以發(fā)現(xiàn)configapp的一個(gè)屬性,而app是Flask類的一個(gè)示例,并且可以通過app.config["DEBUG"]=True來設(shè)置屬性,可以大膽猜測config應(yīng)該是一個(gè)字典類型的類屬性變量,這一點(diǎn)在源碼中驗(yàn)證了:

#: The configuration dictionary as :class:`Config`.  This behaves
#: exactly like a regular dictionary but supports additional methods
#: to load a config from files.
self.config = self.make_config(instance_relative_config)

我們進(jìn)一步看看make_config函數(shù)的定義:

def make_config(self, instance_relative=False):
    """Used to create the config attribute by the Flask constructor.
    The `instance_relative` parameter is passed in from the constructor
    of Flask (there named `instance_relative_config`) and indicates if
    the config should be relative to the instance path or the root path
    of the application.

    .. versionadded:: 0.8
    """
    root_path = self.root_path
    if instance_relative:
        root_path = self.instance_path
    return self.config_class(root_path, self.default_config)

config_class = Config

其中有兩個(gè)路徑要選擇其中一個(gè)作為配置導(dǎo)入的默認(rèn)路徑,這個(gè)用法在上面推薦的博文中用到過,感興趣的看看,make_config真正功能是返回config_class的函數(shù),而這個(gè)函數(shù)直接指向Config類,也就是說make_config返回的是Config類的實(shí)例。似乎這里面有一些設(shè)計(jì)模式在里面,后續(xù)再研究一下。記下來是Config類的定義:

class Config(dict):
      def __init__(self, root_path, defaults=None):
        dict.__init__(self, defaults or {})
        self.root_path = root_path

root_path代表的是項(xiàng)目配置文件所在的目錄。defaults是Flask默認(rèn)的參數(shù),用的是immutabledict數(shù)據(jù)結(jié)構(gòu),是dict的子類,其中default中定義為:

#: Default configuration parameters.
    default_config = ImmutableDict({
        'DEBUG':                                get_debug_flag(default=False),
        'TESTING':                              False,
        'PROPAGATE_EXCEPTIONS':                None,
        'PRESERVE_CONTEXT_ON_EXCEPTION':        None,
        'SECRET_KEY':                          None,
        'PERMANENT_SESSION_LIFETIME':          timedelta(days=31),
        'USE_X_SENDFILE':                      False,
        'LOGGER_NAME':                          None,
        'LOGGER_HANDLER_POLICY':              'always',
        'SERVER_NAME':                          None,
        'APPLICATION_ROOT':                    None,
        'SESSION_COOKIE_NAME':                  'session',
        'SESSION_COOKIE_DOMAIN':                None,
        'SESSION_COOKIE_PATH':                  None,
        'SESSION_COOKIE_HTTPONLY':              True,
        'SESSION_COOKIE_SECURE':                False,
        'SESSION_REFRESH_EACH_REQUEST':        True,
        'MAX_CONTENT_LENGTH':                  None,
        'SEND_FILE_MAX_AGE_DEFAULT':            timedelta(hours=12),
        'TRAP_BAD_REQUEST_ERRORS':              False,
        'TRAP_HTTP_EXCEPTIONS':                False,
        'EXPLAIN_TEMPLATE_LOADING':            False,
        'PREFERRED_URL_SCHEME':                'http',
        'JSON_AS_ASCII':                        True,
        'JSON_SORT_KEYS':                      True,
        'JSONIFY_PRETTYPRINT_REGULAR':          True,
        'JSONIFY_MIMETYPE':                    'application/json',
        'TEMPLATES_AUTO_RELOAD':                None,
    })

我們再看看Config的三個(gè)導(dǎo)入函數(shù)from_envvar,from_pyfile, from_objectfrom_envvar相當(dāng)于在from_pyfile外面包了一層殼子,從環(huán)境變量中獲得,其函數(shù)注釋中也提到了這一點(diǎn)。而from_pyfile最終也是調(diào)用from_object。所以我們的重點(diǎn)是看from_object這個(gè)函數(shù)的細(xì)節(jié)。
from_pyfile源碼中有一句特別難懂,如下。config_file是讀取的文件頭,file_name是文件名稱。

exec (compile(config_file.read(), filename, 'exec'), d.__dict__)

dict是python的內(nèi)置屬性,包含了該對象(python萬事萬物都是對象)的屬性變量。類的實(shí)例對象的dict只包括類實(shí)例后的變量,而類對象本身的dict還包括包括一些類內(nèi)置屬性和類變量clsvar以及構(gòu)造方法init
再理解exec函數(shù),exec語句用來執(zhí)行存儲在代碼對象、字符串、文件中的Python語句,eval語句用來計(jì)算存儲在代碼對象或字符串中的有效的Python表達(dá)式,而compile語句則提供了字節(jié)編碼的預(yù)編譯。

exec(object[, globals[, locals]]) #內(nèi)置函數(shù)

其中參數(shù)obejctobj對象可以是字符串(如單一語句、語句塊),文件對象,也可以是已經(jīng)由compile預(yù)編譯過的代碼對象,本文就是最后一種。參數(shù)globals是全局命名空間,用來指定執(zhí)行語句時(shí)可以訪問的全局命名空間;參數(shù)locals是局部命名空間,用來指定執(zhí)行語句時(shí)可以訪問的局部作用域的命名空間。按照這個(gè)解釋,上述的語句其實(shí)是轉(zhuǎn)化成了這個(gè)語法:

import types
var2=types.ModuleType("test")
exec("A='bb'",var2.__dict__)

把配置文件中定義的參數(shù)寫入到了定義為config Module類型的變量d的內(nèi)置屬性dict中。
再看看complie函數(shù)compile( str, file, type )
compile語句是從type類型(包括’eval’: 配合eval使用,’single’: 配合單一語句的exec使用,’exec’: 配合多語句的exec使用)中將str里面的語句創(chuàng)建成代碼對象。file是代碼存放的地方,通常為”。compile語句的目的是提供一次性的字節(jié)碼編譯,就不用在以后的每次調(diào)用中重新進(jìn)行編譯了。

from_object源碼中將輸入的參數(shù)進(jìn)行類型判斷,如果是object類型的,則說明是通過from_pyfile中傳過來的,只要遍歷from_pyfile傳輸過來的d比變量的內(nèi)置屬性__dict__即可。如果輸入的string類型,意味著這個(gè)是要從默認(rèn)的config.py文件中導(dǎo)入,用戶需要輸入app.config.from_object("config")進(jìn)行明確,這時(shí)候根據(jù)config直接導(dǎo)入config.py配置。

具體的源碼細(xì)節(jié)如下:

def from_envvar(self, variable_name, silent=False):
    rv = os.environ.get(variable_name)
    if not rv:
        if silent:
            return False
        raise RuntimeError('The environment variable %r is not set '
                          'and as such configuration could not be '
                          'loaded.  Set this variable and make it '
                          'point to a configuration file' %
                          variable_name)
    return self.from_pyfile(rv, silent=silent)


def from_pyfile(self, filename, silent=False):
    filename = os.path.join(self.root_path, filename)
    d = types.ModuleType('config')
    d.__file__ = filename
    try:
        with open(filename) as config_file:
            exec (compile(config_file.read(), filename, 'exec'), d.__dict__)
    except IOError as e:
        if silent and e.errno in (errno.ENOENT, errno.EISDIR):
            return False
        e.strerror = 'Unable to load configuration file (%s)' % e.strerror
        raise
    self.from_object(d)
    return True


def from_object(self, obj):
    """Updates the values from the given object.  An object can be of one
    of the following two types:

    -  a string: in this case the object with that name will be imported
    -  an actual object reference: that object is used directly

    Objects are usually either modules or classes. :meth:`from_object`
    loads only the uppercase attributes of the module/class. A ``dict``
    object will not work with :meth:`from_object` because the keys of a
    ``dict`` are not attributes of the ``dict`` class.

    Example of module-based configuration::

        app.config.from_object('yourapplication.default_config')
        from yourapplication import default_config
        app.config.from_object(default_config)

    You should not use this function to load the actual configuration but
    rather configuration defaults.  The actual config should be loaded
    with :meth:`from_pyfile` and ideally from a location not within the
    package because the package might be installed system wide.

    See :ref:`config-dev-prod` for an example of class-based configuration
    using :meth:`from_object`.

    :param obj: an import name or object
    """
    if isinstance(obj, string_types):
        obj = import_string(obj)
    for key in dir(obj):
        if key.isupper():
            self[key] = getattr(obj, key)

根據(jù)源碼分析,from_envvarfrom_pyfile兩個(gè)函數(shù)的輸入配置文件必須是可以執(zhí)行的py文件,py文件中變量名必須是大寫,只有這樣配置變量參數(shù)才能順利的導(dǎo)入到Flask中。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容