# 路由(Routing)

到目前为止,我们已经接触了各式各样的装饰器,但是这些装饰器是干什么用的?我们该如何使用它?


@app.route("/stairway")
...
@app.get("/to")
...
@app.post("/heaven")
...

# 添加路由(Adding a route)

将响应函数进行挂载的最基本方式就是使用 app.add_route(),具体的细节请查看 API 文档 (opens new window)

async def handler(request):
    return text("OK")
app.add_route(handler, "/test")

默认的情况下,路由会绑定监听 HTTP GET 请求方式, 您可以通过修改 methods 参数,从而达到使用一个响应函数响应 HTTP 的多种请求方式。

app.add_route(
    handler,
    '/test',
    methods=["POST", "PUT"],
)

您也可以使用装饰器来进行路由绑定,下面是使用装饰器的方式进行路由绑定的例子,实现的效果和上一个例子相同。

@app.route('/test', methods=["POST", "PUT"])
async def handler(request):
    return text('OK')

# HTTP 方法(HTTP methods)

每一个标准的 HTTP 请求方式都对应封装了一个简单易用的装饰器:

    注意

    默认情况下,Sanic 将 在不安全的 HTTP 方法(POSTPUTPATCH) 上使用传入的请求体。如果你想以任何其他方法中接收 HTTP 请求中的数据,您需要从以下两种方法中任选其一:

    方法 #1 - 通过 ignore_body 告诉 Sanic 不要忽略请求体

    @app.delete("/path", ignore_body=False)
    async def handler(_):
        ...
    

    方法 #2 - 通过 receive_body 在请求中手动使用

    @app.delete("/path")
    async def handler(request: Request):
        await request.receive_body()
    

    # 路由参数(Path parameters)

    Sanic 允许模式匹配,并从 URL 中提取值。然后将这些参数作为关键字参数传递到响应函数中。

    @app.get("/tag/<tag>")
    async def tag_handler(request, tag):
        return text("Tag - {}".format(tag))
    

    您可以为路由参数指定类型,它将在匹配时进行强制类型转换。

    @app.get("/foo/<foo_id:uuid>")
    async def uuid_handler(request, foo_id: UUID):
        return text("UUID - {}".format(foo_id))
    

    # 匹配类型(Supported types)

      # 正则匹配(Regex Matching)

      更多时候,相对于复杂的路由,以上示例还是过于简单了,由我们使用了和以前完全不同的路由匹配模式,所以在这里我们要详细的说明一下正则的进阶用法。

      有时,您希望匹配路由中的某一部分:

      /image/123456789.jpg
      

      如果您想匹配文件模式,但只捕获数字部分,您需要做一些正则表达式的适配, 来体会编写正则表达式的乐趣 😄 :

      app.route(r"/image/<img_id:(?P<img_id>\d+)\.jpg>")
      

      更进一步,下面的这些匹配方式都是支持的:

      @app.get(r"/<foo:[a-z]{3}.txt>")                # 全模式匹配
      @app.get(r"/<foo:([a-z]{3}).txt>")              # 定义单个匹配组
      @app.get(r"/<foo:(?P<foo>[a-z]{3}).txt>")       # 定义单个命名匹配组
      @app.get(r"/<foo:(?P<foo>[a-z]{3}).(?:txt)>")   # 用一个或多个不匹配组定义单个命名匹配组
      

      值得注意的是,如果您使用了命名的匹配组,它的名称必须与 label 相同

      @app.get(r"/<foo:(?P<foo>\d+).jpg>")  # 正确示例
      @app.get(r"/<foo:(?P<bar>\d+).jpg>")  # 错误示例
      

      更多的用方法请参考:正则表达式操作 (opens new window)

      # 动态访问(Generating a URL)

      Sanic 提供了一种基于处理程序方法名生成 url 的方法:app.url_for(),您只需要函数名称即可实现响应函数之间的处理权力的移交。在您不希望将 url 进行硬编码或希望响应函数之间具有层级关系的时候,这将非常有用。它的使用方法如下:

      @app.route('/')
      async def index(request):
          # generate a URL for the endpoint `post_handler`
          url = app.url_for('post_handler', post_id=5)
          # Redirect to `/posts/5`
          return redirect(url)
      @app.route('/posts/<post_id>')
      async def post_handler(request, post_id):
          ...
      

      您可以传递任意数量的关键字参数,任何非路由参数的部分都会被是做为查询字符串的一部分

      >> > app.url_for(
          "post_handler",
          post_id=5,
          arg_one="one",
          arg_two="two",
      )
      '/posts/5?arg_one=one&arg_two=two'
      

      该方法同样支持为一个键名传递多个值。

      >> > app.url_for(
          "post_handler",
          post_id=5,
          arg_one=["one", "two"],
      )
      '/posts/5?arg_one=one&arg_one=two'
      

      # 特殊关键字参数(Special keyword arguments)

      您可以在 API Docs (opens new window) 查看更多详细信息。

      >> > app.url_for("post_handler", post_id=5, arg_one="one", _anchor="anchor")
      '/posts/5?arg_one=one#anchor'
      # _external requires you to pass an argument _server or set SERVER_NAME in app.config if not url will be same as no _external
      >> > app.url_for("post_handler", post_id=5, arg_one="one", _external=True)
      '//server/posts/5?arg_one=one'
      # when specifying _scheme, _external must be True
      >> > app.url_for("post_handler", post_id=5, arg_one="one", _scheme="http", _external=True)
      'http://server/posts/5?arg_one=one'
      # you can pass all special arguments at once
      >> > app.url_for("post_handler", post_id=5, arg_one=["one", "two"], arg_two=2, _anchor="anchor", _scheme="http",
                       _external=True, _server="another_server:8888")
      'http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor'
      

      # 自定义路由名称(Customizing a route name)

      在注册路由的时候,可以通过给定 name 参数来自定义路由名称

      @app.get("/get", name="get_handler")
      def handler(request):
          return text("OK")
      

      现在,您可以通过自定义的名称进行路由匹配。

      >> > app.url_for("get_handler", foo="bar")
      '/get?foo=bar'
      

      # Websocket 路径(Websockets routes)

      Websocket 的工作方式和 HTTP 是类似的。

      async def handler(request, ws):
          messgage = "Start"
          while True:
              await ws.send(message)
              message = ws.recv()
      app.add_websocket_route(handler, "/test")
      

      它也具备有一个独立的装饰器。

      @app.websocket("/test")
      async def handler(request, ws):
          messgage = "Start"
          while True:
              await ws.send(message)
              message = ws.recv()
      

      具体的工作原理,我们会在之后的 websocket 进行更多描述。

      # 严格匹配分隔符(Strict slashes)

      Sanic 可以按需开启或关闭路由的严格匹配模式,开启后路由将会严格按照 / 作为分隔来进行路由匹配,您可以在以下几种方法中进行匹配,它们的优先级遵循:

      1. 路由(Route)
      2. 蓝图(Blueprint)
      3. 蓝图组(BlueprintGroup)
      4. 应用(Application)
      # 为应用程序下所有的路由都启用严格匹配模式
      app = Sanic(__file__, strict_slashes=True)
      
      # 为指定的路由启用严格匹配模式
      @app.get("/get", strict_slashes=False)
      def handler(request):
          return text("OK")
      
      # 为蓝图所属的路由启用严格匹配模式
      bp = Blueprint(__file__, strict_slashes=True)
      @bp.get("/bp/get", strict_slashes=False)
      def handler(request):
          return text("OK")
      
      bp1 = Blueprint(name="bp1", url_prefix="/bp1")
      bp2 = Blueprint(
          name="bp1",
          url_prefix="/bp2",
          strict_slashes=False,
      )
      # This will enforce strict slashes check on the routes
      # under bp1 but ignore bp2 as that has an explicitly
      # set the strict slashes check to false
      group = Blueprint.group([bp1, bp2], strict_slashes=True)
      

      # 静态文件(Static files)

      为了确保 Sanic 可以正确代理静态文件,请使用 app.static() 方法进行路由分配。

      在这里,参数的顺序十分重要

      第一个参数是静态文件所需要匹配的路由

      第二个参数是渲染文件所在的文件(夹)路径

      更多详细用法请参考 API docs

      app.static("/static", "/path/to/directory")
      

      您也可以提供单独的文件

      app.static("/", "/path/to/index.html")
      

      它同样支持自定义名称,来帮助您实现快速访问

      app.static(
          "/user/uploads",
          "/path/to/uploads",
          name="uploads",
      )
      

      检索 URL 的流程和响应函数类似,但是当您需要特定的文件的时候,可以通过添加 filename 参数来达到效果。

      >> > app.url_for(
          "static",
          name="static",
          filename="file.txt",
      )
      '/static/file.txt'
      ​```python
      >> > app.url_for(
          "static",
          name="uploads",
          filename="image.png",
      )
      '/user/uploads/image.png'
      

      提示

      如果您想要设置多个静态文件路由,我们强烈建议您手动为 static() 加上 name 参数。可以确定的是,这样做可以减少一些潜在且隐蔽的 bug。

      app.static("/user/uploads", "/path/to/uploads", name="uploads")
      app.static("/user/profile", "/path/to/profile", name="profile_pics")
      

      # 路由上下文(Route context)

      定义路由时,您可以添加任意数量的带有 ctx_ 前缀的关键字参数。这些值将被注入到路由的 ctx 对象中。

      @app.get("/1", ctx_label="something")
      async def handler1(request):
          ...
      @app.get("/2", ctx_label="something")
      async def handler2(request):
          ...
      @app.get("/99")
      async def handler99(request):
          ...
      @app.on_request
      async def do_something(request):
          if request.route.ctx.label == "something":
              ...
      
      MIT Licensed
      Copyright © 2018-present Sanic Community Organization

      ~ Made with ❤️ and ☕️ ~