前后端分離了诉探!
第一次知道這個事情的時候,內心是困惑的棍厌。
前端都出去搞 SPA肾胯,SEO 們同意嗎竖席?
后來,SSR 來了敬肚。
他說:“SEO 們同意了毕荐!”
任何人的反對,都沒用了艳馒,時代變了憎亚。
各種各樣的 SPA 們都來了,還有穿著跟 SPA 們一樣衣服的各種小程序們弄慰。
為他們做點什么吧第美?于是 rxModels 誕生了,作為一個不希望被拋棄的后端陆爽,它希望能以更便捷的方式服務前端什往。
順便把如何設計制作也分享出來吧,說不定會有一些借鑒意義慌闭。即便有不合理的地方别威,也會有人友善的指出來。
保持開放驴剔,付出與接受會同時發(fā)生兔港,是雙向受益的一個過程。
rxModels 是什么衫樊?
一個款開源、通用利花、低代碼后端科侈。
使用 rxModels,只需要繪制 ER 圖就可以定制一個開箱即用的后端炒事。提供粒度精確到字段的權限管理功能臀栈,并對實例級別的權限管理提供表達式支持。
主要模塊有:圖形化的實體挠乳、關系管理界面( rx-models Client)权薯,通用JSON格式的數(shù)據操作接口服務( rx-models ),前端調用輔助 Hooks 庫( rxmodels-swr )等睡扬。
rxModels 基于 TypeScript盟蚣,NestJS,TypeORM 和 Antv x6 實現(xiàn)卖怜。
TypeScript 的強類型支持屎开,可以把一些錯誤在編譯時就解決掉了,IDE有了強類型的支持马靠,可以自動引入依賴奄抽,提高了開發(fā)效率蔼两,節(jié)省了時間。
TypeScript 編譯以后的目標執(zhí)行碼時JS逞度,一種運行時解釋語言额划,這個特性賦予了 rxModels 動態(tài)發(fā)布實體和熱加載 指令
的能力。用戶可以使用 指令
實現(xiàn)業(yè)務邏輯档泽,擴展通用 JSON 數(shù)據接口锁孟。給 rxModels 增加了更多使用場景。
NestJS 有助于代碼的組織茁瘦,使其擁有一個良好的架構品抽。
TypeORM 是一款輕量級 ORM 庫,可以把對象模型映射到關系數(shù)據庫甜熔。它能夠 “分離實體定義”圆恤,傳入 JSON 描述就可以構建數(shù)據庫,并對數(shù)據庫提供面向對象的查詢支持腔稀。得益于這個特性盆昙,圖形化的業(yè)務模型轉換成數(shù)據庫數(shù)據庫模型,rxModels 僅需要少量代碼就可以完成焊虏。
AntV X6 功能相對已經比較全面了淡喜,它支持在節(jié)點(node)里面嵌入 React組件,利用這個個性诵闭,使用它來繪制 ER 圖炼团,效果非常不錯。如果后面有時間疏尿,可以再寫一篇文章瘟芝,介紹如何使用 AntV x6繪制 ER 圖。
要想跟著本文褥琐,把這個項目一步步做出來锌俱,最好能夠提前學習一下本節(jié)提到的技術棧。
rxModels 目標定位
主要為中小項目服務敌呈。
為什么不敢服務大項目贸宏?
真不敢,作者是業(yè)余程序員磕洪,沒有大項目相關的任何經驗吭练。
梳理數(shù)據及數(shù)據映射
先看一下演示,從直觀上知道項目的樣子:rxModels演示 褐鸥。
元數(shù)據定義
元數(shù)據(Meta)线脚,用于描述業(yè)務實體模型的數(shù)據赐稽。一部分元數(shù)據轉化成 TypeORM 實體定義叫榕,隨之生成數(shù)據庫浑侥;另一部分元數(shù)據業(yè)務模型是圖形信息,比如實體的大小跟位置晰绎,關系的位置跟形狀等寓落。
需要轉化成 TypeORM 實體定義的元數(shù)據有:
import { ColumnMeta } from "./column-meta";
/**
* 實體類型枚舉,目前僅支持普通實體跟枚舉實體荞下,
* 枚舉實體類似語法糖伶选,不映射到數(shù)據庫,
* 枚舉類型的字段映射到數(shù)據庫是string類型
*/
export enum EntityType{
NORMAL = "Normal",
ENUM = "Enum",
}
/**
* 實體元數(shù)據
*/
export interface EntityMeta{
/** 唯一標識 */
uuid: string;
/** 實體名稱 */
name: string;
/** 表名尖昏,如果tableName沒有被設置仰税,會把實體名轉化成蛇形命名法,并以此當作表名 */
tableName?: string;
/** 實體類型 */
entityType?: EntityType|"";
/** 字段元數(shù)據列表 */
columns: ColumnMeta[];
/** 枚舉值JSON抽诉,枚舉類型實體使用陨簇,不參與數(shù)據庫映射 */
enumValues?: any;
}
/**
* 字段類型,枚舉迹淌,目前版本僅支持這些類型河绽,后續(xù)可以擴展
*/
export enum ColumnType{
/** 數(shù)字類型 */
Number = 'Number',
/** 布爾類型 */
Boolean = 'Boolean',
/** 字符串類型 */
String = 'String',
/** 日期類型 */
Date = 'Date',
/** JSON類型 */
SimpleJson = 'simple-json',
/** 數(shù)組類型 */
SimpleArray = 'simple-array',
/** 枚舉類型 */
Enum = 'Enum'
}
/**
* 字段元數(shù)據,基本跟 TypeORM Column 對應
*/
export interface ColumnMeta{
/** 唯一標識 */
uuid: string;
/** 字段名 */
name: string;
/** 字段類型 */
type: ColumnType;
/** 是否主鍵 */
primary?: boolean;
/** 是否自動生成 */
generated?: boolean;
/** 是否可空 */
nullable?: boolean;
/** 字段默認值 */
default?: any;
/** 是否唯一 */
unique?: boolean;
/** 是否是創(chuàng)建日期 */
createDate?: boolean;
/** 是否是更新日期 */
updateDate?: boolean;
/** 是否是刪除日期唉窃,軟刪除功能使用 */
deleteDate?: boolean;
/**
* 是否可以在查詢時被選擇耙饰,如果這是為false,則查詢時隱藏纹份。
* 密碼字段會使用它
*/
select?: boolean;
/** 長度 */
length?: string | number;
/** 當實體是枚舉類型時使用 */
enumEnityUuid?:string;
/**
* ============以下屬性跟TypeORM對應苟跪,但是尚未啟用
*/
width?: number;
version?: boolean;
readonly?: boolean;
comment?: string;
precision?: number;
scale?: number;
}
/**
* 關系類型
*/
export enum RelationType {
ONE_TO_ONE = 'one-to-one',
ONE_TO_MANY = 'one-to-many',
MANY_TO_ONE = 'many-to-one',
MANY_TO_MANY = 'many-to-many',
}
/**
* 關系元數(shù)據
*/
export interface RelationMeta {
/** 唯一標識 */
uuid: string;
/** 關系類型 */
relationType: RelationType;
/** 關系的源實體標識 */
sourceId: string;
/** 關系目標實體標識 */
targetId: string;
/** 源實體上的關系屬性 */
roleOnSource: string;
/** 目標實體上的關系屬性 */
roleOnTarget: string;
/** 擁有關系的實體ID,對應 TypeORM 的 JoinTable 或 JoinColumn */
ownerId?: string;
}
不需要轉化成 TypeORM 實體定義的元數(shù)據有:
/**
* 包的元數(shù)據
*/
export interface PackageMeta{
/** ID蔓涧,主鍵 */
id?: number;
/** 唯一標識 */
uuid: string;
/** 包名 */
name: string;
/**實體列表 */
entities?: EntityMeta[];
/**ER圖列表 */
diagrams?: DiagramMeta[];
/**關系列表 */
relations?: RelationMeta[];
}
import { X6EdgeMeta } from "./x6-edge-meta";
import { X6NodeMeta } from "./x6-node-meta";
/**
* ER圖元數(shù)據
*/
export interface DiagramMeta {
/** 唯一標識 */
uuid: string;
/** ER圖名稱 */
name: string;
/** 節(jié)點 */
nodes: X6NodeMeta[];
/** 關系的連線 */
edges: X6EdgeMeta[];
}
export interface X6NodeMeta{
/** 對應實體標識uuid */
id: string;
/** 節(jié)點x坐標 */
x?: number;
/** 節(jié)點y坐標 */
y?: number;
/** 節(jié)點寬度 */
width?: number;
/** 節(jié)點高度 */
height?: number;
}
import { Point } from "@antv/x6";
export type RolePosition = {
distance: number,
offset: number,
angle: number,
}
export interface X6EdgeMeta{
/** 對應關系 uuid */
id: string;
/** 折點數(shù)據 */
vertices?: Point.PointLike[];
/** 源關系屬性位置標簽位置 */
roleOnSourcePosition?: RolePosition;
/** 目標關系屬性位置標簽位置 */
roleOnTargetPosition?: RolePosition;
}
rxModels有一個后端服務削咆,基于這些數(shù)據構建數(shù)據庫。
rxModels有一個前端管理界面蠢笋,管理并生產這些數(shù)據拨齐。
服務端 rx-models
整個項目的核心,基于NestJS構建昨寞。需要安裝TypeORM瞻惋,只安裝普通 TypeORM 核心項目,不需要安裝 NestJS 封裝版援岩。
nest new rx-models
cd rx-models
npm install npm install typeorm
這只是關鍵安裝歼狼,其他的庫,不一一列舉了享怀。
具體項目已經完成羽峰,代碼地址:https://github.com/rxdrag/rx-models。
第一個版本承擔技術探索的任務,僅支持 MySQL 足夠了梅屉。
通用JSON接口
設計一套接口值纱,規(guī)定好接口語義,就像 GraphQL 那樣坯汤。這樣做的是優(yōu)勢虐唠,就是不需要接口文檔,也不需要定義接口版本了惰聂。
接口以 JSON 為參數(shù)疆偿,返回也是 JSON 數(shù)據,可以叫 JSON 接口搓幌。
查詢接口
接口描述:
url: /get/jsonstring...
method: get
返回值:{
data:any,
pagination?:{
pageSize: number,
pageIndex: number,
totalCount: number
}
}
URL 長度是 2048 個字節(jié)杆故,這個長度傳遞一個查詢字符串足夠用了,在查詢接口中溉愁,可以把 JSON 查詢參數(shù)放在 URL 里反番,使用 get 方法查數(shù)據。
把 JSON 查詢參數(shù)放在 URL 里叉钥,有一個明顯的優(yōu)勢罢缸,就是客戶端可以基于 URL 緩存查詢結果,比如使用 SWR 庫投队。
有個特別需要注意的點就是URL轉碼枫疆,要不然查詢時,like 使用 %
會導致后端出錯敷鸦。所以息楔,給客戶端寫一套查詢 SDK,封裝這些轉碼類操作是有必要的扒披。
查詢接口示例
傳入實體名字值依,就可以查詢實體的實例,比如要查詢所有的文章(Post)碟案,可以這么寫:
{
"entity": "Post"
}
要查詢 id = 1
的文章愿险,則這樣寫:
{
"entity": "Post",
"id": 1
}
把文章按照標題和日期排序,這么寫:
{
"entity": "Post",
"@orderBy": {
"title": "ASC",
"updatedAt": "DESC"
}
}
只需要查詢文章的 title 字段价说,這么寫:
{
"entity": "Post",
"@select": ["title"]
}
這么寫也可以:
{
"entity @select(title)": "Post"
}
只取一條記錄:
{
"entity": "Post",
"@getOne": true
}
或者:
{
"entity @getOne": "Post"
}
只查標題中有“水”字的文章:
{
"entity": "Post",
"title @like": "%水%"
}
還需要更復雜的查詢辆亏,內嵌類似 SQL 的表達式吧:
{
"entity": "Post",
"@where": "name %like '%風%' and ..."
}
數(shù)據太多了,分頁鳖目,每頁25條記錄取第一頁:
{
"entity": "Post",
"@paginate": [25, 0]
}
或者:
{
"entity @paginate(25, 0)": "Post"
}
關系查詢扮叨,附帶文章的圖片關系 medias :
{
"entity": "Post",
"medias": {}
}
關系嵌套:
{
"entity": "Post",
"medias": {
"owner":{}
}
}
給關系加個條件:
{
"entity": "Post",
"medias": {
"name @like": "%風景%"
}
}
只取關系的前5個
{
"entity": "Post",
"medias @count(5)": {}
}
聰明的您,可以按照這個方向领迈,對接口做進一步的設計更改彻磁。
@
符號后面的碍沐,稱之為 指令。
把業(yè)務邏輯放在指令里衷蜓,可以對接口進行非常靈活的擴展累提。比如在文章內容(content)底部附加加一個版權聲明,可以定義一個 @addCopyRight
指令:
{
"entity": "Post",
"@addCopyRight": "content"
}
或者:
{
"entity @addCopyRight(content)": "Post"
}
指令看起來是不是像一個插件恍箭?
既然是個插件刻恭,那就賦予它熱加載的能力瞧省!
通過管理界面扯夭,上傳第三方指令代碼,就可以把指令插入系統(tǒng)鞍匾。
第一版不支持指令上傳功能交洗,但是架構設計已經預留了這個能力,只是配套的界面沒做橡淑。
post 接口
接口描述:
url: /post
method: post
參數(shù): JSON
返回值: 操作成功的對象
通過post方法构拳,傳入JSON數(shù)據。
預期post接口具備這樣的能力梁棠,傳入一組對象組合(或者說附帶關系約束的對象樹)置森,直接把這組對象同步到數(shù)據庫。
如果給對象提供了id字段符糊,則更新已有對象凫海,沒有提供id字段,則創(chuàng)建新對象男娄。
post接口示例
上傳一篇文章行贪,帶圖片關聯(lián),可以這么寫:
{
"Post": {
"title": "輕輕的模闲,我走了",
"content": "...",
// 作者關聯(lián) id
"author": 1,
// 圖片關聯(lián) id
"medias":[3, 5, 6 ...]
}
}
也可以一次傳入多篇文章
{
"Post": [
{
"id": 1,
"title": "輕輕的建瘫,我走了",
"content": "內容有所改變...",
"author": 1,
"medias":[3, 5, 6 ...]
},
{
"title": "正如,我輕輕的來",
"content": "...",
"author": 1,
"medias": [6, 7, 8 ...]
}
]
}
第一篇文章有id字段尸折,是更新數(shù)據庫的操作啰脚,第二篇文章沒有id字段,是創(chuàng)建新的实夹。
也可以傳入多個實體的實例拣播,類似這樣,同時傳入文章(Post)跟媒體(Media)的實例:
{
"Post": [
{
...
},
{
...
}
],
"Media": [
{
...
}
]
}
可以把關聯(lián)一并傳入收擦,如果一篇文章關聯(lián)一個 SeoMeta 對象贮配,創(chuàng)建文章時,一并創(chuàng)建 SeoMeta:
{
"Post": {
"title": "輕輕的塞赂,我走了",
"content": "...",
"author": 1,
"medias":[3, 5, 6 ...],
"seoMeta":{
"title": "詩篇解讀:輕輕的泪勒,我走了|詩篇解讀網",
"descript": "...",
"keywords": "詩篇,解讀,詩篇解讀"
}
}
}
傳入這個參數(shù)圆存,會同時創(chuàng)建兩個對象叼旋,并在它們之間建立關聯(lián)。
正常情況下刪除這個關聯(lián)沦辙,可以這樣寫:
{
"Post": {
"title": "輕輕的夫植,我走了",
"content": "...",
"author": 1,
"medias":[3, 5, 6 ...],
"seoMeta":null
}
}
這樣的方式保存文章,會刪除跟 SeoMeta 的關聯(lián)油讯,但是 SeoMeta 的對象并沒有被刪除详民。別的文章也不需要這個 SeoMeta,不主動刪除它陌兑,數(shù)據庫里就會生成一條垃圾數(shù)據沈跨。
保存文章的時候,添加一個 @cascade
指令兔综,能解決這個問題:
{
"Post @cascade(medias)": {
"title": "輕輕的饿凛,我走了",
"content": "...",
"author": 1,
"medias":[3, 5, 6 ...],
"seoMeta":null
}
}
@cascade
指令會級聯(lián)刪除與之關聯(lián)的 SeoMeta 對象。
這個指令能放在關聯(lián)屬性上软驰,寫成這樣嗎涧窒?
{
"Post": {
"title": "輕輕的锭亏,我走了",
"content": "...",
"author": 1,
"medias @cascade":[3, 5, 6 ...],
"seoMeta":null
}
}
最好不要這樣寫纠吴,客戶端用起來不會很方便。
自定義指令可以擴展post接口贰镣,比如呜象,要加一個發(fā)送郵件的業(yè)務,可以開發(fā)一個 @sendEmail
指令:
{
"Post @sendEmail(title, content, water@rxdrag.com)": {
"title": "輕輕的碑隆,我走了",
"content": "...",
"author": 1,
"medias @cascade":[3, 5, 6 ...],
}
}
假設每次保存文章成功后恭陡,sendEmail 指令都會把標題跟內容,發(fā)送到指定郵箱上煤。
update 接口
接口描述:
url: /update
method: post
參數(shù): JSON
返回值: 操作成功的對象
post
接口已經具備了 update 功能了休玩,為什么還要再做一個 update
接口?
有時候劫狠,需要一個批量修改一個或者幾個字段的能力拴疤,比如把指定的消息標記為已讀。
為了應對這樣的場景独泞,設計了 update
接口呐矾。假如,要所有文章的狀態(tài)更新為“已發(fā)布”:
{
"Post": {
"status": "published",
"@ids":[3, 5, 6 ...],
}
}
基于安全方面的考慮懦砂,接口不提供條件指令蜒犯,只提供 @ids
指令(遺留原因组橄,演示版不需要@符號,直接寫 ids
就行罚随,后面會修改)玉工。
delete 接口
接口描述:
url: /delete
method: post
參數(shù): JSON
返回值: 被刪除的對象
delete 接口跟 update 接口一樣,不提供條件指令淘菩,只接受 id 或者 id 數(shù)組骨望。
要刪除文章涤伐,只需要這么寫:
{
"Post": [3, 5, ...]
}
這樣的刪除蜻牢,跟 update 一樣罪既,也不會刪除跟文章相關的對象,級聯(lián)刪除的話需要指令 @cascade
进陡。
級聯(lián)刪除 SeoMeta愿阐,這么寫:
{
"Post @cascade(seoMeta)": [3, 5, ...]
}
upload 接口
url: /upload
method: post
參數(shù): FormData
headers: {"Content-Type": "multipart/form-data;boundary=..."}
返回值: 上傳成功后生成RxMedia對象
rxModels 最好提供在線文件管理服務功能微服,跟第三方的對象管理服務趾疚,比如騰訊云、阿里云以蕴、七牛什么的糙麦,結合起來。
第一版先不實現(xiàn)跟第三方對象管理的整合丛肮,文件存在本地赡磅,文件類型僅支持圖片。
用實體 RxMedia 管理這些上傳的文件宝与,客戶端創(chuàng)建FormData焚廊,設置如下參數(shù):
{
"entity": "RxMedia",
"file": ...,
"name": "文件名"
}
全部JSON接口介紹完了,接下就是如何實現(xiàn)并使用這些接口习劫。
繼續(xù)之前咆瘟,說一下為什么選用JSON,而不用其他方式诽里。
為什么不用 oData
開始這個項目的時候袒餐,對 oData 并不了解。
簡單查了點資料谤狡,說是灸眼,只有在需要Open Data(開放數(shù)據給其他組織)時候,才有必要按照OData協(xié)議設計RESTful API墓懂。
如果不是把數(shù)據開放給其他組織焰宣,引入 oData 增加了發(fā)雜度。需要開發(fā)解析oData參數(shù)解析引擎捕仔。
oData 出了很長時間匕积,并沒有多么流行佛嬉,還不如后來的 GraphQL 知名度高。
為什么不用 GraphQL闸天?
嘗試過暖呕,沒用起來。
一個人苞氮,做開源項目湾揽,只能接入現(xiàn)有的開源生態(tài)。一個人什么都做笼吟,是不可能完成的任務库物。
要用GraphQL,只能用現(xiàn)有的開源庫〈铮現(xiàn)有的主流 GraphQL 開源庫戚揭,大部分都是基于代碼生成的。前一篇文章說過撵枢,不想做一個基于代碼生成的低代碼項目民晒。
還有一個原因,目標定位是中小項目锄禽。GraphQL對這些中小項目來說潜必,有兩個問題:1、有些笨重沃但;2磁滚、用戶的學習成本高。
有的小項目就三五個頁面宵晚,拉一個輕便的小后端垂攘,很短時間就搭起來了,沒有必要用 GraphQL淤刃。
GraphQL的學習成本并不低晒他,有些中小項目的用戶是不愿意付出這些學習成本的。
綜合這些因素钝凶,第一個版本的接口仪芒,沒有使用 GraphQL。
使用 GraphQL 的話耕陷,需要怎么做掂名?
跟一些朋友交流的時候,有些朋友對 GraphQL 還是情有獨鐘的哟沫。并且經過幾年的發(fā)展饺蔑,GraphQL 的熱度慢慢開始上來了。
假如使用 GraphQL 做一個類似項目嗜诀,需要怎么做呢猾警?
需要自己開發(fā)一套 GraphQL 服務端孔祸,這個服務端類似 Hasura,不能用代碼生成機制发皿,使用動態(tài)運行機制崔慧。Hasura 把 GQL 編譯成 SQL,你可以選擇這樣做穴墅,也可以不選擇這樣做惶室,只要能不經過編譯過程,就把對象按照 GQL 查詢要求玄货,拉出來就行皇钞。
需要在 GraphQL 的框架下,充分考慮權限管理松捉,業(yè)務邏輯擴展和熱加載等方面夹界。這就需要對 GraphQL 有比較深入的理解。
如果要做低代碼前端隘世,那么還需要做一個特殊的前端框架可柿,像 apollo 這樣的 GraphQL 前端庫庫,并不適合做低代碼前端以舒。因為低代碼前端需要動態(tài)類型綁定趾痘,這個需求跟這些前端庫的契合慢哈,并不是特別理想蔓钟。
每一項,都需要大量時間跟精力卵贱,不是一個人能完成的工作滥沫,需要一個團隊。
或有一天键俱,有機會兰绣,作者也想進行這樣方面的嘗試。
但也未必會成功编振,GraphQL 本身并不代表什么缀辩,假如它能夠使用者帶來實實在在的好處,才是被選擇的理由踪央。
登錄驗證接口
使用 jwt 驗證機制臀玄,實現(xiàn)兩個登錄相關的接口。
url: /auth/login
method: post
參數(shù): {
username: string,
password: string
}
返回值:jwt token
url: /auth/me
method: get
返回值: 當前登錄用戶畅蹂,RxUser類型
這兩個接口實現(xiàn)起來健无,沒有什么難的,跟著NestJs文檔做一下就行了液斜。
元數(shù)據存儲
客戶端通過 ER 圖的形式生產的元數(shù)據累贤,存儲在數(shù)據庫叠穆,一個實體 RxPackage
就夠了:
export interface RxPackage {
/* id 數(shù)據庫主鍵 */
id: number;
/** 唯一標識uuid,當不同的項目之間共享元數(shù)據時臼膏,這個字段很有用 */
uuid: string;
/** 包名 */
name: string;
/** 包的所有實體元數(shù)據硼被,以JSON形式存于數(shù)據庫 */
entities: any;
/** 包的所有 ER 圖,以JSON形式存于數(shù)據庫 */
diagrams?: any;
/** 包的所有關系渗磅,以JSON形式存于數(shù)據庫 */
relations?: any;
}
數(shù)據映射完成后祷嘶,在界面中看到的一個包的所有內容,就對應 rx_package
表的一條數(shù)據記錄夺溢。
這些數(shù)據怎么被使用呢论巍?
我們給包增加一個發(fā)布功能,如果包被發(fā)布风响,就根據這條數(shù)據庫記錄嘉汰,做一個JSON文件,放在 schemas 目錄下状勤,文件名就是 ${uuid}.json
鞋怀。
服務端創(chuàng)建 TypeORM 連接時,熱加載這些JSON文件持搜,并把它們解析成 TypeORM 實體定義數(shù)據密似。
應用安裝接口
rxModels 的最終目標是,發(fā)布一個代碼包葫盼,使用者通過圖形化界面安裝即可残腌,不要接觸代碼。
兩頁向導贫导,即可完成安裝抛猫,需要接口:
url: install
method: post
參數(shù): {
/** 數(shù)據庫類型 */
type: string;
/** 數(shù)據庫所在主機 */
host: string;
/** 數(shù)據庫端口 */
port: string;
/** 數(shù)據庫schema名 */
database: string;
/** 數(shù)據登錄用戶 */
username: string;
/** 數(shù)據庫登錄密碼 */
password: string;
/** 超級管理員登錄名 */
admin: string;
/** 超級管理員密碼 */
adminPassword: string;
/** 是否創(chuàng)建演示賬號 */
withDemo: boolean;
}
還需要一個查詢是否已經安裝的接口:
url: /is-installed
method: get
返回值: {
installed: boolean
}
只要完成這些接口,后端的功能就實現(xiàn)了孩灯,加油闺金!
架構設計
得益于 NestJs 優(yōu)雅的框架,可以把整個后端服務分為以下幾個模塊:
auth, 普通 NestJS module峰档,實現(xiàn)登錄驗證接口败匹。本模塊很簡單,后面不會單獨介紹了讥巡。
package-manage, 元數(shù)據的管理發(fā)布模塊掀亩。
install, 普通 NestJS module,實現(xiàn)安裝功能尚卫。
schema, 普通 NestJS module归榕,管理系統(tǒng)元數(shù)據,并把前面定義的格式的元數(shù)據吱涉,轉化成 TypeORM 能接受的實體定義刹泄,核心代碼是
SchemaService
外里。typeorm, 對 TypeORM 的封裝,提供帶有元數(shù)據定義的 Connection特石,核心代碼是
TypeOrmService
盅蝗,該模塊沒有 Controller。magic, 項目最核心模塊姆蘸,通用JSON接口實現(xiàn)模塊墩莫。
directive, 指令定義模塊,定義指令功能用到的基礎類逞敷,熱加載指令狂秦,并提供指令檢索服務。
directives, 所有指令實現(xiàn)類推捐,系統(tǒng)從這個目錄熱加載所有指令裂问。
magic-meta, 解析JSON參數(shù)用到的數(shù)據格式,主要使用模塊是
magic
牛柒,由于directive
模塊也會用到這些數(shù)據堪簿,為了避免模塊之間的循環(huán)依賴,把這部分數(shù)據抽出來皮壁,單獨作為一個模塊椭更,那兩個模塊同時依賴這個模塊蛾魄。entity-interface, 系統(tǒng)種子數(shù)據類型接口虑瀑,主要用于 TypeScript 編譯器的類型識別∥吠螅客戶端的代碼導出功能導出的文件缴川,直接復制過來的∶柘冢客戶端也會復制一份同樣的代碼來用。
包管理 package-manage
提供一個接口 publishPackages
而线。把參數(shù)傳入的元數(shù)據铭污,發(fā)布到系統(tǒng)里,同步到數(shù)據庫模式:
就是一個包一個文件膀篮,放在根目錄的
schemas
目錄下嘹狞,文件名就是包的uuid
+ .json 后綴。通知 TypeORM 模塊重新創(chuàng)建數(shù)據庫連接誓竿,同時同步數(shù)據庫磅网。
安裝模塊 install
模塊內有一個種子文件 install.seed.json
,里面是系統(tǒng)預置的一些實體筷屡,格式就是上文定義的元數(shù)據格式涧偷,這些數(shù)據統(tǒng)一組織在 System
包里簸喂。
客戶端沒有完成的時候,手寫了一個 ts 文件用于調試燎潮,客戶端完成以后喻鳄,直接利用包的導出功能,導出了一個 JSON 文件确封,替換了手寫的 ts 文件除呵。相當于基礎數(shù)據部分,可以自舉了爪喘。
這個模塊的核心代碼在 InstallService
里颜曾,它分步完成:
把客戶端傳來的數(shù)據庫配置信息,寫入根目錄的dbconfig.json 文件秉剑。
把
install.seed.json
文件里面的預定義包發(fā)布泛啸。直接調用上文說的publishPackages
實現(xiàn)發(fā)布功能。
元數(shù)據管理模塊 schema
該模塊提供一個 Controller秃症,名叫 SchemaController
候址。提供一個 get 接口 /published-schema
,用于獲取已經發(fā)布的元數(shù)據信息种柑。
這些已經發(fā)布的元數(shù)據信息可以被客戶端的權限設置模塊使用岗仑,因為只有已經發(fā)布的模塊,對它設置權限才有意義聚请。低代碼可視化編輯前端荠雕,也可以利用這些信息,進行下拉選擇式的數(shù)據綁定驶赏。
核心類 SchemaService
炸卑,還提供了更多的功能:
從
/schemas
目錄下,加載已經發(fā)布的元數(shù)據煤傍。把這些元數(shù)據組織成列表+樹的結構盖文,提供按名字、按UUID等方式的查詢服務蚯姆。
把元數(shù)據解析成 TypeORM 能接受的實體定義 JSON五续。
封裝 TypeORM
自己寫一個 ORM 庫工作量是很大的,不得不使用現(xiàn)成的龄恋,TypeORM 是個不錯的選擇疙驾,一來,她像個年輕的姑娘郭毕,漂亮又活力四射它碎。二來,她不像 Prisma 那么臃腫。
為了迎合現(xiàn)有的 TyeORM扳肛,有些地方不得不做妥協(xié)傻挂。這種低代碼項目后端,比較理想的實現(xiàn)方式自己做一個 ORM 庫敞峭,完全根據自己的需求實現(xiàn)功能踊谋,那樣或許就有青梅竹馬的感覺了,但是需要團隊旋讹,不是一個人能完成殖蚕。
既然是一個人,那么就安心做一個人能做的事情好了沉迹。
TypeORM 只有一個入口能夠傳入實體定義睦疫,就是 createConnection
。需要在這個函數(shù)調用前鞭呕,解析完元數(shù)據蛤育,分離出實體定義。這個模塊的 TypeOrmService
完成這些 connection 的管理工作葫松,依賴的 schema 模塊的 SchemaService
瓦糕。
通過 TypeOrmService
可以重啟當前連接(關閉并重新創(chuàng)建),以更新數(shù)據庫定義腋么。創(chuàng)建連接的時候咕娄,使用 install 模塊創(chuàng)建的 dbconfig.json
文件獲取數(shù)據庫配置。注意珊擂,TypeORM 的 ormconfig.json
文件是沒有被使用的圣勒。
magic 模塊
在 magic 模塊,不管查詢還是更新摧扇,每一個接口實現(xiàn)的操作圣贸,都在一個完整的事務里。
難道查詢接口也要包含在一個事務里扛稽?
是的吁峻,因為有的時候查詢可能會包含一些簡單操作數(shù)據庫的指令,比如查詢一篇文章的時候庇绽,順便把它的閱讀次數(shù) +1锡搜。
magic 模塊的增刪查改等操作,都受到權限的約束瞧掺,把它的核心模塊 MagicInstanceService
傳遞給指令,指令代碼里可以放心使用它的接口操作數(shù)據庫凡傅,不需要關心權限問題辟狈。
MagicInstanceService
MagicInstanceService
是接口 MagicService
的實現(xiàn)。接口定義:
import { QueryResult } from 'src/magic-meta/query/query-result';
import { RxUser } from 'src/entity-interface/RxUser';
export interface MagicService {
me: RxUser;
query(json: any): Promise<QueryResult>;
post(json: any): Promise<any>;
delete(json: any): Promise<any>;
update(json: any): Promise<any>;
}
magic 模塊的 Controller 直接調用這個類,實現(xiàn)上文定義的接口哼转。
AbilityService
權限管理類明未,查詢當前登錄用戶的實體跟字段的權限配置。
query
/magic/query
目錄壹蔓,實現(xiàn) /get/json...
接口的代碼趟妥。
MagicQuery
是核心代碼,實現(xiàn)查詢業(yè)務邏輯佣蓉。它使用 MagicQueryParser
把傳入的 JSON 參數(shù)披摄,解析成一棵數(shù)據樹,并分離相關指令勇凭。數(shù)據結構定義在 /magic-meta/query
目錄疚膊。代碼量太大,沒有精力一一解析虾标。自己翻閱一下寓盗,有問題可以跟作者聯(lián)系。
需要特別注意的是 parseWhereSql
函數(shù)璧函。這個函數(shù)負責解析類似 SQL Where 格式的語句傀蚌,使用了開源庫 sql-where-parser
。
把它放在這個目錄蘸吓,是因為 magic 模塊需要用到它善炫,同時 directive 模塊也需要用到它,為了避免模塊的循環(huán)依賴美澳,把它獨立抽到這個目錄销部。
/magic/query/traverser
目錄存放一些遍歷器,用于處理解析后的樹形數(shù)據制跟。
MagicQuery
使用 TypeORM 的 QueryBuilder
構建查詢舅桩。關鍵點:
使用 directive 模塊的
QueryDirectiveService
獲取指令處理類。指令處理類可以:1雨膨、構建QueryBuilder
用到的條件語句擂涛,2、過濾查詢結果聊记。從
AbilityService
拿到權限配置撒妈,根據權限配置修改QueryBuilder
, 根據權限配置過濾查詢結果中的字段排监。QueryBuilder 用到的查詢語句分兩部分:1狰右、影響查詢結果數(shù)量的語句,比如 take 指令舆床、paginate指令棋蚌。這些指令只是要截取指令數(shù)量的結果嫁佳;2、其他沒有這種影響的查詢語句谷暮。因為分頁時蒿往,需要返回一個總的記錄條數(shù),用第二類查詢語句先查一次數(shù)據庫湿弦,獲得總條數(shù)瓤漏,然后加入第一類查詢語句獲得查詢結果。
post
/magic/post
目錄颊埃,實現(xiàn) /post
接口的代碼蔬充。
MagicPost
類是核心代碼,實現(xiàn)業(yè)務邏輯竟秫。它使用 MagicPostParser
把傳入的JSON參數(shù)娃惯,解析成一棵數(shù)據樹,并分離相關指令肥败。數(shù)據結構定義在 /magic-meta/post
目錄趾浅。它可以:
遞歸保存關聯(lián)對象,理論上可以無限嵌套馒稍。
根據
AbilityService
做權限檢查皿哨。使用 directive 模塊的
PostDirectiveService
獲取指令處理類, 在實例保存前跟保存后會調用指令處理程序纽谒,詳情請翻閱代碼证膨。
update
/magic/update
目錄,實現(xiàn) /update
接口的代碼鼓黔。
功能簡單央勒,代碼也簡單。
delete
/magic/delete
目錄澳化,實現(xiàn) /delete
接口的代碼崔步。
功能簡單,代碼也簡單缎谷。
upload
/magic/upload
目錄井濒,實現(xiàn) /upload
接口的代碼。
upload 目前功能比較簡單列林,后面可以考添加一些裁剪指令等功能瑞你。
directive 模塊
指令服務模塊。熱加載指令希痴,并對這些指令提供查詢服務者甲。
這個模塊也比較簡單,熱加載使用的是 require 語句砌创。
關于后端过牙,其它模塊就沒什么好說的甥厦,都很簡單纺铭,直接看一下代碼就好寇钉。
客戶端 rx-models-client
需要一個客戶端,管理生產并管理元數(shù)據舶赔,測試通用數(shù)據查詢接口扫倡,設置實體權限,安裝等竟纳。創(chuàng)建一個普通的 React 項目撵溃, 支持 TypeScript。
npx create-react-app rx-models-client--template typescript
這個項目已經完成了锥累,在GitHub上缘挑,代碼地址:https://github.com/rxdrag/rx-models-client。
代碼量有點多桶略,全部在這里展開解釋语淘,有點放不下。只能挑關鍵點說一下际歼,有問題需要交流的話惶翻,請跟作者聯(lián)系。
ER圖 - 圖形化的業(yè)務模型
這個模塊是客戶端的核心鹅心,看起來比較唬人吕粗,其實一點都不難。目錄 src/components/entity-board
下旭愧,是該模塊全部代碼颅筋。
得益于 Antv X6,使得這個模塊的制作比預想簡單了許多输枯。
X6 充當?shù)慕巧楸茫皇且粋€視圖層。它只負責渲染實體圖形跟關系連線用押,并傳回一些用戶交互事件肢簿。它用于撤銷、重做的操作歷史功能蜻拨,在這個項目里用不上池充,只能全部自己寫。
Mobx 在這個模塊也占非常重要的地位缎讼,它管理了所有的狀態(tài)并承擔了部分業(yè)務邏輯收夸。低代碼跟拖拽類項目,Mobx 確實非常好用血崭,值得推薦卧惜。
定義 Mobx Observable 數(shù)據
上文定義的元數(shù)據厘灼,每一個對應一個 Mobx Observable 類座掘,再加一個根索引類绰寞,這數(shù)據相互包含池凄,構成一個樹形結構蛔垢,在 src/components/entity-board/store
目錄下邓馒。
-
EntityBoardStore
, 處于樹形結構的根節(jié)點棘街,也是該模塊的整體狀態(tài)數(shù)據怕篷,它記錄下面這些信息:
export class EntityBoardStore{
/**
* 是否有修改捐川,用于未保存提示
*/
changed = false;
/**
* 所有的包
*/
packages: PackageStore[];
/**
* 當前正在打開的 ER 圖
*/
openedDiagram?: DiagramStore;
/**
* 當前使用的 X6 Graph對象
*/
graph?: Graph;
/**
* 工具條上的關系被按下钻洒,記錄具體類型
*/
pressedLineType?: RelationType;
/**
* 處在鼠標拖動劃線的狀態(tài)
*/
drawingLine: LineAction | undefined;
/**
* 被選中的節(jié)點
*/
selectedElement: SelectedNode;
/**
* Command 模式奋姿,撤銷列表
*/
undoList: Array<Command> = [];
/**
* Command 模式,重做列表
*/
redoList: Array<Command> = [];
/**
* 構造函數(shù)傳入包元數(shù)據素标,會自動解析成一棵 Mobx Observable 樹
*/
constructor(packageMetas:PackageMeta[]) {
this.packages = packageMetas.map(
packageMeta=> new PackageStore(packageMeta,this)
);
makeAutoObservable(this);
}
/**
* 后面大量的set方法称诗,就不需要了展開了
*/
...
}
-
PackageStore
, 樹形完全跟上文定義的 PackageMeta 一致,區(qū)別就是 meta 相關的全都換成了 store 相關的:
export class PackageStore{
id?: number;
uuid: string;
name: string;
entities: EntityStore[] = [];
diagrams: DiagramStore[] = [];
relations: RelationStore[] = [];
status: PackageStatus;
constructor(meta:PackageMeta, public rootStore: EntityBoardStore){
this.id = meta.id;
this.uuid = meta?.uuid;
this.name = meta?.name;
this.entities = meta?.entities?.map(
meta=>new EntityStore(meta, this.rootStore, this)
)||[];
this.diagrams = meta?.diagrams?.map(
meta=>new DiagramStore(meta, this.rootStore, this)
)||[];
this.relations = meta?.relations?.map(
meta=>new RelationStore(meta, this)
)||[];
this.status = meta.status;
makeAutoObservable(this)
}
/**
* 省略set方法
*/
...
/**
* 最后提供一個把 Store 逆向轉成元數(shù)據的方法头遭,用于往后端發(fā)送數(shù)據
*/
toMeta(): PackageMeta {
return {
id: this.id,
uuid: this.uuid,
name: this.name,
entities: this.entities.map(entity=>entity.toMeta()),
diagrams: this.diagrams.map(diagram=>diagram.toMeta()),
relations: this.relations.map(relation=>relation.toMeta()),
status: this.status,
}
}
}
依此類推寓免,可以做出 EntityStore
、ColumnStore
任岸、RelationStore
和 DiagramStore
再榄。
前面定義的 X6NodeMeta
和 X6EdgeMeta
不需要制作相應的 store 類,因為沒法通過 Mobx 的機制更新 X6 的視圖享潜,要用其它方式完成這個工作困鸥。
DiagramStore
主要為展示 ER 圖提供數(shù)據。給它添加兩個方法:
export type NodeConfig = X6NodeMeta & {data: EntityNodeData};
export type EdgeConfig = X6EdgeMeta & RelationMeta;
export class DiagramStore {
...
/**
* 獲取當前 ER 圖所有的節(jié)點剑按,利用 mobx 更新機制疾就,
* 只要數(shù)據有更改,調用該方法的視圖會自動被更新艺蝴,
* 參數(shù)只是用了指示當前選中的節(jié)點猬腰,或者是否需要連線,
* 這些狀態(tài)會影響視圖猜敢,可以在這里直接傳遞給每個節(jié)點
*/
getNodes(
selectedId:string|undefined,
isPressedRelation:boolean|undefined
): NodeConfig[]
/**
* 獲取當前 ER 圖所有的連線姑荷,利用 mobx 更新機制,
* 只要數(shù)據有更改缩擂,調用該方法的視圖會自動被更新
*/
getAndMakeEdges(): EdgeConfig[]
}
如何使用 Mobx Observable 數(shù)據
使用 React 的 Context鼠冕,把上面定義的 store 數(shù)據傳遞給子組件。
定義 Context:
export const EnityContext = createContext<EntityBoardStore>({} as EntityBoardStore);
export const EntityStoreProvider = EnityContext.Provider;
export const useEntityBoardStore = (): EntityBoardStore => useContext(EnityContext);
創(chuàng)建 Context:
...
const [modelStore, setModelStore] = useState(new EntityBoardStore([]));
...
return (
<EntityStoreProvider value = {modelStore}>
...
</EntityStoreProvider>
)
使用的時候胯盯,直接在子組件里調用 const rootStore = useEntityBoardStore()
就可以拿到數(shù)據了懈费。
樹形編輯器
利用 Mui的樹形控件 + Mobx 對象,代碼并不復雜博脑,感興趣的話憎乙,翻翻看看票罐,有疑問留言或者聯(lián)系作者。
如何使用 AntV X6
X6 支持在節(jié)點里嵌入 React 組件泞边,定義一個組件 EntityView
嵌入進去就好该押。X6 相關代碼都在這個目錄下:
src/componets/entity-board/grahp-canvas
業(yè)務邏輯被拆分成很多 React Hooks:
useEdgeChange
, 處理關系線被拖動useEdgeLineDraw
, 處理畫線動過useEdgeSelect
, 處理關系線被選中useEdgesShow
, 渲染關系線,包括更新useGraphCreate
, 創(chuàng)建 X6 的 Grpah對象useNodeAdd
, 處理拖入一個節(jié)點的動作useNodeChange
, 處理實體節(jié)點被拖動或者改變大小useNodeSelect
, 處理節(jié)點被選中useNodesShow
, 渲染實體節(jié)點繁堡,包括更新
撤銷沈善、重做
撤銷、重做不僅跟 ER 圖相關椭蹄,還跟整個 store 樹相關。這就是說净赴,X6 的撤銷绳矩、重做機制用不了,只能自己重新做玖翅。
好在設計模式中的 Command 模式還算簡單翼馆,定義一些 Command,并定義好正負操作金度,可以很容易完成应媚。實現(xiàn)代碼在:
src/componets/entity-board/command
全局狀態(tài) AppStore
按照上問的方法,利用 Mobx 做一個全局的狀態(tài)管理類 AppStore猜极,用于管理整個應用的狀態(tài)中姜,比如彈出操作成功提示,彈出錯誤信息等跟伏。
代碼在 src/store
目錄下丢胚。
接口測試
代碼在 src/components/api-board
目錄下。
很簡單一個模塊受扳,代碼應該很容易懂携龟。使用了 rxmodels-swr 庫,直接參考它的文檔就好勘高。
JSON 輸入控件峡蟋,使用了 monaco 的 react 封裝:react-monaco-editor
,使用起來很簡單华望,安裝稍微麻煩一點蕊蝗,需要安裝 react-app-rewired
。
monaco 用的還不熟練立美,后面熟練了可以加入如下功能輸入提示和代碼校驗等功能匿又。
權限管理
代碼在 src/components/auth-board
目錄下。
這個模塊之主要是后端數(shù)據的組織跟接口定義建蹄,前端代碼很少碌更,基于rxmodels-swr 庫完成裕偿。
權限定義支持表達式,表達式類似 SQL 語句痛单,并內置了變量 $me
指代當前登錄用戶嘿棘。
前端輸入時,需要對 SQL 表達式進行校驗旭绒,所以也引入了開源庫 sql-where-parser
鸟妙。
安裝、登錄
安裝代碼在 src/components/install
目錄下挥吵。
登錄頁面是 src/components/login.tsx
重父。
代碼一眼就能瞅明白。
后記
這篇文章挺長的忽匈,但是還不確定有沒有把需要說的說清楚房午,有問題的話留言或者聯(lián)系作者吧。
演示能跑起來以后丹允,就已經冒著被踢的危險郭厌,在幾個 QQ 群發(fā)了一下。收到了很多反饋雕蔽,非常感謝熱心的朋友們折柠。
rxModels,終于走出去了第一步...
與前端的第一次接觸
rxModels來了批狐,熱情的走向前端們扇售。
前端們皺起了眉頭,說:“離遠點兒贾陷,你不是我們理想中的樣子缘眶。”
rxModels 說:“我還會改變髓废,還會成長巷懈,未來的某一天,我們一定是最好的搭檔慌洪《パ啵”
下一篇文章
《從 0 構建一個可視化低代碼前端》,估計要等一段時間了冈爹,要先把前端重構完涌攻。