长话短说吧。在住的地方搭了一个小雅 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 地址啥的。
虽然写法上简便了一些,而且不用一个一个地维护路径和模板文件之间的关系,但是还是有许多不足之处。比如说:
- 参数只有一个 path,想要 FastAPI 的所有参数还得一个一个加上去;
- 没了,就想到这么多。
延申
为啥不以这种形式为目标来实现呢:
@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