docs/zh-cn/quickstart/view_and_interceptors.md
# 视图和拦截器
视图(View)是接受Web请求的入口。
我们在视图上绑定接口(Interface),在接口中处理客户端提交的提交。
## 基础视图 BaseView
[源码地址](https://github.com/fy0/slim/blob/master/slim/base/_view/base_view.py)
### 地址路由
我们以 slim cli 生成的项目为例,其中的 api.index 文件就是一个 BaseView
```python
from slim.base.view import BaseView
from slim.retcode import RETCODE
from app import app
@app.route.view('misc')
class MiscView(BaseView):
@app.route.interface('GET')
async def info(cls):
"""
提供给前端使用的后端配置信息
"""
cls.finish(RETCODE.SUCCESS, {
'retcode': RETCODE.to_dict(),
'retinfo': RETCODE.txt_cn,
})
@app.route.interface('POST')
async def hello(cls):
data = await cls.post_data()
cls.finish(RETCODE.SUCCESS, 'Hi, %s' % data.get('name', 'visitor'))
```
根据自动生成的接口文档可以知道,这里面定义了一个GET接口,一个POST接口,分别是:
```
GET /api/misc/info
POST /api/misc/hello
```
结合代码,能够看出URL的拼装规律,首先 `/api` 是恒定前缀(可以在app.py中统一修改,Application对象构造函数的mountpoint参数)。
其次 `@app.route.view('misc')` 提供了第二段地址,函数名 `info` 提供了第三段地址。
`@app.route.interface('GET')`则是将这个方法注册成为了 `GET` 接口。
我们看一下 `app.route.interface` 这个函数的定义:
```python
@staticmethod
def interface(method, url=None, *, summary=None, va_query=None, va_post=None, deprecated=False):
pass
```
`method` 很好理解,url一般会自动填充为函数名,也可以自定义。
`summary` 是对应生成出接口文档的summary
`va_query` 与 `va_post` 是 [Schematics](https://schematics.readthedocs.io/en/latest/index.html) Model,用于输入校验,分别对应 Query parameter 和 Post body,我们稍后在“表单验证”一节详述。
`deprecated` 用于标记接口是否弃用,标记后会在接口文档上有直接体现。
### Query parameter、Post body 以及其他
作为一个 Web 框架,接口用于处理请求,是其核心。
接口有两个限制:
* 必须定义为某一视图的 async 方法
* 必须调用一次 `cls.finish` 来确定返回内容,注意参数code,原则是成功为0,失败为非零。框架提供一组常用返回代码,为 `slim.retcode.RETCODE`,其中异常基本是小于0的,因此开发者自定义异常码最好设计为大于0
`BaseView.finish` 函数定义如下:
```python
def finish(cls, code: int, data=sentinel, msg=sentinel, *, headers=None):
"""
Set response as {'code': xxx, 'data': xxx}
:param code: Result code
:param data: Response data
:param msg: Message, optional
:param headers: Response header
:return:
"""
pass
```
提取 Web 请求中的常用输入数据请看示例:
```python
@app.route.view('misc')
class MiscView(BaseView):
@app.route.interface('POST')
async def hello(cls):
params = cls.params # 获取 parameters
data = await cls.post_data() # 获取 post 内容
client_ip = await cls.get_ip() # 获取 IP
headers = cls.headers # 获取请求头
role = cls.current_request_role # 当前请求的权限角色
cls.finish(RETCODE.SUCCESS, 'Hi, %s' % data.get('name', 'visitor'))
```
### 表单验证
刚才在讲注册接口的 app.route.interface 函数时,提到了 `va_query` 与 `va_post` 两个参数。
这两个参数分别用于指定 Query parameter 和 Post body 的校验器,这里我们借助 [Schematics](https://schematics.readthedocs.io/en/latest/index.html) 来进行表单验证。
这里还是拿 slim cli 创建的项目举个例子:
```python
from schematics import Model
from schematics.types import EmailType, StringType
from slim.base.types.doc import ValidatorDoc
class SignupDataModel(Model):
email = EmailType(min_length=3, max_length=30, required=True, metadata=ValidatorDoc('Email'))
username = StringType(min_length=2, max_length=30, metadata=ValidatorDoc('Username'))
password = StringType(required=True, min_length=6, max_length=64, metadata=ValidatorDoc('Password'))
nickname = StringType(min_length=2, max_length=10, metadata=ValidatorDoc('Nickname'))
@app.route.view('user')
class UserView(PeeweeView, UserMixin):
model = User
@app.route.interface('POST', summary='注册', va_post=SignupDataModel)
async def signup(cls):
"""
用户注册接口
User Signup Interface
"""
vpost: SignupDataModel = cls._.validated_post
u = User.new(vpost.username, vpost.password, email=vpost.email, nickname=vpost.nickname)
if not u:
cls.finish(RETCODE.FAILED, msg='注册失败!')
else:
t: UserToken = await cls.setup_user_token(u.id)
cls.finish(RETCODE.SUCCESS, {'id': u.id, 'username': u.username, 'access_token': t.get_token()})
```
这里我们定义了 `SignupDataModel` 并用于 /api/user/signup 接口的`va_post`参数
有校验器时,能执行到函数代码即代表校验已通过,使用 `cls._.validated_query` 和 `cls._.validated_post` 来获取通过对应的 Model 实例。
我们做个请求试一下:
```shell script
http post http://localhost:9618/api/user/signup email=test -v
POST /api/user/signup HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 17
Content-Type: application/json
Host: localhost:9618
User-Agent: HTTPie/1.0.2
{
"email": "test"
}
HTTP/1.1 200 OK
Content-Length: 159
Content-Type: application/json; charset=utf-8
Date: Wed, 26 Feb 2020 09:41:12 GMT
Server: Python/3.6 aiohttp/3.6.2
{
"code": -218,
"data": {
"email": [
"Not a well-formed email address."
],
"password": [
"This field is required."
]
},
"msg": "非法提交内容"
}
```
请注意这一行代码,这是比直接从 cls.post_data() 取值更优的方式:
```python
vpost: SignupDataModel = cls._.validated_post
```
举例来说
```python
class SigninDataModel(Model):
email = EmailType(min_length=3, max_length=30)
username = StringType(min_length=2, max_length=30)
password = StringType(required=True, min_length=6, max_length=64)
remember = BooleanType(default=True)
```
这里的 remember 参数在 post data 中的形态可能会是 '1' '0' 'true' 'false' 'True' 'False' 等等。
使用 `cls._.validated_post.remember` 则可以直接取得 bool 类型的值,非常方便。
验证器建议放在 api/validate 目录下。
对于 schematics,更详细的用法可以参考其官方文档:
> https://schematics.readthedocs.io/en/latest/basics/quickstart.html
## SQL视图 AbstractSQLView
[源码地址](https://github.com/fy0/slim/blob/master/slim/base/_view/abstract_sql_view.py)
这种视图来自于这样的设想:
> 开发者建立数据表,框架自动生成增删改查API
实际我们是见不到 AbstractSQLView 这个类的,我们会见到的是其子类 `PeeweeView`,能够以这样的形式将 peewee 的 Model 映射出增删改查系列API接口:
```python
@app.route.view('/topic')
class TopicView(PeeweeView):
model = Topic
```
得到以下接口:
```
[GET]/api/topic/get
[GET]/api/topic/list/{page}
[GET]/api/topic/list/{page}/{size}
[POST]/api/topic/set
[POST]/api/topic/new
[POST]/api/topic/delete
```
这些接口怎么用我们下一节再统一说,这里先讲一下`AbstractSQLView`的特殊之处。
看下定义:
```python
class AbstractSQLView(BaseView):
LIST_PAGE_SIZE = 20 # list 单次取出的默认大小,若为-1取出所有
LIST_PAGE_SIZE_CLIENT_LIMIT = None # None 为与LIST_PAGE_SIZE相同,-1 为无限
LIST_ACCEPT_SIZE_FROM_CLIENT = False # 是否允许客户端指定 page size
options_cls = SQLViewOptions
_sql_cls = AbstractSQLFunctions
is_base_class = True # skip cls_init check
table_name: str = None
primary_key: str = None
data_model: Type[schematics.Model] = None
foreign_keys: Dict[str, List[SQLForeignKey]] = {}
foreign_keys_table_alias: Dict[str, str] = {} # to hide real table name
```
我们主要关心头三项,用于调整分页大小,跟两个list接口相关。
用法示例:
```python
@app.route.view('/topic')
class TopicView(PeeweeView):
model = Topic
LIST_PAGE_SIZE = 50
```
自动生成的几个接口除了`list`和`bulk_insert`之外,默认情况下都只作用于单条数据。
`set` 和 `delete` 接口在 headers 中增加 bulk 参数,可以影响多条数据。
当 bulk 存在,例如为'true'的时候,接口会对可查询到的全部项起效。bulk还可以是大于零的整数,代表影响的数据项个数。
因为同一类行为的操作既有单条也有多条,因此针对行为,而不是接口设定了以下拦截器:
```python
async def before_query(cls, info: SQLQueryInfo):
"""
在发生查询时触发。
触发接口:get list set delete
:param info:
:return:
"""
pass
async def after_read(cls, records: List[DataRecord]):
"""
触发接口:get list new set
:param records:
:return:
"""
pass
async def before_insert(cls, values_lst: List[SQLValuesToWrite]):
"""
插入操作之前
触发接口:new
:param values_lst:
:return:
"""
pass
async def after_insert(cls, values_lst: List[SQLValuesToWrite], records: List[DataRecord]):
"""
插入操作之后
触发接口:new
:param values_lst:
:param records:
:return:
"""
pass
async def before_update(cls, values: SQLValuesToWrite, records: List[DataRecord]):
"""
触发接口:set
:param values:
:param records:
:return:
"""
pass
async def after_update(cls, values: SQLValuesToWrite, old_records: List[DataRecord],
new_records: List[DataRecord]):
"""
触发接口:set
:param values:
:param old_records:
:param new_records:
:return:
"""
async def before_delete(cls, records: List[DataRecord]):
"""
触发接口:delete
:param records:
:return:
"""
pass
async def after_delete(cls, deleted_records: List[DataRecord]):
"""
触发接口:delete
:param deleted_records:
:return:
"""
pass
```
不过,写起来仍然有些别扭,这是我目前能想到的最不差的API设计。
如果有更好的方案,请告诉我。