Featured image of post lowcode 低代码前端框架 amis 调研

lowcode 低代码前端框架 amis 调研

背景什么是低代码开发?“所谓低代码开发,即无需编码或只需少量代码就可以快速生成应用程序。也就是说,企业的应用

背景

什么是低代码开发?

所谓低代码开发,即无需编码或只需少量代码就可以快速生成应用程序。也就是说,企业的应用开发通过“拖拉拽”的方式即可完成。

低代码平台有哪些?

我知道的知名的比如有:

  • 阿里的宜搭
  • 腾讯的微搭
  • 百度的爱速搭

当然对于企业版都是收费的,阿里的宜搭依托于钉钉生态发展的挺好,据消息称字节也正在内部灰度自己的低代码平台,依托于飞书也应该会有比较广泛的应用。

我对低代码怎么看?

最近低代码的概念很火,对于业内的人来说,可能感觉就像:技术还是那个技术,新瓶装旧酒,又换了个包装再卖一次一样。

一直以来我都认为低代码是个伪命题,因为它只能针对非定制化或者说非常标准化的功能才有意义,才能够发挥它的价值,但真正的企业内部的需求多数又是定制化非常高的业务,否则每个公司就不用自己招人建团队了,都是标准化的,全部包出去或者随便找几个人做就好了,何必花大钱在技术投入上呢。

直到现在我也是这样认为,可能是因为 lowcode 的概念被很多厂商和公司二次包装的不像样子,有点儿偏了,忽悠不懂行的,宣传的多了,懂行的有些反感,像我就是这样,所以忽视了它本身中立的价值立场。具体来说就是,事物的存在是有它的合理性,那么站在这个角度冷静地思考下,低代码结合我们真实的应用场景到底有没有价值呢?具体来讲是对于一个已有技术团队的公司有什么价值。

在企业内部所有的业务系统自然不在低代码的应用范围,而如果你的团队小,假设是个创业公司,没有那么多人的情况下,像 CRM、OA 这种需求,在像飞书、钉钉这种 IM 中有各种 ISV 提供各种应用,一般情况下也能轻松解决。

但随着团队规模越来越大,岗位分工越来越垂直和精细,就会有越来越多企业内不同领域的需求出现,需要系统来解决,比如:

  • 运维团队需要开发自己的运维系统
  • DBA 团队需要开发 SQL 上线审核系统
  • 业务团队需要自己的运营系统,甚至会分拆成不同的子系统
  • IT 需要系统维护公司的设备信息
  • ……

而以上所有这些需要的系统需求很大程度上是不能被现有的应用功能(飞书、钉钉)完全满足的,所以需要开发实现,但它们是有相同点的:

  • 这些系统多数都是类似 MIS 的信息管理系统
  • 功能上相似化程度高
  • 也都有一定的定制化功能场景
  • 实现上都需要前、后端开发,人员成本高

总结来说就是:在同质化功能基础上又有部分定制化需求的系统。

又因为是在企业内部,所以又有两个隐含的需求:

  • 成本要低
  • 要安全(私有化部署)

现在市面上的 lowcode/nocode 平台提供云端应用和私有化部署,但无论如何是收费的。对于企业来说,最好是有一个免费又能够部署在自己服务器上(安全)且用起来比较灵活能够满足绝大多数需求的一个平台。

有这样的东西吗?

没有

那么我们来拆解一下这个需求:

1 免费

花钱能解决的问题都不叫做问题,然而问题是老板不愿意花钱,另外,就算我们愿意掏钱也不一定能解决应用灵活开发的问题,使用人家的东西就要按照人家的逻辑玩儿,你不能既用着 word, 还要求它有个 wps 的新功能。所以免费是个刚需,当然我说的免费只是软件成本,不是说这事儿完全不需要花一分钱成本,私有化部署也要占用服务器资源的。

2 安全

跟第一点有联系,既然不花钱就肯定部署在自己这儿了,那当然安全了

3 灵活开发应用

我们再细拆解一下,现在的应用开发一般都是前后端分离的。

先说前端,作为后台的系统,至少需要前端页面来展示,那么就需要有前端开发来做,或者后端自己做。我们都知道团队的前端资源一般都很紧张,业务系统都开发不完,哪有时间帮兄弟团队搞东搞西的,很多情况下都是各团队自已 solo 全栈,虽然都是程序员,但毕竟术业有专攻,那开发出来的页面就五花八门了,且不说好不好看,这个事儿对于开发同学的成本就很高,而且各做各的,有很多重复开发。

再说后端,以 java 技术栈为例,后端这边已经有很多的框架和工具可以帮助我们快速的建立一个应用,比如 springboot, 也可以快速的完成 CRUD,比如 springboot+mybatisplus。对于一个熟手来说,写几个 CRUD 接口还是比较快的。另外,要说 lowcode, 一些自动生成代码的工具也可以帮我们在一定程序上实现后端 lowcode, 常见如各种 code generator

还有契约,前后端联调 API 最好有个工具,无论是 swagger, 还是 yapi 这种工具能够提高效率,最好是用 yapi 这种能够 mock 数据的,那么就可以在契约+mock 出的数据的基础上实现并行开发。

通过上面的分析,我得出的结论是:

  • 后端可以最大限度地灵活开发自定义功能和逻辑部分
  • 后端对于通用功能的开发成本也不高
  • 前端开发成本高,且复用率低
  • 前端学习成本不低,大家的水平参差不齐
  • 前端维护成本高,新技术和版本更新较快

看来问题主要集中在前端,如果有个工具能够拥有以下特点就好了

  • 不需要什么学习成本,最好都不用懂前端框架
  • 能够通过 UI 进行简单配置就可以组合出各种功能
  • 对后端的接口调用简单配置就能实现功能
  • 不用管各依赖的升级更新,维护成本低

那么有吗?有!

amis

amis 是什么?

amis 是一个百度开源的低代码前端框架,它使用 JSON 配置来生成页面,可以减少页面开发工作量,极大提升效率。

有时候其实只想做个普通的增删改查界面,用于信息管理,类似下面这种Image

但仔细观察会发现它有大量细节功能 比如:

  • 可以对数据做筛选
  • 有按钮可以刷新数据
  • 编辑单行数据
  • 批量修改和删除
  • 查询某列
  • 按某列排序
  • 隐藏某列
  • 开启整页内容拖拽排序
  • 表格有分页(页数还能同步到地址栏)
  • 有数据汇总
  • 支持导出 Excel
  • 表头有提示文字
  • 鼠标移动到「平台」那列的内容时还有放大镜符号,可以展开查看更多

全部实现这些需要大量的代码。

amis 的初衷是:对于大部分常用页面,应该使用最简单的方法来实现,甚至不需要学习前端框架和工具。

amis 的亮点

  • 提供完整的界面解决方案:其它 UI 框架必须使用 JavaScript 来组装业务逻辑,而 amis 只需 JSON 配置就能完成完整功能开发,包括数据获取、表单提交及验证等功能,做出来的页面不需要经过二次开发就能直接上线;
  • 大量内置组件(100+),一站式解决:其它 UI 框架大部分都只有最通用的组件,如果遇到一些稍微不常用的组件就得自己找第三方,而这些第三方组件往往在展现和交互上不一致,整合起来效果不好,而 amis 则内置大量组件,包括了富文本编辑器、代码编辑器、diff、条件组合、实时日志等业务组件,绝大部分中后台页面开发只需要了解 amis 就足够了;
  • 支持扩展:除了低代码模式,还可以通过 自定义组件 来扩充组件,实际上 amis 可以当成普通 UI 库来使用,实现 90% 低代码,10% 代码开发的混合模式,既提升了效率,又不失灵活性;
  • 容器支持无限级嵌套:可以通过嵌套来满足各种布局及展现需求;
  • 经历了长时间的实战考验:amis 在百度内部得到了广泛使用,在 5 年多的时间里创建了 3.8 万页面,从内容审核到机器管理,从数据分析到模型训练,amis 满足了各种各样的页面需求,最复杂的页面有超过 1 万行 JSON 配置。

上手 amis

从官网得知 amis 有两种使用方法

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

SDK 版本适合对前端或 React 不了解的开发者,它不依赖 npm 及 webpack,可以像 Vue/jQuery 那样外链代码就能使用。

我们选择的是更通用的 JS SDK

搭建 demo

首先创建一个工程目录,比如 amis-demo, 创建 css、js、public、index.html 等目录和文件

然后运行 npm i amis 下载 sdk, 在 node_modules\amis\sdk 目录里就能找到

项目目录结构大概是这样

Image

然后编辑 index.html

 1<!DOCTYPE html>
 2<html lang="zh">
 3  <head>
 4    <meta charset="UTF-8" />
 5    <title>amis demo</title>
 6    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 7    <meta
 8      name="viewport"
 9      content="width=device-width, initial-scale=1, maximum-scale=1"
10    />
11    <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
12    <link rel="stylesheet" href="sdk.css" />
13    <link rel="stylesheet" href="helper.css" />
14    <!--  1.1.0 开始 sdk.css 将不支持 IE 11如果要支持 IE11 请引用这个 css并把前面那个删了 -->
15    <!-- <link rel="stylesheet" href="sdk-ie11.css" /> -->
16    <!-- 不过 amis 开发团队几乎没测试过 IE 11 下的效果所以可能有细节功能用不了如果发现请报 issue -->
17    <style>
18      html,
19      body,
20      .app-wrapper {
21        position: relative;
22        width: 100%;
23        height: 100%;
24        margin: 0;
25        padding: 0;
26      }
27    </style>
28  </head>
29  <body>
30    <div id="root" class="app-wrapper"></div>
31    <script src="sdk.js"></script>
32    <script type="text/javascript">
33      (function () {
34        let amis = amisRequire('amis/embed');
35        // 通过替换下面这个配置来生成不同页面
36        let amisJSON = {
37          type: 'page',
38          title: '表单页面',
39          body: {
40            type: 'form',
41            mode: 'horizontal',
42            api: '/saveForm',
43            body: [
44              {
45                label: 'Name',
46                type: 'input-text',
47                name: 'name'
48              },
49              {
50                label: 'Email',
51                type: 'input-email',
52                name: 'email'
53              }
54            ]
55          }
56        };
57        let amisScoped = amis.embed('#root', amisJSON);
58      })();
59    </script>
60  </body>
61</html

以上是根据官方文档写的,运行会报错,要解决两个问题

1 引入的 js 和 css 路径要写成自己的,所以我从 node_modules 找到我需要的文件 copy 到自己对应的 css 和 js 目录,所以我的引用代码类似这样

1 <link rel="stylesheet" href="css/sdk.css" />
2    <link rel="stylesheet" href="css/cxd.css" />
3    <link rel="stylesheet" href="css/antd.css" />
4    <link rel="stylesheet" href="css/helper.css" />
5    <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
6    <script src="https://cdn.jsdelivr.net/npm/history/umd/history.js"></script>

2 会报 Cannot read property 'locale' of undefined

解决方法是

先给 amis.embed() 的第四个参数传入一个空对象,例如:

1 let amisScoped = amis.embed(
2            '#root', 
3            amisJSON,
4            {
5                // 这里是初始 props
6            },
7            {} // 空对象
8            
9        );

为了方便调试我用的 IDE 是 VSCode,安装了 vscode 插件 Live Server,然后右键点击 Open with Live Server 即可在浏览器实时预览页面

接下来就可以根据文档的描述一个个的边写代码边调试体验所有的功能了。

接着你就会发现,原来主要需要开发的是 json, 要是能自动生成 json 就好了。

这个可以有~

不用自己编辑 json,通过编辑器直接拖拽组件自动生成,目前 amis-editor 未开源,但可以免费使用(包括商用)

  • 地址:https://aisuda.github.io/amis-editor-demo/#
  • github 地址:https://github.com/aisuda/amis-editor-demo

为了让页面有个框架(菜单),从 https://github.com/aisuda/amis-admin 项目参考(抄)了部分代码,让页面有个一个基本的架构,大概是这样:

Image

我们将每个子页面的具体 json 放到了pages目录,所以子页面 json 编辑就从pages里找到对应的 json 文件就可以了。

比如,这个demo.json

  1{
  2    "type": "page",
  3    "title": "demo 示例页面",
  4    "body": [
  5      {
  6        "type": "tpl",
  7        "tpl": "这是你刚刚新增的页面。",
  8        "inline": false
  9      },
 10      {
 11        "type": "chart",
 12        "config": {
 13          "xAxis": {
 14            "type": "category",
 15            "data": [
 16              "Mon",
 17              "Tue",
 18              "Wed",
 19              "Thu",
 20              "Fri",
 21              "Sat",
 22              "Sun"
 23            ]
 24          },
 25          "yAxis": {
 26            "type": "value"
 27          },
 28          "series": [
 29            {
 30              "data": [
 31                820,
 32                932,
 33                901,
 34                934,
 35                1290,
 36                1330,
 37                1320
 38              ],
 39              "type": "line"
 40            }
 41          ]
 42        },
 43        "replaceChartOption": true,
 44        "api": ""
 45      },
 46      {
 47        "type": "tabs",
 48        "tabs": [
 49          {
 50            "title": "tab1",
 51            "body": [
 52              {
 53                "type": "tpl",
 54                "tpl": "第一个选项卡",
 55                "inline": false
 56              },
 57              {
 58                "type": "list-select",
 59                "label": "列表",
 60                "name": "list",
 61                "options": [
 62                  {
 63                    "label": "选项 A",
 64                    "value": "A"
 65                  },
 66                  {
 67                    "label": "选项 B",
 68                    "value": 0
 69                  },
 70                  {
 71                    "label": "options3",
 72                    "value": true
 73                  }
 74                ]
 75              }
 76            ]
 77          },
 78          {
 79            "title": "tab2",
 80            "body": [
 81              {
 82                "type": "tpl",
 83                "tpl": "这是必有项",
 84                "inline": false
 85              },
 86              {
 87                "type": "input-text",
 88                "label": "文本",
 89                "name": "text"
 90              }
 91            ]
 92          }
 93        ],
 94        "tabsMode": "chrome"
 95      },
 96      {
 97        "type": "matrix-checkboxes",
 98        "name": "matrix",
 99        "label": "矩阵开关",
100        "rowLabel": "行标题说明",
101        "columns": [
102          {
103            "label": "列 1"
104          },
105          {
106            "label": "列 2"
107          }
108        ],
109        "rows": [
110          {
111            "label": "行 1"
112          },
113          {
114            "label": "行 2"
115          }
116        ]
117      },
118      {
119        "type": "steps",
120        "value": "3",
121        "steps": [
122          {
123            "title": "第一步",
124            "subTitle": "副标题",
125            "description": "描述"
126          },
127          {
128            "title": "第二步"
129          },
130          {
131            "title": "第三步"
132          },
133          {
134            "type": "wrapper",
135            "body": "子节点内容",
136            "title": "第四步"
137          }
138        ],
139        "status": "wait",
140        "source": ""
141      },
142      {
143        "type": "crud",
144        "api": {
145          "method": "get",
146          "url": "https://yapi.gaolvzongheng.com/mock/387/list",
147          "replaceData": false,
148          "responseData": null,
149          "dataType": "json",
150          "responseType": "blob",
151          "data": null
152        },
153        "bulkActions": [
154          {
155            "type": "button",
156            "level": "danger",
157            "label": "批量删除",
158            "actionType": "ajax",
159            "confirmText": "确定要删除?",
160            "api": "get:/xxx/batch-delete"
161          },
162          {
163            "type": "button",
164            "level": "danger",
165            "label": "批量编辑",
166            "actionType": "dialog",
167            "dialog": {
168              "title": "批量编辑",
169              "size": "md",
170              "body": {
171                "type": "form",
172                "api": "/xxx/bacth-edit",
173                "body": [
174                  {
175                    "label": "字段 1",
176                    "text": "字段 1",
177                    "type": "input-text"
178                  }
179                ]
180              }
181            }
182          }
183        ],
184        "itemActions": [
185          {
186            "label": "按钮",
187            "type": "button",
188            "hiddenOnHover": true
189          }
190        ],
191        "features": [
192          "create",
193          "filter",
194          "bulkDelete",
195          "bulkUpdate",
196          "update",
197          "view",
198          "delete"
199        ],
200        "headerToolbar": [
201          {
202            "label": "新增",
203            "type": "button",
204            "actionType": "dialog",
205            "dialog": {
206              "title": "新增",
207              "body": {
208                "type": "form",
209                "api": "xxx/create",
210                "body": [
211                  {
212                    "label": "字段 1",
213                    "text": "字段 1",
214                    "type": "input-text"
215                  }
216                ]
217              }
218            }
219          },
220          {
221            "type": "bulk-actions"
222          },
223          {
224            "type": "pagination"
225          },
226          {
227            "type": "statistics",
228            "tpl": "内容"
229          },
230          {
231            "type": "load-more",
232            "tpl": "内容"
233          },
234          {
235            "type": "export-excel",
236            "tpl": "内容"
237          }
238        ],
239        "filter": {
240          "title": "查询条件",
241          "body": [
242            {
243              "type": "input-text",
244              "name": "keywords",
245              "label": "关键字"
246            }
247          ],
248          "autoFocus": true
249        },
250        "perPageAvailable": [
251          1
252        ],
253        "messages": {},
254        "keepItemSelectionOnPageChange": true,
255        "primaryField": "id",
256        "labelTpl": "${name}",
257        "draggable": true,
258        "footable": {
259          "expand": "all"
260        },
261        "title": "demo 表格",
262        "mode": "table",
263        "columns": [
264          {
265            "name": "id",
266            "label": "ID",
267            "type": "text",
268            "placeholder": "-",
269            "sortable": true,
270            "fixed": ""
271          },
272          {
273            "name": "name",
274            "label": "name",
275            "type": "text",
276            "placeholder": "-"
277          },
278          {
279            "type": "operation",
280            "label": "操作",
281            "buttons": [
282              {
283                "label": "编辑",
284                "type": "button",
285                "actionType": "dialog",
286                "level": "link",
287                "dialog": {
288                  "title": "编辑",
289                  "body": {
290                    "type": "form",
291                    "api": "xxx/update",
292                    "body": [
293                      {
294                        "name": "id",
295                        "label": "ID",
296                        "type": "input-text"
297                      },
298                      {
299                        "name": "engine",
300                        "label": "渲染引擎",
301                        "type": "input-text"
302                      }
303                    ]
304                  }
305                }
306              },
307              {
308                "label": "查看",
309                "type": "button",
310                "actionType": "dialog",
311                "level": "link",
312                "dialog": {
313                  "title": "查看详情",
314                  "body": {
315                    "type": "form",
316                    "body": [
317                      {
318                        "name": "id",
319                        "label": "ID",
320                        "type": "input-text"
321                      },
322                      {
323                        "name": "engine",
324                        "label": "渲染引擎",
325                        "type": "input-text"
326                      }
327                    ]
328                  }
329                }
330              },
331              {
332                "type": "button",
333                "label": "删除",
334                "actionType": "ajax",
335                "level": "link",
336                "className": "text-danger",
337                "confirmText": "确定要删除?",
338                "api": "/xxx/delete"
339              }
340            ]
341          }
342        ],
343        "footerToolbar": [
344          {
345            "type": "load-more"
346          },
347          {
348            "type": "pagination"
349          }
350        ],
351        "alwaysShowPagination": true,
352        "filterTogglable": false,
353        "perPage": 6,
354        "checkOnItemClick": true,
355        "initFetch": true,
356        "quickSaveApi": ""
357      }
358    ],
359    "messages": {},
360    "name": "demo",
361    "subTitle": "这是副标题",
362    "remark": "这是一个提示",
363    "aside": []
364  }

看起来很长对吧,但都是用 editor 自动生成的

Image

最终 html 页面是这样,一模一样

Image

可以看到页面上有个表格,那么数据是从哪儿来的呢?我是利用 yapi 先定义好接口契约,然后通过 yapi mock 出来的,当然 mock 规则你可以自定义

Image

接下来我只需要把后端接口真正实现(java、python、go 随你),然后把 API 地址重新配置好就可以了,一个复杂的功能前端通过 editor,后端自己实现,中间契约用 yapi,实现起来是非常快的。(后端自己 solo 吧 😁 )

剩下的就是还有很多 editor 的细节,以及 amis 的细节可能要通过文档+实践总结+案例来摸索了,但整体看学习成本并不高。对开发非常友好。

参考

位旅人路过 次翻阅 初次见面