从零到一自建云表格系统
大部分业务系统、管理后台 70% 的功能是数据表格的增删改查。这些功能的实现,以复制粘贴修改字段为主。本文将尝试用一种更优雅的 “复制粘贴” 方式编写此类功能。如果你希望告别枯燥乏味的重复劳动,或者需要自建一套云表格系统,接下来的内容可以帮到你。
DEMO 演示
通过表格编辑器,在线设计制作表格类功能模块。适用于用户管理、供应商管理、订单管理等表格 CURD 操作的功能模块。
源码地址: https://github.com/YaoApp/demo-widget
**一键部署: **https://letsinfra.com/openapp/demo-widget
构建工具
YAO 开源应用引擎
YAO 是一款开源应用引擎,使用 Golang 编写,以一个命令行工具的形式存在, 下载即用。适合用于搭建业务系统、网站/APP API 接口、管理后台等应用程序。
YAO 采用 flow-based 的编程模式,通过编写 YAO DSL (JSON 格式逻辑描述) 或 JavaScript 处理器,实现各种功能。 YAO DSL 可以有多种编写方式:
- 纯手工编写
- 使用自动化脚本,根据上下文逻辑生成
- 使用可视化编辑器,通过“拖拉拽”制作
**GitHub 地址: ** https://github.com/yaoapp/yao
Github Stars: 4.3K 开源协议: Apache 2.0 官方文档: https://yaoapps.com/doc
YAO Widget
YAO 将通用功能模块抽象为 Widget,通过 YAO DSL 描述差异,快速复制各种功能。 在开发中,如果开发一个相似的功能模块,需要将已有的功能复制粘贴,批量替换差异内容。 YAO 提供一种新方式 ,使用 DSL 描述差异内容,快速“复制粘贴”一个功能模块,以此来提升开发效率。
**YAO 内建了一组 Widgets, **涵盖大部分应用程序开发常见功能。
内建 Widget | 功能 |
---|---|
model | 数据结构建模 |
flow | 数据逻辑编排 |
api | RESTFul API |
table | 表格 CURD |
chart | 数据图表 |
login | 用户登录 |
register | 用户注册 |
task | 并发任务 |
schedule | 计划任务 |
socket | Socket |
websocket | WebSocket |
Widget 支持开发者自定义,可基于自身业务逻辑特征,自定义 DSL, 快速复制各种通用功能。本文将通过自定义 Widget 的方式,实现一套云表格系统。
实战: 极简实用的云表格系统
Part I: 制作 dyform Widget
准备工作: 安装 YAO 命令
阅读以下内容,需要具备基础编程能力,熟悉 RESTFul API ,关系数据库,JavaScript 语言等编程常识。 参考文档 入门指南 , 在本地安装 YAO 命令 ( 版本: v0.10.1),并熟悉基本使用方法。
**在本地创建一个应用: **
# 创建一个项目文件夹 mkdir -p /your/project/root # 初始化项目 yao init # 创建数据表 & 初始化菜单 yao migrate && yao run flows.setmenu
第一步: 设计 Widge DSL 数据结构
根据业务逻辑特征,设计 DSL 结构,本例中,不同表格间的差异为 表格字段、搜索器以及表格名称。 **dyform Widget DSL 数据结构: **
{ "name": "TEST", "decription": "A TEST DYFORM", "columns": [ { "title": "First Name", "name": "name", "type": "input", "search": true, "props": { "placeholder": "Please input your first name" } }, { "title": "Amount", "name": "amount", "type": "input", "search": true, "props": { "placeholder": "Please input amount" } }, { "title": "Description", "name": "desc", "type": "textArea", "props": { "placeholder": "Please input Description" } } ] }
字段描述 DSL :
配置项 | 说明 |
---|---|
title | 在表格中显示的标题 |
name | 字段名称, 对应数据库字段名 |
type | 填写表单时,使用的组件 |
props | 组件属性 |
search | 该字段是否可以作为筛查项 |
创建 dyforms 文件,用于保存 DSL
cd /your/project/root mkdir dyforms
将上面 DSL 保存到 **dyforms/demo.form.json **文件,用于调试。
第二步: 创建 dyform Widget
创建 dyform widget:
cd /your/project/root mkdir -p widgets/dyform # 创建 widget 文件 touch widgets/dyform/widget.json # Widget 基本信息 touch widgets/dyform/compile.js # dyform DSL 编译器 touch widgets/dyform/process.js # dyform 处理器 touch widgets/dyform/export.js # dyform 导出内建 Widget 定义
提示: YAO 自定义 widget 放置在 widgets 目录,需要手动创建这个目录。
使用编辑器打开 ** dyform/widget.json** 填写 dyform widget 基本信息
{ "label": "Dynamic Form", "description": "A form widget. users can design forms online", "version": "0.1.0", "root": "dyforms", "extension": ".form.json", "modules": ["Models", "Tables"] }
**字段说明: **
配置项 | 说明 |
---|---|
label | Widget 名称,在可视化编辑器显示。 |
description | Widget 介绍, 在可视化编辑器显示。 |
version | 版本号 |
root | DSL 文件保存路径(相对于项目根目录) |
extension | DSL 文件扩展名 |
modules | 需要导出的模块。 在 export.js 中根据 DSL 描述,转换的 YAO 内建 widgets。 如 model, table 等。这些 widgets 与保存在项目目录中的 DSL 文件等效。 |
第三步: 编写 dyform 处理器
编写 **dyform/process.js **脚本,实现 **Model, Table **处理器,将自定义 DSL 转换为 YAO 数据模型 widget 和 表格 widget。
编写 Export 函数,声明要导出的处理器, 查看源码
function Export() { return { Model: "Model", Table: "Table" }; }
实现 Model & Table 处理器逻辑
处理器 | 说明 | 源码链接 |
---|---|---|
Model | 将 dyform DSL 转换为 model DSL, 等效于在 models 文件夹手动编写一个 xxx.mod.json DSL 文件 | 查看源码 |
Table | 将 dyform DSL 转换为 table DSL, 等效于在 tables 文件夹手动编写一个 xxx..json DSL 文件 | 查看源码 |
提示: 建议使用 yao run 命令调试处理器,参考源码注释文档。
第四步: 导出为 Model & Table widget
编写 dyform/export.js 脚本, 实现 Models, Tables 函数,导出为 YAO model & table widgets.
导出函数 | 说明 | 源码链接 |
---|---|---|
Models | 调用 Model 处理器,导出 Yao model widget, 数据结构为 {MODEL_NAME:String: Model_DSL:Object} | 查看源码 |
Tables | 调用 Table 处理器, 导出为 Yao table widget , 数据结构为: {TABLE_NAME:String: Table_DSL:Object} | 查看源码 |
效果预览
在 dyforms 文件夹下,添加 **xxx.form.json **文件, 编写 dyform DSL 描述文件,即可快速创建一组表格管理模块、表格 API、表格处理器和模型处理器。
使用 yao migrate 命令创建数据表结构,yao start 命令启动服务,登录管理界面,即可使用表格管理功能。 路由地址: http://127.0.0.1:5099/xiang/table/dyform.xxx
Part II: 将 dyform DSL 保存到数据库,实现动态读写
如果希望把表格制作功能开放给用户,且在通常情况下代码目录应设定为只读,这就无法动态创建 DSL 文件。对 dyform widget 稍加改造,将 dyform DSL 保存在数据库中即可。
第一步: 创建一个 template model, 存储 dyform DSL
如何创建使用数据模型 models/template.mod.json 和管理表格 tables/template.tab.json, 参考Model 文档 和 Table 文档 models/template.mod.json 如下
{ "name": "Template", "table": { "name": "template", "comment": "For dyform DSL source" }, "columns": [ { "label": "ID", "name": "id", "type": "ID", "comment": "ID" }, { "label": "Name", "name": "name", "type": "string", "index": true }, { "label": "DSL", "name": "dsl", "type": "text", "comment": "DSL" } ], "values": [], "option": { "timestamps": true, "soft_deletes": true } }
DSL 管理界面
第二步: 给 dyform Widget 添加 Save & Delete 处理器
编写 Export 函数,声明要导出的处理器, 查看源码
function Export() { return { Model: "Model", Table: "Table", Save: "Save", Delete: "Delete" }; }
实现 Save & Delete 处理器逻辑
处理器 | 说明 | 源码链接 |
---|---|---|
Save | 根据 dyform DSL 转换为 model DSL, 更新对应数据模型结构,同时重新加载对应实例。 | 查看源码 |
Delete | 删除数据模型对应数据表 | 查看源码 |
编辑 tables/template.tab.json,添加 hooks 脚本,在表格保存和删除时,更新/删除数据表,创建/删除菜单。
**修改 tables/template.tab.json 添加 Hook **查看源码** **
"hooks": { "after:save": "scripts.template.AfterSave", "after:delete": "scripts.template.AfterDelete" },
**添加 hooks 脚本 scripts/template.js **查看源码
Hook | 说明 |
---|---|
AfterSave | 根据 DSL ,更新 dyform 数据表,并添加菜单 |
Delete | 删除 dyform 数据表,并移除菜单 |
第三步: 给 dyform Widget 添加内容源
编辑 widgets/dyform/compile.js , 从数据库中读取 DSL
/** * Source * Where to get the source of DSL */ function Source() { var sources = {}; tpls = Process("models.template.Get", { select: ["id", "dsl"], limit: 1000 }); if (tpls.code && tpls.message) { log.Error("Load dyform sources: %s", tpls.message); return sources; } tpls.forEach((tpl) => { tpl = tpl || {}; try { instance = `instance_${tpl.id}`; dsl = JSON.parse(tpl.dsl); sources[instance] = dsl; } catch (e) { log.Error("Source %v DSL: %s", tpl.id, e.message); return; } }); return sources; }
**Compile.js 方法说明 **查看文档
Hook | 说明 |
---|---|
Source | 自定义 DSL 数据源读取逻辑 |
Compile | 根据上下文或环境信息,调整 DSL 结构或准备相关资源 |
OnLoad | 当 DSL 加载完成,调用此方法。 |
效果预览
登录管理后台,动态添加 dyform DSL ,即可动态创建删除对应表格。
Part III: 可视化表单编辑器
可以使用表单制作工具来生成 form DSL。有很多优秀的开源表单编辑器可以选择,比如 XXXX , 为了演示效果,XGEN 快速实现了一个 DEMO 级的编辑器组件,产品级的实现将在 XGEN-NEXT (正式版) 发布时提供。 源码地址: https://github.com/YaoApp/xgen/tree/main/src/cloud/components/form/FormPrinter
第一步: 适配编辑器组件输入输出
为 template 表格添加 Hook, 用来转换 DSL 结构。 编辑 tables/template.tab.json
文件、 scripts/template.js
文件
**添加 **before:save**
hook ** 将编辑器组件输出,转换为 form DSL
tables/template.tab.json
{ ... "hooks": { "before:save": "scripts.template.BeforeSave", "after:save": "scripts.template.AfterSave", "after:delete": "scripts.template.AfterDelete" } ... }
scripts/template.js
/** * BeforSave Hook: transform the form editor data to the DSL * @param {*} payload */ function BeforeSave(payload) { payload = payload || {}; columns = payload.dsl || []; payload["dsl"] = { columns: [], name: payload.name || "UNTITLE" }; columns.forEach((column) => { let type = column.id || ""; payload["dsl"].columns.push({ title: column.title || "UNTITLE", name: column.bind, type: type.toLowerCase(), props: column.props || {}, }); }); payload["dsl"] = JSON.stringify(payload["dsl"]); return [payload]; }
tips : 可是使用 yao run
命令调试例如:
yao run scripts.template.BeforeSave '::{"dsl":...}'
添加 **after:find**
hook 将 form DSL 转换为组件输入结构
tables/template.tab.json
{ ... "hooks": { "after:find": "scripts.template.AfterFind", "before:save": "scripts.template.BeforeSave", "after:save": "scripts.template.AfterSave", "after:delete": "scripts.template.AfterDelete" } ... }
scripts/template.js
/** * AfterFind Hook: transform the DSL format to the form editor needs * @param {*} id * @param {*} template */ function AfterFind(template, id) { let dsl = JSON.parse(template["dsl"]); let columns = dsl.columns || []; let types = { input: "Input", select: "Select" }; template["dsl"] = []; columns.forEach((column) => { // let props = column.props || {}; // let search = props.showSearch ? true : false; template["dsl"].push({ title: column.title, bind: column.name, id: types[column.type], props: column.props || {}, search: true, width: 6, chosen: false, selected: false, }); }); return template; }
第二步: 更新 DSL 绑定的组件
编辑 tables/template.tab.json
文件, 将 DSL 字段编辑组件设定为 FormPrinter。 源码地址: https://github.com/YaoApp/demo-widget/blob/main/tables/template.tab.json
{ .... "Form Editor": { "label": "Form Editor", "edit": { "type": "FormPrinter", "props": { "value": ":dsl" } } } ... }
效果预览
打开 templates 表格,点击编辑一个模板,即可使用可视化编辑器制作表单。
Part IV: 制作 C 端表单页面
对于活动报名、预约注册等 C 端页面,需要个性化设计来提升用户体验。对于此类场景,可以有针对性的设计一组模板,使用任意前端技术栈实现。这里提供一个 VUE3 实现的 DEMO。 **源码地址: **https://github.com/YaoApp/demo-widget-front
第一步: 添加一个 API,用来读取表格配置信息
添加一个 GET /page/form/setting/:name
API , 在 apis 文件夹下,添加一个 page.http.json 文件, 添加一个接口,第一个参数为 表格名称.
{ "name": "Table Setting", "version": "1.0.0", "description": "Table Setting", "group": "page", "guard": "-", "paths": [ { "path": "/form/setting/:name", "method": "GET", "guard": "-", "process": "xiang.table.Setting", "in": ["$param.name", ":query"], "out": { "status": 200, "type": "application/json" } } ] }
源码地址: https://github.com/YaoApp/demo-widget/blob/main/apis/page.http.json
第二步: 在 SETUP 时,请求配置接口,获取配置信息。
const url = `${window.location.protocol}//${window.location.host}/api/page/form/setting/${formName}`; const response = fetch(url); response .then((res) => { return res.json(); }) .then((data) => { formTitle.value = data.name; const fieldset = data?.edit?.layout?.fieldset || []; const columns = fieldset.length > 0 ? fieldset[0]?.columns : []; const mapping = data?.columns || {}; columns.forEach((col: any) => { let column = mapping[col.name] || false; if (column?.edit) { let name = column.edit.props?.value || ""; column.edit.label = col.name; column.edit.field = name.replace(":", ""); column.edit.type = components[column.edit.type]; formItems.value.push(column.edit); } }); loading.value = true; }) .catch((err) => { console.log("ERR", err); failure.value = err.message; });
源码地址: https://github.com/YaoApp/demo-widget-front/blob/main/vue3/src/App.vue
第三步: 动态渲染表单
使用 component
组件,根据配置渲染表单
<template> <div v-if="loading"> <div class="header">{{ formTitle }}</div> <form> <div class="form-item" v-for="item of formItems" :key="item.label"> <div class="label">{{ item.label }}</div> <component :is="item.type" :name="item.field"></component> </div> <div class="form-item" v-if="formItems"> <button class="button" type="button">Submit</button> </div> </form> </div> <div v-else-if="failure" class="failure">{{ failure }}</div> <div v-else> <div>Loading...</div> </div> </template>
源码地址: https://github.com/YaoApp/demo-widget-front/blob/main/vue3/src/App.vue
第四步: 将制品复制到 UI 目录
YAO 内建了一个 HTTP server,将制品复制到 ui 目录即可访问
npm run build cp -r dist ../demo-widget/ui/vue3
源码地址: https://github.com/YaoApp/demo-widget/tree/main/ui/vue3
效果预览
打开浏览器,输入 http://your-host:port/xiang/login/admin
进入后台,录入并保存一个表单。 默认用户名: xiang@iqka.com
默认密码: A123456p+
打开 http://your-host:port/vue3/?form=dyform.instance_1
即可访问自定义表单页面