天天看點

源碼解析flask的路由系統

當我們建立一個flask項目時,pycharm通常已經為項目定義了一個基本路由

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

此時在浏覽器中輸入位址

http://127.0.0.1:5000

,頁面會顯示出"Hello World!"的字樣

如下圖所示

源碼解析flask的路由系統

那麼此時在flask背景程式中,到底發生了什麼事情呢??

在上面的例子中,可以看到對hello_world視圖函數被app.route這個有參裝假器裝飾

來看下app.route這個有參裝飾器的内部實作原理

app是Flask主程式的類執行個體化本項目名得到的一個對象

app = Flask(__name__)           

然後調用app對象的route方法來裝飾hello_world視圖函數

route方法的源碼:

def route(self, rule, **options):
    def decorator(f):
        endpoint = options.pop('endpoint', None)
        self.add_url_rule(rule, endpoint, f, **options)
        return f
    return decorator           

在用app.route裝飾hello_world視圖函數的時候,實際上app.route中還可以添加一些參數。

比如指定請求的方法的變量:

methods=["GET","POST"]

以及指定視圖函數的endpoint,相當于Django中視圖函數的别名等

在這裡,rule參數相當于hello_world視圖函數中的"/"路徑,options參數中包含methods和endpoint等

在route裝飾器裡,傳回decorator閉包函數。

在decorator閉包函數中,先從options中擷取endpoint的值,endpoint的值預設為None

然後調用self.add_url_rule内部方法處理傳遞的參數rule,endpoint,f等,在這裡self指的是app這個對象

檢視app對象中的add_url_rule方法:

@setupmethod
    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 the methods are not given and the view_func object knows its
        # methods we can use that instead.  If neither exists, we go with
        # a tuple of only ``GET`` as default.
        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)
    
        # Methods that should always be added
        required_methods = set(getattr(view_func, 'required_methods', ()))
    
        # starting with Flask 0.8 the view_func object can disable and
        # force-enable the automatic options handling.
        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
    
        # Add the required methods now.
        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           

可以看到,當在視圖函數中沒有指定endpoint時,程式會調用_endpoint_from_view_func方法為endpoint指派

def _endpoint_from_view_func(view_func):
    assert view_func is not None, 'expected view func if endpoint ' \
                                  'is not provided.'
    return view_func.__name__           

可以看出,_endpoint_from_view_func實際上傳回的就是view_func函數的函數名。

在上面的例子中,view_func指的是hello_world這個視圖函數

是以此時,在options這個字典中有一個鍵為endpoint,對應的值為view_func函數的函數名

接着,程式從options字典中彈出"methods"的值,并對methods中的每個方法轉換為大寫,如果methods沒有從程式中擷取,則預設為"GET"

接着,程式從函數中擷取"required_methods"的值,并進行去重,預設得到一個空集合

再對methods和required_methods進行

"|="

操作,也就是按位或運算

|=(按位或)運算
    
    >>> a = 15
    >>> bin(a)
    '0b1111'
    >>> b = 100
    >>> bin(b)
    '0b1100100'
    >>> a |= b
    >>> a
    111
    >>> bin(a)
    '0b1101111'
    >>> 0b1101111
    111
    
    先把a這個十進制數轉換成二進制,得到1111
    再把b這個十進制數轉換成二進制,得到1100100
    
    對a和b的二進制格式進行按位或運算
    
    a   000  0111
    b   110  0100
        110  0111
    
    因為a轉換成二進制隻有四位,如果要和b的二進制格式做位運算,則必須在頭部填充0到和b的二進制相同的長度,得到"0000111"
    
    或運算中,隻要有一個數為1,則這一位上做或運算的結果就為1
    是以上面兩個數做或運算得到的二進制數為"0b1100111"
    把這個二進制數轉換成十進制,則為111,把111這個十進制數指派給a           

對methods和required_methods進行按位或運算,實際上就是把required_methods的值添加到methods方法集合裡

接着程式調用

self.url_rule_class

方法處理rule(也就是"/"),methods和options字典

得到rule這個對象,在這裡self同樣指的是app這個對象

可以看到,url_rule_class指向的是Rule這個類的記憶體位址

url_rule_class = Rule           

然後用Map類執行個體化得到self.url_map對象,調用self.url_map對象中的add方法處理rule這個對象

self.url_map = Map()           

分析了上面的app.route的流程,知道使用app對象的route方法裝飾rule,實際上就是執行了add_url_rule這個方法

那如果定義一個視圖函數,調用app對象中的add_url_rule方法來處理對應的rule,是不是也可以完成route的裝飾器功能呢

from flask import Flask
    app = Flask(__name__)
    
    @app.route('/')
    def hello_world():
        return 'Hello World!'
    
    def login():
        return "登入成功!!"
    
    app.add_url_rule("/login",endpoint=None,view_func=login,methods=["GET"])           

啟動這個項目,在浏覽器中打開"http://127.0.0.1:5000/login"位址,

得到的效果如下

源碼解析flask的路由系統

由些我們可以知道,雖然flask的路由實作表面上是使用了route這個裝飾器,實際上内部也是調用app對象中的add_url_rule方法來實作,類似于Django中中路由的用法

多個路由指向同一個視圖函數

使用多個路由指向同一個視圖函數

from flask import Flask
app = Flask(__name__)

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

def login():
    return "登入成功!!"

@app.route("/index1/")
@app.route("/index2/")
def index():
    return "This is the index page!!"

app.add_url_rule("/login",endpoint=None,view_func=login,methods=["GET"])           

啟動項目,在浏覽器中分别打開

http://127.0.0.1:5000/index1/

http://127.0.0.1:5000/index2/

,可以看到前端頁面指向同一個頁面

源碼解析flask的路由系統
源碼解析flask的路由系統

使用正規表達式進行路由比對

在Django中,可以有路由系統中調用正規表達式進行路由規則比對,在flask中調用

werkzeug

插件也可以實作這個功能

from flask import Flask, render_template, redirect
from werkzeug.routing import BaseConverter

class RegexConverter(BaseConverter):
    def __init__(self,url_map,*items):
        super(RegexConverter,self).__init__(url_map)
        self.regex = items[0]

app = Flask(__name__)
app.debug = True
app.url_map.converters['regex'] = RegexConverter

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

@app.route('/user/<regex("[0-9]{4}"):user_id>')
def user1(user_id):
    return "User %s" % user_id

@app.route('/user/<regex("[a-z]{4,8}"):user_id>')
def user2(user_id):
    return "User %s" % user_id

@app.route("/index1/")
@app.route("/index2/")
def index():
    return "This is the index page!!"

@app.errorhandler(404)
def page_not_found(error):
    return render_template('404.html'),404

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

啟動項目,就可以使用正規表達式進行使用者名的規則比對了

在上面的例子裡,使用者名可以由4位數字或4到8位的小寫字母組成,還實作了404頁面的路由

當使用者輸入的路由傳回狀态碼為404時,執行page_not_found這個視圖函數

用浏覽器分别打開包含不同類型使用者名的URL,可以看到實作了正規表達式進行URL比對

例1:輸入3位數字的使用者名

源碼解析flask的路由系統

例2:輸入4位數字的使用者名

源碼解析flask的路由系統

例3:輸入5位數字的使用者名

源碼解析flask的路由系統

例4:輸入3位小寫字母的使用者名

源碼解析flask的路由系統

例5:輸入4到8位小寫字母的使用者名

源碼解析flask的路由系統
源碼解析flask的路由系統

例6:輸入9位小寫字母的使用者名

源碼解析flask的路由系統

由此可以實作在路由中完成正規表達式的規則比對了