深入理解Flask路由 (3) - 動態 url 及轉換器

本篇講解動態 url 和轉換器的用法及原理。

動態 url 實現原理

動態 url 由 werkzeug 通過轉換器 (converter) 來實現,爲說明動態 url 的使用方法,我們先從簡單的示例開始,逐步展開。假設我們要編寫一個向手機號碼發送短信的 Flask 程序,有如下一段代碼:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Index page'

@app.route('/message/<mobile_number>')
def send_message(mobile_number):
    return 'Message was sent to {}'.format(mobile_number)

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

由於 Flask 對路由過程作了較抽象的封裝,並不容易看出完整的過程,爲了便於理解,我們直接用下面的一段代碼來說明在 werkzeurg 中路由綁定 (bind) 和匹配 (match) 的過程。

from werkzeug.routing import Rule, Map
from werkzeug.serving import run_simple
from werkzeug.exceptions import HTTPException

rules = [
    Rule('/', endpoint='index'),
    Rule('/message/<mobile_number>', endpoint='mobile')
]
url_map = Map(rules)

def application(environ, start_response):
    urls = url_map.bind_to_environ(environ)
    try:
        endpoint, args = urls.match()
    except HTTPException as ex:
        return ex(environ, start_response)

    headers = [('Content-Type', 'text/plain')]
    start_response('200 OK', headers)

    body = 'Rule points to {} with arguments {}' \
        .format(endpoint, args).encode('utf-8')
    return [body]

if __name__ == "__main__":
    run_simple('localhost', 5000, application)

這段代碼能架起一個簡單的 Web 服務器。當客戶端從 /messages/mobile_number 發起 GET 請求,程序返回如下信息:

Rule points to mobile with arguments {'mobile_number': '13811112222'}

這段代碼展示了 werkzueg 路由過程的三大階段:

創建 Rule 和 Map 的實例上篇已經講過。本篇從第二步開始講解。

綁定到特定環境

bind_to_environ() 方法在內部調用了 Map.bind() 方法, Map.bind() 方法創建 MapAdapter 的實例。MapAdapter 類負責 URL 匹配的工作。

以下是關鍵代碼及說明。

  • bind_to_environ() 方法在內部調用了 Map.bind() 方法:
def bind_to_environ(self, environ, server_name=None, subdomain=None):   
    # 其他代碼略
    
    return Map.bind(
        self,
        server_name,
        script_name,
        subdomain,
        environ["wsgi.url_scheme"],
        environ["REQUEST_METHOD"],
        path_info,
        query_args=query_args,
    )
  • Map.bind() 方法創建 MapAdapter的實例:
def bind(
    self,
    server_name,
    script_name=None,
    subdomain=None,
    url_scheme="http",
    default_method="GET",
    path_info=None,
    query_args=None,
):
   # 其他代碼略
   
    return MapAdapter(
        self,
        server_name,
        script_name,
        subdomain,
        url_scheme,
        path_info,
        default_method,
        query_args,
    )

MapAdapter.match() 方法

該方法的作用是,傳入 path_info 和 method,返回 tuple 類型包括 endpoint + arguments 的信息或者 rule + arguments 信息 (You get a tuple in the form (endpoint, arguments) if there is a match (unless return_rule is True, in which case you get a tuple in the form (rule, arguments)))。該方法內部調用 Rule.match() 方法:

def match(self, path_info=None, method=None, return_rule=False, query_args=None):
    
    self.map.update()
    if path_info is None:
        path_info = self.path_info
    else:
        path_info = to_unicode(path_info, self.map.charset)
    if query_args is None:
        query_args = self.query_args
    method = (method or self.default_method).upper()

    path = u"%s|%s" % (
        self.map.host_matching and self.server_name or self.subdomain,
        path_info and "/%s" % path_info.lstrip("/"),
    )

    have_match_for = set()
    for rule in self.map._rules:
        try:
        	#-----------------------------------
        	# 內部調用 Rule.match()方法
        	#-----------------------------------
            rv = rule.match(path, method)
        except RequestSlash:
            raise RequestRedirect(
                self.make_redirect_url(
                    url_quote(path_info, self.map.charset, safe="/:|+") + "/",
                    query_args,
                )
            )
        except RequestAliasRedirect as e:
            raise RequestRedirect(
                self.make_alias_redirect_url(
                    path, rule.endpoint, e.matched_values, method, query_args
                )
            )
        if rv is None:
            continue
        if rule.methods is not None and method not in rule.methods:
            have_match_for.update(rule.methods)
            continue

        if self.map.redirect_defaults:
            redirect_url = self.get_default_redirect(rule, method, rv, query_args)
            if redirect_url is not None:
                raise RequestRedirect(redirect_url)

        if rule.redirect_to is not None:
            if isinstance(rule.redirect_to, string_types):

                def _handle_match(match):
                    value = rv[match.group(1)]
                    return rule._converters[match.group(1)].to_url(value)

                redirect_url = _simple_rule_re.sub(_handle_match, rule.redirect_to)
            else:
                redirect_url = rule.redirect_to(self, **rv)
            raise RequestRedirect(
                str(
                    url_join(
                        "%s://%s%s%s"
                        % (
                            self.url_scheme or "http",
                            self.subdomain + "." if self.subdomain else "",
                            self.server_name,
                            self.script_name,
                        ),
                        redirect_url,
                    )
                )
            )
		# 如果設置 return_rule,返回 rule+arguments(tuple)
        if return_rule:
            return rule, rv
        # 否則返回 endpoint+arguments(tuple)
        else:
            return rule.endpoint, rv

    if have_match_for:
        raise MethodNotAllowed(valid_methods=list(have_match_for))
    raise NotFound()

比如在本例中, 請求的 path 爲 /message/13811112222,調用 match() 方法後,返回 {'mobile_number' : '13811112222'} (dict 類型)

轉換器與動態 url

Flask 中實現動態 url 的方法是通過 Map 類的轉換器 (converter)來實現,converter 中定義了 regular expression,在創建 Map 實例的時候,添加 Rule 的時候會有一系列方法調用:

  • Map 初始化方法調用 Map.add() 方法
  • Map.add() 方法針對每一個 Rule, 調用 Rule.bind() 方法
  • Rule.bind() 方法調用 Rule.compile() 方法
  • Rule.compile() 方法中,定義了一個內部方法 _build_regex() 將 URL 解析爲 converter, arguments 和 variable:


然後根據 Rule 中 converter 名稱調用對應的 converter,沒有指定 converter 名稱則默認爲 UnicodeConverter:

Rule 的格式是<converter(arguments):name> ,符合規則的格式才能被正確解析,werkzueg 實現了 7 中預定義的 converter,能滿足絕大部分需求。在 routing.py 中,我們可以看到如下的代碼:

DEFAULT_CONVERTERS = {
    "default": UnicodeConverter,
    "string": UnicodeConverter,
    "any": AnyConverter,
    "path": PathConverter,
    "int": IntegerConverter,
    "float": FloatConverter,
    "uuid": UUIDConverter,
}

class Map(object):
    default_converters = ImmutableDict(DEFAULT_CONVERTERS)
    # ...

7 種 converter,都直接或者間接繼承自 BaseConverterBaseConverter 類的代碼如下:

class BaseConverter(object):
    """Base class for all converters."""

    regex = "[^/]+"
    weight = 100

    def __init__(self, map):
        self.map = map

    def to_python(self, value):
        return value

    def to_url(self, value):
        if isinstance(value, (bytes, bytearray)):
            return _fast_url_quote(value)
        return _fast_url_quote(text_type(value).encode(self.map.charset))

每一種 converter 的 __init__() 方法確定了 converter 可以使用哪些 arguments。比如 UnicodeConverter 的 __init__() 方法是這樣的:

def __init__(self, map, minlength=1, maxlength=None, length=None):
	# 代碼略

所以我們在使用 UnicodeConverter 的時候能夠使用 lengthminlenghtmaxlenght 這些參數。比如下面的示例:

rules = [
    Rule('/message/<string(length=11):mobile_number>', endpoint='mobile')
]

BaseConveter 類的 to_python() 方法在 MapAdapter.match() 方法中匹配成功後被調用,將請求的 path 中動態 url 部分解析出 argument value, 傳遞給該方法的value 參數。在上面的示例中,13811112222 手機號碼被解析出來,傳遞給 to_python() 方法。

to_url() 方法在 url_for() 函數反向構建url 的時候,將 arguments 參數傳遞給該方法。後面結合具體的示例來幫助大家理解。

UnicodeConverter 是默認的轉換器,用於實現 string 類型的動態 url。

自定義轉換器

剛剛給出的例子沒有針對手機號碼的校驗規則,假設我們要對請求中傳遞的手機號碼進行校驗,可以用自定義轉換器來實現。如果只是想增加校驗規則,在轉換器的 __init__() 方法中改寫 BaseConverter 的 regex 屬性

自定義轉換器代碼如下:

# CustomConverter.py

from werkzeug.routing import BaseConverter

class MobileConverter(BaseConverter):
    def __init__(self, map):
        BaseConverter.__init__(self, map)
        self.regex = r'1[35678]\d{9}'

然後在創建 Map 的時候 converters 參數從自定義轉換器賦值:

from CustomConverter import MobileConverter

mobile_converter = [{'mobile', MobileConverter}]
rules = [
    Rule('/', endpoint='index'),
    Rule('/message/<mobile:mobile_number>', endpoint='mobile')
]

url_map = Map(rules, converters=mobile_converter)

to_python() 方法如何使用呢?假設爲了增加友好性,將返回到前端的手機號碼用 - 分割,比如 13811112222 顯示爲 138-1111-2222。根據剛纔的說明, to_python() 方法在匹配成功後將被調用,所以可以改寫 BaseConverter 的 to_python() 方法,編寫如下代碼:

from werkzeug.routing import BaseConverter

class MobileConverter(BaseConverter):
    def __init__(self, map):
        BaseConverter.__init__(self, map)
        self.regex = r'1[35678]\d{9}'

    def to_python(self, value):
        return '{}-{}-{}'.format(value[:3], value[3:7], value[7:12])

to_url() 方法在 url_for() 函數中被調用,url_for() 函數的 argument 參數被傳遞給該方法的 value 參數。下面的示例演示了 to_url() 的用法。

from werkzeug.routing import BaseConverter

class MobileConverter(BaseConverter):
    def __init__(self, map):
        BaseConverter.__init__(self, map)
        self.regex = r'1[35678]\d{9}'

    def to_python(self, value):
        return '{}-{}-{}'.format(value[:3], value[3:7], value[7:12])

    def to_url(self, value):
        print(value)
        return value
from flask import Flask, url_for
from CustomConverter import MobileConverter

app = Flask(__name__)
app.url_map.converters['mobile'] = MobileConverter

@app.route('/')
def index():
    return 'Index page'

@app.route('/message/<mobile:mobile_number>')
def send_message(mobile_number):
    print(url_for('send_message', mobile_number='13833334444'))
    return 'Message was sent to {}'.format(mobile_number)

if __name__ == '__main__':
    app.run()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章