长话短说吧。在住的地方搭了一个小雅 Alist,打算平时看看电影追追老番啥的,但是追番没有保存记录功能怎么行?于是我打算自己写一个追番网站。这类快速开发的小网站用服务端渲染再合适不过了,再加上 Python 简直是如虎添翼——个屁,Python 的服务端渲染不能说没有吧,只是我都没学过。于是我打算自己撸一个简单的服务端渲染框架。

回到正题。看到标题应该就能得出结论——框架开发失败,或者说暂时搁置了,不然标题应该是“手撸一个服务端渲染”啥的。不过虽然开发没那么顺心,但还是有点收获的。

装饰器效果

装饰器(Decorator)本质是一个函数,接收另一个函数作为参数,返回包装这个函数的函数。详细的定义和使用网上一搜一大堆,在这里就不讲了,直接开始撸码。

优化前:

@app.get("/", response_class=HTMLResponse)
async def index01(hello: str = 'zheng'):
    return r('/view/index.html', {'hello': hello})

可以看到,因为这是服务端渲染,所以需要指定返回类型为 HTMLResponse,还有在 return 的时候需要手动调用我自己写的渲染函数,把模板文件和数据传进去。同时还得用脑子维护 get 请求的路径和模板文件之间的映射。这种写法没那么 pythonic。

优化后:

@template_get("/")
async def index(hello: str = 'zheng'):
    return {'hello': hello}

主要优化了请求路径映射模板文件和自动带上 HTMLResponse,让开发过程更加人性化。

装饰器解析

def template_get(path: str):
    def wrapper(func: Callable):
        @functools.wraps(func)
        async def _(*args, **kwargs):
            # 先获取返回值
            res = dict(await func(*args, **kwargs))
            # 处理路径
            file_path = path
            if file_path.endswith('/'):
                file_path += 'index.html'
            if not file_path.endswith('.html'):
                file_path += '.html'

            return r(file_path, res)

        # 先调用 app.get,指定响应类为 HTMLResponse
        # 然后把 app.get 的返回函数传入我们包装过的函数作为参数(手动调用装饰器)
        return __app.get(path, response_class=HTMLResponse)(_)

    return wrapper

在最里面的包装函数 _ 里,我们主要是处理函数的返回值,把函数的返回值变成 html 字符串(渲染后的页面),然后再 wrapper 中,我们把包装的函数手动调用 app.get 注册到路由表中。在 FastAPI 眼中,我们传进的函数和普通的函数一样,因为用 @functools.wraps(func) 装饰器把原函数的元信息覆盖到包装函数里了。

调用装饰器的部分看注释。

最后 return wrapper,因为这个装饰器是有参的,所以起接收一个函数作为参数的函数(包装函数)作用的是这个装饰器的返回值(被 python 调用之前先自己调用了一次,传了 path 参数)。

还可以拓展一下,把增强的方法抽出来作为一个 list 传进 template_get 中,这样能方便添加全局错误处理,认证鉴权,解析 ip 地址啥的。

虽然写法上简便了一些,而且不用一个一个地维护路径和模板文件之间的关系,但是还是有许多不足之处。比如说:

  1. 参数只有一个 path,想要 FastAPI 的所有参数还得一个一个加上去;
  2. 没了,就想到这么多。

延申

为啥不以这种形式为目标来实现呢:

@app.get('/', response_class=HTMLResponse)
@template_get('/')
async def index02(hello: str = 'zheng'):
    return {'hello', hello}

首先 path 得写 2 份,没那么 pythonic,其次每个有 @template_get 的地方都要加上 response_class=HTMLResponse,没那么 pythonic。只是这种方法实现起来没那么绕而已。

PYTHONIC!

2023-07-30