【技术分享】Sanic+Amis:3天快速搭建一个web服务

前言

测试工作中,需要搭建一些生产力小工具来提高工作效率。

基于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 使用方法

  1. 建立数据库表,语句见git仓库中

  2. 修改config.ini mysql连接信息

  3. 运行服务 python main.py 

  4. 访问服务 http://127.0.0.1:9300/device/page/index.html

  • 4
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值