用Fastapi结合AMIS实现个后台管理框架 - fastapi-amis-webframe

·  阅读 5352
用Fastapi结合AMIS实现个后台管理框架 - fastapi-amis-webframe

为什么有这个想法

    之前一直用Flask自己进行全栈开发,后来发现了Fastapi异步框架,感觉性能上确实强了不少,就用来做后端接口服务了。用Flask的时候,页面主要是利用Jinja2模板引擎+BS样式+JQuery来实现,对于一个后端开发来说还是可以接受的,因为如果前后端分离的话还要去学习Vue/React等框架,而且一个人开发双端总觉得时比较耗时间的。

    用了Fastapi后也纵向尝试下新的方式来实现前端页面,自己做的一些小东西总是离不开后台管理,用Flask的时候主要用Flask-Admin来实现的,该库结合了Jinja2+BS模板,用起来感觉还是很顺畅的。不过用了Fastapi后发现没有类似的库,不过发现了一个咱们国人开发的Fastapi-admin库,不过这个库是前后端分离的,前端用的是Vue,所以我还是有点望而却步。

    这就让人很头疼了,直到我发现了AMIS前端低代码框架。简单研究了下,感觉应该可以将AMIS和Fastapi结合一下。

    我们先来了解下AMIS框架吧。 amis 是一个低代码前端框架,它使用 JSON 配置来生成页面,可以减少页面开发工作量,极大提升效率。

    为了实现用最简单方式来生成大部分页面,amis 的解决方案是基于JSON来配置,它的独特好处是:

  • 不需要懂前端:在百度内部,大部分 amis 用户之前从来没写过前端页面,也不会JavaScript,却能做出专业且复杂的后台界面,这是所有其他前端 UI 库都无法做到的;
  • 不受前端技术更新的影响:百度内部最老的 amis 页面是 4 年多前创建的,至今还在使用,而当年的Angular/Vue/React版本现在都废弃了,当年流行的Gulp也被Webpack取代了,如果这些页面不是用 amis,现在的维护成本会很高;
  • 享受 amis 的不断升级:amis 一直在提升细节交互体验,比如表格首行冻结、下拉框大数据下不卡顿等,之前的 JSON 配置完全不需要修改;
  • 可以完全使用可视化页面编辑器来制作页面:一般前端可视化编辑器只能用来做静态原型,而 amis 可视化编辑器做出的页面是可以直接上线的。

amis 有两种使用方法:

  • JS SDK,可以用在任意页面中
  • React,可以用在 React 项目中

看到有JS SDK让我很惊喜,这样的话我就可以把JS嵌入到项目中了。然后利用AMIS的可视化设计生成前端JSON,再放入到项目的页面模板里,这样就可以快速的实现页面展示了嘛。

看一下官方的示例:

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <title>amis demo</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.css" />
    <!-- 从 1.1.0 开始 sdk.css 将不支持 IE 11,如果要支持 IE11 请引用这个 css,并把前面那个删了 -->
    <!-- <link rel="stylesheet" href="sdk-ie11.css" /> -->
    <!-- 不过 amis 开发团队几乎没测试过 IE 11 下的效果,所以可能有细节功能用不了,如果发现请报 issue -->
    <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.js"></script>
    <script type="text/javascript">
      (function () {
        let amis = amisRequire('amis/embed');
        // 通过替换下面这个配置来生成不同页面
        let amisJSON = {
          type: 'page',
          title: '表单页面',
          body: {
            type: 'form',
            mode: 'horizontal',
            api: '/saveForm',
            controls: [
              {
                label: 'Name',
                type: 'text',
                name: 'name'
              },
              {
                label: 'Email',
                type: 'email',
                name: 'email'
              }
            ]
          }
        };
        let amisScoped = amis.embed('#root', amisJSON);
      })();
    </script>
  </body>
</html>
复制代码

所以非常简单,我们只需要替换JSON部分即可展现我们自己的页面了

       let amisJSON = {
          type: 'page',
          title: '表单页面',
          body: {
            type: 'form',
            mode: 'horizontal',
            api: '/saveForm',
            controls: [
              {
                label: 'Name',
                type: 'text',
                name: 'name'
              },
              {
                label: 'Email',
                type: 'email',
                name: 'email'
              }
            ]
          }
        };
复制代码

    下面我就讲解下具体的实现细节。

框架思路

    一个基于fastapi和amis框架的后台脚手架。 基于本脚手架可以方便快捷的开始你自己的后台管理服务的开发工作。(Python3.9)

  • fastapi : 服务支撑
  • fastapi-login : 实现登陆验证
  • loguru : 日志打印
  • jinja2 : 实现模板机制
  • amis-js-sdk : 实现前端页面
  • sqlite : 数据库

目录结构

很简单的目录结构,app下面两个目录和三个py文件。db目录为空目录,自动生成的sqlite数据库会放在该目录下。templates目录下面是三个主页面和amis的js-sdk目录,login.html是登录页,index.html是用户主页,admin.html是管理主页。app下的三个py脚本分别是main.py主逻辑也是启动入口文件,database.py是数据库连接及表结构定义,utils.py是工具方法。

重点逻辑讲解

    我们先从数据库及表结构说起。主要在database.py脚本中。

只有一个(users)用户表,表结构如下:

字段名字段类型说明
idint自增id主键
emailstr用户邮箱作为登录名
hashed_passwordstr密文用户密码
is_activebool启用状态

利用sqlalchemy库来操作数据库,定义表结构

metadata = sqlalchemy.MetaData()

users = sqlalchemy.Table(
    "users",
    metadata,
    sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
    sqlalchemy.Column("email", sqlalchemy.String),
    sqlalchemy.Column("hashed_password", sqlalchemy.String),
    sqlalchemy.Column("is_active", sqlalchemy.Boolean)
)
复制代码

然后自动创建数据库和表结构

engine = sqlalchemy.create_engine(
    DATABASE_URL, connect_args={"check_same_thread": False}
)

metadata.create_all(engine)
复制代码

最后是第一次初始化一条管理员账号(管理员没有给字段来区分,这个自行加上也可以,我是默认指定了一个账号为管理员账号)

async def init_db():
    await database.connect()
    query = users.select()
    user = await database.fetch_all(query)
    if len(user) == 0:
        query_insert = users.insert()
        values = {
            "email": "yk1001@163.com",
            "hashed_password": get_password_hash('ykbjfree'),
            "is_active": True
        }
        await database.execute(query=query_insert, values=values)
    await database.disconnect()


loop = asyncio.get_event_loop()
result = loop.run_until_complete(init_db())
loop.close()
复制代码

密码的加密逻辑我放倒了utils.py中,使用的是passlib.context

from passlib.context import CryptContext


pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


# 验证密码
def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

# 加密密码
def get_password_hash(password):
    return pwd_context.hash(password)
复制代码

然后我们来看看main.py主脚本

里面的/admin/开头的路由都是管理用户的路由,例如:

  • /admin/user/list - 获取所有用户列表
  • /admin/insert/user/ - 新增用户
  • /admin/delete/user/{user_id} - 删除用户
  • /admin/change/status/{user_id} - 修改用户状态
  • /admin/update/pwd/ - 修改用户密码
  • /admin - 显示管理页面

这个接口主要都是用户基本管理功能,在amis中进行调用,后续会提到。

其他的接口都是处理登陆验证相关的,例如:

  • /auth/token - 登陆验证
  • /current_user - 从cookie获取用户信息(登陆成功后)
  • /cookie/clear - 登出(清理cookie)
  • /login - 显示登陆页面
  • / - 显示用户主页面

还有几个方法,我也说明一下:

@manager.user_loader async def load_user(email: str)

这个是fastapi-login的获取用户信息的实现方法。

@app.on_event("startup") async def startup(): await database.connect()

@app.on_event("shutdown") async def shutdown(): await database.disconnect()

这两个方法是在应用启动和结束后的钩子函数,在里面来自动连接数据库和断开数据库的。

下面我们就来看下amis和jinja2是如何结合的。

前端页面部分都在templates目录下,sdk目录基本上不用看,我只在里面添加了一个vars.js文件,这个文件里面只有一行:

var baseUrl = 'http://localhost:8080'

主要是将html文件中的amis json定义页面时调用的服务器地址统一为这个全局变量了。

这个地方要特别注意一下,如果你不是本地服务调用的话,需要将baseUrl改为你的实际服务器地址,不然时调用不到后端服务的。

我们如何将amis的页面通过jinja2来调用的,看个例子吧。

@app.get("/login") async def login(request: Request): return templates.TemplateResponse("login.html", {"request": request})

这个是显示登陆页面,对应的html是templates/login.html,里面的页面json定义如下:

           {
                "type": "form",
                "title": "账号",
                "controls": [
                    {
                        "label": "邮箱地址",
                        "type": "text",
                        "name": "username",
                        "hint": "",
                        "addOn": null,
                        "required": true,
                        "validations": {
                            "isEmail": true
                        },
                        "validationErrors": {
                            "isEmail": "Email 格式不正确"
                        }
                    },
                    {
                        "type": "password",
                        "label": "密码",
                        "name": "password",
                        "required": true
                    }
                ],
                "submitText": "登陆",
                "affixFooter": false,
                "messages": {
                    "fetchFailed": "初始化失败",
                    "saveSuccess": "保存成功",
                    "saveFailed": "保存失败",
                    "validateFailed": ""
                },
                "initApi": "",
                "api": {
                    "method": "post",
                    "url": baseUrl + "/auth/token",
                    "dataType": "form"
                },
                "debug": false,
                "autoFocus": true,
                "redirect": baseUrl
            }
复制代码

可以看到里面的api字段就是页面调用后端接口的配置,url中我统一使用了vars.js里面的baseUrl变量,这下大家明白了吧。

然后为了html中的css和js文件可以正常在jinja2下访问到,还需要在fastapi中定义下静态文件目录。

app.mount("/sdk", StaticFiles(directory="templates/sdk"), name="static") templates = Jinja2Templates(directory="templates")

然后注意下html中需要将我们的vars.js也引用进去

这样基本上就通了。

最后就是用户登陆验证这块,这部分大家先看我的代码吧,细节比较多,后续如果大家有需要的话,我再详细讲解。目前是可以直接用的,所以大家也不用太去关心实现细节。

代码仓库

github.com/ykbj/fastap…

希望大家可以给我点点star哟。

分类:
开发工具
标签:
收藏成功!
已添加到「」, 点击更改