三步实现低代码框架
突然想到这么一个标题党的事情,试试看。注:仅基于PHP做简单梳理,未完成成品。
先想清楚核心原理,然后分别从后端、前端设计实现。
核心原理
低代码,如果简单理解为针对常规应用的CRUD场景,以一种DSL语言的形式,实现系统的开发。这种形式,减少了程序员的重复劳动,甚至可以让不太懂程序开发的人也能完成系统的开发-这也许正是“低”的含义。实则,我们知道多数人的认知是模型驱动的开发思想。
要达到后者的目的,首先得定义一套DSL语言,以便一般用户容易掌握,从而通过对CRUD这类功能所需信息的定义来实现系统的开发。
软件工程中,传统过程是要先设计,再实现;通过低代码的DSL语言,实际上将设计和实现两个动作合二为一了,设计即实现。
软件设计方法分结构化设计和面向对象设计,我们所熟知的画ER图就是结构化设计的经典步骤。数据实体、属性、关系,作为我们对现实世界事物静态状态下的认知的映射,是最基础的分析和设计工作。因此,我们的DSL语言中,首先要支持实体的定义。
这里,我们可以考虑使用PlantUML中的实体关系图语言作为我们的DSL语言,这样有一个很大的好处,实体图可以渲染成图形格式便于评审交流。
不过稍微有点尴尬的是,目前PlantUML还没有原生的PHP解析引擎,需要封装其官方的Jar包调用执行解析和转换,才能最终生成PHP版的实体类代码。https://github.com/mk-conn/plant2code
后端实现
路由
选一MVC框架,如ThinkPHP v6,基于其灵活的路由机制,实现按实体的CRUD界面和API请求入口调度。
在路由定义文件(config/route.php)中加上:
use think\facade\Route;
// 主界面
Route::get('lc/:entity$', 'EntityCRUD/index');
// API
Route::rule('lc-api/:entity/:action', 'EntityCRUD/:action');
CRUD
基于 think-orm 的特性,可以很便捷的实现相应功能。下面做个示范:
https://www.kancloud.cn/manual/think-orm/1257998
<?php
namespace app\controller;
use app\BaseController;
use think\facade\Db;
class EntityCRUD extends BaseController
{
/**
* 构造 CRUD UI
* @param $entity
*/
public function index($entity)
{
echo $entity . ' Entity/index';
}
/**
* 查询API
* @param $entity
*/
public function retrieve($entity)
{
echo $entity . ' Entity/retrieve';
}
/**
* 增加API
* @param $entity
*/
public function create($entity)
{
$data = input('post.');
$id = Db::name($entity)->insertGetId($data);
echo $entity . ' Entity/create:' . $id;
}
/**
* 修改API
* @param $entity
* @param $id
*/
public function update($entity, $id)
{
$data = input('post.');
Db::name($entity)->where('id' , $id)->update($data);
echo $entity . ' Entity/update ' . $id;
}
/**
* 删除API
* @param $entity
* @param $id
*/
public function delete($entity, $id)
{
Db::table($entity)->delete($id);
echo $entity . ' Entity/delete ' . $id;
}
}
实体定义
基于plantuml 的语法,如果数据库使用mongodb,则直接用plantuml的实体关系图特性即可完成实体类的完整定义,都不用考虑与数据库层字段类型的映射问题;如果基于传统的关系型数据库如mysql,则需要进一步实现字段类型的映射。
app/definition/entity/company.puml
@startuml
entity "Company" as e01 {
*id: int <<generated>>
*name: string
*phone1: string
--
phone2: string
fax: string
address1: string
address2: string
city: string
state: string
zip: string
primary_url: string
owner: int
*type: int
email: string
description: text
}
@enduml
或者基于plantuml也能处理的JSON或YAML格式:
@startyaml
name: account_receivable_invoice
label: 收票
icon: account
enable_api: true
enable_files: true
fields:
name:
label: 标题
type: text
required: true
bill_id:
label: 付款单ID
omit: true
hidden: true
type: text
amount:
label: 发票总金额
type: number
required: true
invoice_number:
label: 发票张数
type: number
required: true
payable_id:
label: 应付记录
type: lookup
reference_to: account_receivable
relatedList: true
required: true
contract_id:
label: 合同
type: master_detail
reference_to: contracts
required: true
owner:
label: 上传人
omit: false
readonly: true
type: lookup
reference_to: users
company_id:
omit: false
hidden: false
label: 我方单位
list_views:
all:
label: 所有
columns:
- name
- amount
- invoice_number
- owner
- created
permission_set:
user:
allowCreate: false
allowDelete: false
allowEdit: false
allowRead: true
modifyAllRecords: false
viewAllRecords: false
modifyCompanyRecords: false
viewCompanyRecords: true
admin:
allowCreate: true
allowDelete: true
allowEdit: true
allowRead: true
modifyAllRecords: true
viewAllRecords: true
@endyaml
注:取自 steedos 项目中代码,最终语法与前端框架统一或不统一均可,仅演示思路。
对应的SQL
CREATE TABLE `company` (
`id` INT(10) NOT NULL auto_increment,
`module` INT(10) NOT NULL default '0',
`name` varchar(100) default '',
`phone1` varchar(30) default '',
`phone2` varchar(30) default '',
`fax` varchar(30) default '',
`address1` varchar(50) default '',
`address2` varchar(50) default '',
`city` varchar(30) default '',
`state` varchar(30) default '',
`zip` varchar(11) default '',
`primary_url` varchar(255) default '',
`owner` int(11) NOT NULL default '0',
`description` text,
`type` int(3) NOT NULL DEFAULT '0',
`email` varchar(255),
`custom` LONGTEXT,
PRIMARY KEY (`id`),
KEY `idx_cpy1` (`owner`)
);
以前一直眼馋的是yii框架中的db相关工具,比较专业又易懂,如果能将其抽取出来独立使用就好了。
DB初始化
实现实体定义中字段类型与相应数据库引擎的字段映射关系解析和处理后,再借助于框架的数据库迁移工具,可以比较容易实现DB Shcema的初始化,包括Schema升级.
https://www.kancloud.cn/manual/thinkphp6_0/1118028
UI 渲染
选一前端低代码框架如百度的 amis,其中自带CRUD专用组件,可以很简单的方式实现。
https://baidu.gitee.io/amis/zh-CN/components/crud
按照其CRUD所需返回的相应格式在后端组装数据格式,返回传递给amis即可。
<!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" />
<?php
$base = request()->root();
$root = strpos($base, '.') ? ltrim(dirname($base), DIRECTORY_SEPARATOR) : $base;
if ('' != $root) {
$root = '/' . ltrim($root, '/');
}
?>
<link rel="stylesheet" href="<?php echo $root ?>/static/amis-sdk-v1.1.6/sdk.css" />
<link rel="stylesheet" href="<?php echo $root ?>/static/amis-sdk-v1.1.6/helper.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="<?php echo $root ?>/static/amis-sdk-v1.1.6/sdk.js"></script>
<script type="text/javascript">
(function () {
let amis = amisRequire('amis/embed');
// 通过替换下面这个配置来生成不同页面
let amisJSON = {
"type": "page",
"body": [
{
"label": "新增",
"type": "button",
"actionType": "dialog",
"level": "primary",
"className": "m-b-sm",
"dialog": {
"title": "新增表单",
"body": {
"type": "form",
"api": "post:https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/sample",
"controls": [
{
"type": "text",
"name": "engine",
"label": "Engine"
},
{
"type": "text",
"name": "browser",
"label": "Browser"
}
]
}
}
}, {
"type": "crud",
"api": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/sample",
"syncLocation": false,
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "engine",
"label": "Rendering engine"
},
{
"name": "browser",
"label": "Browser"
},
{
"name": "platform",
"label": "Platform(s)"
},
{
"name": "version",
"label": "Engine version"
},
{
"type": "operation",
"label": "操作",
"buttons": [
{
"label": "修改",
"type": "button",
"actionType": "drawer",
"drawer": {
"title": "修改表单",
"body": {
"type": "form",
"initApi": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/sample/${id}",
"api": "post:https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/sample/${id}",
"controls": [
{
"type": "text",
"name": "engine",
"label": "Engine"
},
{
"type": "text",
"name": "browser",
"label": "Browser"
}
]
}
}
},
{
"label": "删除",
"type": "button",
"actionType": "ajax",
"level": "danger",
"confirmText": "确认要删除?",
"api": "delete:https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/sample/${id}"
}
]
}
]
},
]
};
let amisScoped = amis.embed('#root', amisJSON);
})();
</script>
</body>
</html>
注:未完成动态拼接。
进阶
权限、安全、代码缓存、细节完善等。