前言
测试工作中,需要搭建一些生产力小工具来提高工作效率。
基于web的服务,无需终端可多人协作,易部署,成为第一选择。
问题:搭建web服务需要全栈知识,编写http服务和前端样式需要一定技术门槛。
本文提供一套简单易用的解决方案,以一个测试机借还管理平台为例,展示如何快速落地一个web工具。
1.需求
预期功能
- 设备列表页 --- 可查看设备信息,和设备维护(增删改)。
- 使用历史记录页 --- 查看设备的借用历史
- 借用页面 --- 给设备生成二维码地址,填写信息完成借用和交还
实现结果如下图,下面展示如何完成
2.设计
一个web服务,由(前端)(后端)(数据库)3部分组成,内部使用的工具,不考虑炫酷的页面,企业级的性能响应,效率优先,如何快速完成搭建和部署,考虑轻量+易用的框架。
1.前端(Amis) ------ (数据展示)
2.后端(Sanic) ------ (业务逻辑的处理)
3.数据库(peewee+DB) ------ (数据持久化)
3.实现
3.1 数据库
ORM(Object–relational mapping)
ORM特点
- ✅ 数据模型都在一个地方定义,更容易更新和维护,也利于重用代码。
- ✅ ORM 有现成的工具,很多功能都可以自动完成,比如数据消毒、预处理、事务等等。
- ✅ 它迫使你使用 MVC 架构,ORM 就是天然的 Model,最终使代码更清晰。
- ✅ 代码比较简单,代码量少,语义性好,容易理解。
- ❌ 对于复杂的查询,性能不如原生的 SQL,或者无法表达。
- ❌ ORM 抽象掉了数据库层,开发者无法了解底层的数据库操作,也无法定制一些特殊的 SQL。
为什么选择peewee,ORM框架对比
框架 | ✅优点 | ❌缺点 |
---|---|---|
SQLObject | 易于理解的模式 | 不支持数据库会话以隔离工作单元 |
Storm | 轻便的API | 强制程序员编写手动表创建DDL语句 |
Django的ORM | 易于使用,学习时间短;与Django紧密集成 | 不能很好地处理复杂的查询;强迫开发人员回到原始SQL |
⭐peewee | 易于使用,轻量级;支持多种数据库;多扩展 | 多对多查询的编写不直观 |
SQLAlchemy | 企业级API;使代码健壮且适应性强;设计灵活,轻松编写复杂查询 | 学习曲线漫长 |
PonyORM | 简化设置和使用 | 不是为了同时处理数十万或数百万条记录而设计 |
3.1.1示例代码
新建model.py文件,定义数据模型。
from peewee import DateTimeField, ForeignKeyField, MySQLDatabase, Model
from peewee import CharField, PrimaryKeyField, TextField, IntegerField,FloatField
db=MySQLDatabase('db', user='username', password='password', host='11.22.11.111', port=3306)
class History(Model):
id = PrimaryKeyField()
device_id = ForeignKeyField(Device, backref='history', lazy_load=False)
to = CharField()
from_ = CharField(column_name='from')
date = DateTimeField(default=datetime.datetime.now(), formats='%Y-%m-%d %H:%M')
class Meta:
database = db
table_name = 'tb_history'
class Device(Model):
....
操作数据
# 存储数据
History.create(device_id='123', from_='loda', to='baozi' ,date=date(1935, 3, 1))
# 查询数据
datas = History.select().where(History.to == 'baozi')
for h in datas:
print(h.from_, h.to)
# 修改数据
user = datas[0]
user.to='amani'
user.save()
3.2 http-server
Sanic 提供一种简单且快速,集创建和启动于一体的方法,来实现一个易于修改和拓展的 HTTP 服务。可认为是高性能的Flask
Sanic 特点
-
简单且快速(可看作基于协程的Flask)
-
生产环境就绪
-
高拓展性
-
支持 ASGI
-
简单直观的 API 设计
-
社区保障
quickstart
创建 server.py
from sanic import Sanic
from sanic.response import text
app = Sanic("My Hello, world app")
@app.get("/")
async def hello_world(request):
return text("Hello, world.")
运行
$ sanic server.app
$ curl http://127.0.0.1
StatusCode : 200
StatusDescription : OK
Content : hello world
RawContent : HTTP/1.1 200 OK
Connection: keep-alive
Keep-Alive: 5
Content-Length: 11
Content-Type: text/plain; charset=utf-8
hello world
Forms : {}
Headers : {[Connection, keep-alive], [Keep-Alive, 5], [Content-Length, 11], [Content-Type, text/plain; charset=utf-8]}
Images : {}
InputFields : {}
Links : {}
ParsedHtml : mshtml.HTMLDocumentClass
RawContentLength : 11
3.2.1 Sanic应用
实例:Sanic() 是最基础的组成部分
from sanic import Sanic
app = Sanic("My Hello, world app")
3.2.2 响应函数(handler)
- 功能:响应对指定端点的访问,即这承载业务逻辑的代码的地方
- 组成:响应函数需要至少一个 request 实例作为参数, 并返回一个 HTTPResponse.
- 可以是普通函数或者异步的函数
def i_am_a_handler(request):
return HTTPResponse()
async def i_am_ALSO_a_handler(request):
return HTTPResponse()
3.2.3 路由(routing)
通过url找到对应的handler处理请求
给app添加路由 app.add_route(handler, path, methods)
def i_am_a_handler(request):
return HTTPResponse()
app.add_route(i_am_a_handler, '/test', methods=["POST", "PUT"])
装饰器格式 @app.[method](path, methods=["GET", "PUT", ...])
@app.route("/stairway") # 默认method是get
...
@app.route('/test', methods=["POST", "PUT"]) # 支持post和put请求
...
@app.get("/to")
...
@app.delete("/heaven/<id>")
...
3.2.4 路由参数
Sanic 允许模式匹配,并从 URL 中提取值。然后将这些参数作为关键字参数传递到响应函数中。
@app.get("/tag/<tag>")
async def tag_handler(request, tag):
return text("Tag - {}".format(tag))
# 路由参数指定类型,它将在匹配时进行强制类型转换
@app.route("/path/to/<foo:int>")
async def handler(request, foo: int):
...
3.2.5 静态文件
第一个参数是静态文件所需要匹配的路由 第二个参数是渲染文件所在的文件(夹)路径
app.static("/static", "/path/to/directory")
app.static("/", "/path/to/index.html")
3.2.6 请求(request)
除了路由参数,一次请求的其他信息,可以从request中获得
访问请求参数
- request.args
- request.query_args
$ curl http://localhost:8000\?key1\=val1\&key2\=val2\&key1\=val3
>>> print(request.args)
{'key1': ['val1', 'val3'], 'key2': ['val2']}
>>> print(request.args.get("key1"))
val1
>>> print(request.args.getlist("key1"))
['val1', 'val3']
>>> print(request.query_args)
[('key1', 'val1'), ('key2', 'val2'), ('key1', 'val3')]
>>> print(request.query_string)
key1=val1&key2=val2&key1=val3
3.2.7 请求body
- request.raw
- request.json
- request.form
$ curl localhost:8000 -d '{"foo": "bar"}'
>>> print(request.body)
b'{"foo": "bar"}'
>>> print(request.json)
{'foo': 'bar'}
>>> print(request.form)
{'foo': ['bar']}
3.2.8 返回(response)
Sanic 内置了 9 种常用的返回类型
- response.text
- text("Hi 😎")
- response.html
- html('<!DOCTYPE html><html lang="en"><meta charset="UTF-8"><div>Hi 😎')
- response.json
- json({"foo": "bar"})
- response.file
- file("/path/to/whatever.png")
- response.streaming
- response.file_stream
- response.raw
- response.redirect
- response.empty
3.2.9 自定义状态码
@app.post("/")
async def create_new(request):
new_thing = await do_create(request)
return json({"created": True, "id": new_thing.thing_id}, status=201)
3.2.10 基于类的视图
为什么需要基于类的视图?
在日常的 API 设计过程中,将不同的响应函数通过不同的 HTTP 方法挂载到同一路由上是一种常用的设计模式。
@app.get("/foo")
async def foo_get(request):
...
@app.post("/foo")
async def foo_post(request):
...
@app.put("/foo")
async def foo_put(request):
...
@app.route("/bar", methods=["GET", "POST", "PATCH"])
async def bar(request):
if request.method == "GET":
...
elif request.method == "POST":
...
elif request.method == "PATCH":
...
MethodView实现
from sanic.views import HTTPMethodView
from sanic.response import text
class SimpleView(HTTPMethodView):
def get(self, request):
return text("I am get method")
# You can also use async syntax
async def post(self, request):
return text("I am post method")
def put(self, request):
return text("I am put method")
def patch(self, request):
return text("I am patch method")
def delete(self, request):
return text("I am delete method")
app.add_route(SimpleView.as_view(), "/")
3.2.11 使用sanic实现设备管理的增删改查接口
# 定义返回格式
def amis_response(data=''):
return response.json({
"status": 0,
"msg": "ok",
"data": data
})
class DeviceListView(HTTPMethodView):
# 设备列表接口
def get(self, request):
args = dict(request.query_args)
page = int(args.get('page', 1))
size = int(args.get('perPage', 10))
select = Device.select()
# 支持按照参数进行筛选
if args.get('os'):
select = select.where(Device.os == args['os'])
if args.get('version'):
select = select.where(Device.version == args['version'])
ret = select.paginate(page, size)
data = {
'items': [d.to_dict() for d in ret],
'total': select.count()
}
return amis_response(data)
# 新增设备接口
async def post(self, request):
Device.create(**request.json)
return amis_response()
class DeviceView(HTTPMethodView):
# 修改设备信息
async def put(self, request, id):
query = Device.update(**request.json).where(Device.id==id)
code = query.execute()
return amis_response()
# 设备流转 action 1:借 2:还
async def post(self, request, id: int):
action = request.args.get('action')
user = request.args.get('user')
device = Device.get(Device.id==id)
if action == '1':
from_ = device.user
to_ = user
elif action == '2':
from_ = user
to_ = Device.get(Device.id==id).owner
History.create(device_id=id, from_=from_, to=to_)
device.user = to_
device.save()
return amis_response()
# 查询设备详情
async def get(self, request, id: int):
dev = Device.get(Device.id==id)
return amis_response(dev.to_dict())
3.3 前端展示 Amis
amis 是一个baidu开源的低代码前端框架,它使用 JSON 配置来生成页面,可以减少页面开发工作量,极大提升效率
特点
- ✅ 不需要懂前端
- ✅ 完整的界面解决方案
- ✅ 大量内置组件
- ❌ 大量定制的UI
- ❌ 极为复杂的交互
简单的配置即可生成一个页面,例如设备历史页的代码
!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>设备管理</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<link rel="stylesheet" href="sdk\sdk.css" />
<style>
html,
body,
.app-wrapper {
position: relative;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="root" class="app-wrapper"></div>
<script src="sdk\sdk.js"></script>
<script type="text/javascript">
(function () {
let amis = amisRequire('amis/embed');
// 借用管理页面
let amisScoped = amis.embed('#root',
{
"type": "page",
"body": {
"type": "service",
"api": "${host}/device/api/history/${id}",
"body": [
{
"type": "panel",
"title": "设备使用记录",
"body": {
"type": "list",
"source": "$items",
"listItem": {
"body": [
{
"type": "hbox",
"columns": [
{
"label": "From",
"name": "from"
},
{
"label": "To",
"name": "to",
},
{
"label": "Date",
"name": "date",
}
]
}
]
}
}
}
]
}
}
);
})();
</script>
</body>
</html>
页面效果
4. 项目代码
代码地址:https://github.com/Be5yond/device_manage
4.1 使用方法
-
建立数据库表,语句见git仓库中
-
修改config.ini mysql连接信息
-
运行服务 python main.py