我為LowCodeEngine低代碼引擎寫了個插件(低代碼開發(fā)工具)
讓不懂代碼的產(chǎn)品人員也能自己開發(fā)頁面了。
背景
低代碼一直以來爭議不斷,很多人認為低代碼平臺不好用,坑很多,很多功能實現(xiàn)不了,還不如自己用代碼開發(fā)。關(guān)于這個我說一下我的個人觀點,我覺得低代碼平臺不應(yīng)該給開發(fā)者使用,應(yīng)該是產(chǎn)品或者業(yè)務(wù)人員去使用,開發(fā)人員去維護低代碼平臺就好了。
低代碼平臺還有一個比較難平衡的點,如果想追求簡單,難免拓展性和靈活性變差,如果追求靈活性,難免要寫一些代碼,這樣上手難度就變高了。
LowCodeEngine低代碼引擎為了追求靈活性,所以對非前端技術(shù)人員不太友好,因為很多時候一個很簡單的功能都還要去寫代碼才能實現(xiàn)。
不過LowCodeEngine插件系統(tǒng)很強大,能夠快速實現(xiàn)自己想要的功能。下面就和大家分享一下我自己寫的一個插件,可以讓非技術(shù)人員也能快速開發(fā)頁面。
不使用插件實現(xiàn)一個小功能
下面先給大家演示一下,不使用我寫的插件的情況下,實現(xiàn)點擊一個按鈕,彈出一個彈框整個配置流程。
我這里用的是基于antd物料的官方demo平臺,官方還有一些基于其他物料的demo平臺,可以到這里查看。
先從左側(cè)的組件庫中拖一個按鈕到畫布中
再拖一個彈框組件到畫布
再拖一個文本組件到彈框里
在js源碼中添加一個變量
給彈框是否可見屬性綁定變量
綁定我們剛才在state里添加的變量,這里不知道是bug,還是特性,這里選不到剛才定義的變量,需要自己手寫。
變量綁定完成后,可以點擊這個給彈框隱藏掉,因為它會干擾我們給按鈕綁定事件。
選中按鈕,然后給按鈕onClick事件綁定方法
這里選擇新建一個方法,把visible設(shè)置為true。
點擊上面的預(yù)覽按鈕,可以看到效果
彈出來之后,發(fā)現(xiàn)點擊取消和確認不會關(guān)閉彈框,需要我們給取消和確認事件綁定方法,這里可以給彈框重新顯示出來
onOk事件也綁定onCancel方法
重新預(yù)覽一下,看一下效果
小結(jié)
看了上面步驟,大家是不是覺得很麻煩,并且對非react前端人員一點都不友好,更別說沒有代碼經(jīng)驗的產(chǎn)品人員了。
站在一個不懂代碼人的角度來看這個功能,無非是點一下按鈕,彈出一個彈框,如果能把這個步驟可視化出來就好了,給按鈕的點擊事件直接綁定彈框組件的顯示方法,而不是使用代碼中的變量去關(guān)聯(lián)。
我做的插件就是把組件之間的邏輯使用可視化的方法配置出來,而不是使用代碼去實現(xiàn)聯(lián)動。
使用插件實現(xiàn)功能
按鈕打開彈框
下面使用我寫的插件后,來實現(xiàn)一下上面例子。
先把一個按鈕和一個彈框到畫布中
點擊上方的事件管理按鈕,這個是自定義插件生成的按鈕
點擊事件管理按鈕后,會彈出一個側(cè)拉框,左側(cè)內(nèi)容會當(dāng)前畫布中的組件,可以在這里給組件的事件綁定動作。
在左側(cè)選擇按鈕組件
選擇onClick事件
點擊開始下面的加號,添加一個動作節(jié)點,點擊動作節(jié)點配置動作類型為組件方法,選擇彈框組件的打開彈框方法。
先保存一下,然后再給彈框的onOk事件添加關(guān)閉彈框動作,先選中彈框,然后選onOk事件,添加一個動作節(jié)點,給動作節(jié)點配置關(guān)閉彈框。
這樣就行了,不用寫一行代碼。
根據(jù)輸入網(wǎng)址打開網(wǎng)頁
下面再給大家演示一個稍微復(fù)雜一點的例子。使用按鈕打開用戶輸入的網(wǎng)址,需要先校驗一下用戶輸入的是否為網(wǎng)址格式,如果格式不對,就不打開,并提示格式錯誤。
鏈接:https://www.ixigua.com/7344764451138372131?utm_source=iframe_share
表單聯(lián)動
再來一個例子,實現(xiàn)簡單的表單元素聯(lián)動。
鏈接:https://www.ixigua.com/7344772943915778610?utm_source=iframe_share
小結(jié)
上面幾個例子沒有寫一行代碼實現(xiàn)了一些簡單的功能,實現(xiàn)的這些例子雖然簡單,但是插件其實已經(jīng)有了實現(xiàn)復(fù)雜功能的基礎(chǔ),并且不需要寫一行代碼,對不懂代碼的人比較友好。
插件內(nèi)容講解
前言
根據(jù)上面例子可以看出,插件主要更改了LowCodeEngine的事件綁定功能的方式和屬性綁定變量的彈框內(nèi)容。
事件管理
LowCodeEngine原生事件綁定動作的方式需要寫一定的代碼,對不懂代碼的人很不友好,所以我給改了一下,使用可視化的方式配置事件動作,比較符合普通人的思維,上手也很簡單。
事件管理頁面有四塊內(nèi)容,如下圖
組件區(qū):當(dāng)前畫布中存在的組件,可以選擇給某個組件事件綁定動作
事件區(qū):可以給當(dāng)前組件支持的某個事件綁定動作
動作配置區(qū):給前面選擇的組件事件綁定動作,因為組件的事件很多情況不只是執(zhí)行一個動作,所以支持配置多個動作。有時候在執(zhí)行動作的時候,需要根據(jù)條件去執(zhí)行,所以加了一個條件節(jié)點。執(zhí)行動作后,動作執(zhí)行完成后可能會有事件,比如調(diào)用接口后,接口有成功事件、失敗事件,這時候我們可以在接口調(diào)用完成后,根據(jù)事件執(zhí)行后續(xù)動作,所以還有一個事件節(jié)點。
所以目前事件流程配置有以下四種類型節(jié)點
- 開始節(jié)點:沒有意義,表示事件入口,不能配置。
- 動作節(jié)點:綁定動作,可以連接事件節(jié)點和條件節(jié)點
- 條件節(jié)點:可以配置多個條件分支,每個條件分支可以綁定一個動作節(jié)點,只能連接事件節(jié)點。
- 事件節(jié)點:每個動作節(jié)點都會有事件節(jié)點,只能連接動作節(jié)點。
節(jié)點配置:目前支持配置的節(jié)點只有動作節(jié)點和條件節(jié)點
- 動作節(jié)點:可以設(shè)置具體的執(zhí)行動作,比如調(diào)用組件方法等。
- 條件節(jié)點:可以添加多個條件,每個條件都可以綁定動作。
屬性綁定變量
為了上手簡單,我把組件屬性綁定變量的彈框內(nèi)容也改造了一下,LowCodeEngine原生綁定變量需要寫代碼,雖然擴展性很強,但是上手難度很高,所以我給改造成只需要選擇組件里暴露出來的變量就行了,還提供了一些常用函數(shù)可以用來判斷和處理數(shù)據(jù),上面demo項目里提供的函數(shù)比較少,真實項目可以內(nèi)置很多常用函數(shù),也可以動態(tài)拓展函數(shù)。
插件核心技術(shù)分享
在實現(xiàn)上面功能的時候,雖然LowCodeEngine文檔很詳細,但是還是遇到了一些問題,看了源碼才解決的。下面和大家分享一下,幫助大家寫好自己的插件。
事件管理插件
初始化插件項目
使用下面命令,可以快速創(chuàng)建一個插件
npm init @alilc/element your-material-name
這里類型選擇插件
如何拓展一個面板,官方文檔很詳細,大家可以看一下。
插件中使用到api
左側(cè)的組件樹數(shù)據(jù)可以通過下面方法獲取,下面代碼中ctx是LowCodeEngine引擎注入進來的上下文參數(shù)
const schema = ctx.project.exportSchema(IPublicEnumTransformStage.Save);return schema.componentsTree as IPublicTypeRootSchema[];
根據(jù)組件名稱獲取組件中文描述
ctx.material.getComponentMetasMap().get(componentName)?.title['zh-CN']
獲取當(dāng)前組件支持那些事件
// 通過當(dāng)前選中的組件id,獲取到組件const node = ctx.project.currentDocument.getNodeById(selectComponentId);// 根據(jù)組件名稱,獲取組件的props配置const { props } = ctx.material.getComponentMeta(node.componentName).getMetadata();// 過濾出事件propsreturn props.filter(p => p.propType === 'func');
動作流程配置完成后,把數(shù)據(jù)保存到當(dāng)前節(jié)點的當(dāng)前事件屬性中。
// 獲取流程編排數(shù)據(jù)const data = flowRef.current.save();// 根據(jù)id獲取節(jié)點const node = ctx.project.currentDocument.getNodeById(selectComponentId);// 給節(jié)點某個屬性設(shè)置值node.props.setPropValue(selectEventName, {type: 'flow',value: data,});
下次編輯的時候,獲取當(dāng)前節(jié)點事件配置的動作編排數(shù)據(jù)
const node = ctx.project.currentDocument.getNodeById(selectComponentId);return node.getPropValue(selectEvent);
流程編排
流程編排使用的組件是antv中的G6庫,具體實現(xiàn)參考了官方的這個案例。
組件方法
有人可能會有疑問,選中了彈框組件組件后,為什么會知道它有打開彈框和關(guān)閉彈框方法,這個是在物料組件里配置的,這個下面說到自定義物料的時候再詳細說,先和大家說一下,獲取物料暴露出來的方法。
// 獲取節(jié)點信息const node = ctx.project.currentDocument.getNodeById(componentId);// 根據(jù)組件名稱獲取物料配置信息const { configure } = ctx.material.getComponentMeta(node.componentName).getMetadata();// 獲取組件暴露出來的可調(diào)用方法return configure?.supports?.methods || [];
變量彈框插件
自定義變量綁定的彈框,這個我在官方文檔沒有找到,還是看了源碼才知道的。
// 注冊變量綁定面板,CustomVariableDialog是自定義組件ctx.skeleton.add({area: 'centerArea',type: 'Widget',content: CustomVariableDialog,name: 'variableBindDialog',props: {ctx,},});
可以在組件內(nèi)部監(jiān)聽打開變量圖標(biāo)點擊事件,打開我們自己的彈框
ctx.event.on('common:variableBindDialog.openDialog', ({ field }) => {// 獲取當(dāng)前屬性綁定的值setScriptValue(field.getValue()?.script || '')// 顯示彈框setVisible(true);// 保存field對象,因為后面給屬性設(shè)置值會用到它fieldRef.current = field;});
面板中內(nèi)置了一些常用函數(shù),如果想拓展也很簡單,符合下面數(shù)據(jù)格式就行。
{label: "isUrl",template: "func.isUrl(${v1})",detail: "判斷內(nèi)容是url",type: "function",handle: (v1: any) => {if (typeof v1 !== "string") return false;return /^https?://(([a-zA-Z0-9_-]) (.)?)*(:d )?(/((.)?(?)?=?&?[a-zA-Z0-9_-](?)?)*)*$/i.test(v1);},},
組件值面板里存放的是組件運行時對外暴露的變量,有哪些變量可以使用也是物料中配置的,這個后面會細說。
// 獲取組件中暴露出來的值名稱和描述const { values } = ctx.material.getComponentMeta(node.componentName).getMetadata().configure.supports;
右側(cè)的編輯器是我以前使用codemirror6封裝的,倉庫地址為github.com/dbfu/bp-scr…,大家感興趣可以去看一下。最大的特點是把插入的變量當(dāng)成標(biāo)簽,不允許修改。
把編輯器中的腳本保存到屬性上,可以使用filed.setValue方法,為啥是這樣的數(shù)據(jù)格式,我后面再說,這里有個很大的坑。
fieldRef.current?.setValue({type: 'variable',value: '[[變量]]',script: scriptValue,});
自定義物料
如何自定義一個物料,官方文檔寫的很清楚了。我這里給它擴展了兩個屬性配置,一個對外暴露的方法描述,還有一個是對外暴露的變量值描述。
使用下面命令,可以快速創(chuàng)建一個插件
npm init @alilc/element your-material-name
這里選擇物料
然后自定義一個組件,我這里以Modal彈框組件為例。
import { ModalProps, Modal as OriginalModal } from 'antd';import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';const Modal: any = (props: ModalProps & { setCurPageValue: (fbn: Function) => void, __designMode: string }, ref) => { // setCurPageValue和__designMode是低代碼引擎注入進來屬性,// setCurPageValue可以把值設(shè)置到全局const { setCurPageValue, __designMode, ...rest } = props; const [open, setopen] = useState(false); useEffect(() => {// 把當(dāng)前open值暴露出去setCurPageValue((prev: any) => ({ ...prev, open }));}, [open]); // 對外暴露open和close方法useImperativeHandle(ref, () => ({open: () => {setOpen(true);},close: () => {setOpen(false);},}), []); const cancelHandle = (e) => {if (props.onCancel) {props?.onCancel(e);} else {setOpen(false);}}; const innerProps: any = {};if (__designMode === 'design') {// 低代碼編輯態(tài)中強制顯示,將控制權(quán)交給引擎?zhèn)萯nnerProps.open = true;}return <OriginalModal {...rest} open={open} {...innerProps} onCancel={cancelHandle} />;};export default forwardRef(Modal);
setCurPageValue是插件里給組件注入的一個方法,可以給組件里的變量暴露到全局。暴露open和close方法,也是為了在組件方法中去調(diào)用。
寫完組件后,執(zhí)行npm run lowcode:build命令,會在根目錄下生成一個lowcode文件夾,里面存放的是物料描述。找到modal的物料描述,把要對外暴露的方法和值手動配置進去,后面想辦法拓展一下官方的插件,自動掃描,不用自己手動配了。
這里配置變量name和方法name要和代碼里的一樣
自定義render
設(shè)計階段
這里我被卡了一段時間,原因是我自定義了變量綁定的腳本格式,如果按照官方把類型設(shè)置為JSExpression,設(shè)計階段會報錯,因為他會執(zhí)行里面的腳本,但是我們的腳本沒有按照官方的格式來,所以會報錯。
然后我就去翻源碼,看看有沒有方法繞過,然后就找到了這個代碼,組件在渲染前會格式化props。
我開始的想法是重寫里面的方法,參考了官方的這篇文檔,但是設(shè)計階段不允許自定義PageRenderer,然后繼續(xù)往下看源碼,看到了這段代碼。
沒想到這段兼容代碼幫了我的忙,我只需要給type設(shè)置為variable就行了,value為腳本。這樣做后我發(fā)現(xiàn)了一個新問題,如果給按鈕文本綁定變量,設(shè)計階段會把腳本當(dāng)成文本顯示。后來一想把value寫死成[[變量]],告訴這里是變量,新加一個script屬性存腳本。
預(yù)覽階段
前面都是關(guān)于配置的,具體怎么把配置轉(zhuǎn)換為可正常使用的功能,還是在這一步。
把配置渲染成功能,官方有一個封裝好的ReactRenderer組件,具體怎么使用可以看一下文檔。
雖然ReactRenderer組件拓展性很強,支持很多東西,比如createElement方法,但是不支持重寫__parseProps方法,不重寫__parseProps方法,我這里變量綁定的腳本運行就會有問題。
還好預(yù)覽階段可以自定義BaseRenderer,__parseProps就是BaseRenderer類里的一個方法,我們只需要寫一個類繼承BaseRenderer,然后只重寫__parseProps方法,但是需要自定義PageRenderer,所以官方的就不能用了,不過可以把官方代碼拿出來改一改就行了。
import ConfigProvider from '@alifd/next/lib/config-provider';import {adapter,addonRendererFactory,baseRendererFactory,blockRendererFactory,componentRendererFactory,pageRendererFactory,rendererFactory,tempRendererFactory,types,} from '@alilc/lowcode-renderer-core';import { isVariable } from '@alilc/lowcode-utils';import React, {Component,ContextType,PureComponent,ReactInstance,createContext,createElement,forwardRef,} from 'react';import ReactDOM from 'react-dom';window.React = React;(window as any).ReactDom = ReactDOM;adapter.setRuntime({Component,PureComponent,createContext,createElement,forwardRef,findDOMNode: ReactDOM.findDOMNode,});const BaseRenderer = baseRendererFactory();class CustomBaseRenderer extends BaseRenderer {constructor(props: any, context: any) {super(props, context); const parseProps = this.__parseProps;this.__parseProps = (props: any, self: any, path: string, info: any) => {// 這里判斷一下如果是變量類型,把type改成script,不然執(zhí)行base的__parseProps方法還是會問題,這個腳本后面在另外一個地方處理if (isVariable(props) as any) {return {type: 'script',value: props.value,script: props.script,} as any;}return parseProps(props, self, path, info);};}}// 把自定義的CustomBaseRenderer,設(shè)置進去。adapter.setRenderers({BaseRenderer: CustomBaseRenderer,} as any);adapter.setConfigProvider(ConfigProvider);const PageRenderer = pageRendererFactory();class CustomPageRenderer extends PageRenderer {constructor(props: any, context: any) {super(props, context);}}function factory(): types.IRenderComponent {adapter.setRenderers({BaseRenderer: CustomBaseRenderer,PageRenderer: CustomPageRenderer,ComponentRenderer: componentRendererFactory(),BlockRenderer: blockRendererFactory(),AddonRenderer: addonRendererFactory(),TempRenderer: tempRendererFactory(),DivRenderer: blockRendererFactory(),}); const Renderer = rendererFactory(); return class ReactRenderer extends Renderer implements Component {readonly props!: types.IRendererProps; context: ContextType<any>;setState!: (state: types.IRendererState, callback?: () => void) => void;forceUpdate!: (callback?: () => void) => void;refs!: {[key: string]: ReactInstance;}; constructor(props: types.IRendererProps, context: ContextType<any>) {super(props, context);} isValidComponent(obj: any) {return obj?.prototype?.isReactComponent || obj?.prototype instanceof Component;}};}export default factory();
這里重寫了BaseRenderer的__parseProps方法,把變量類型改了一下,后面在重寫createElement時再處理。
PageRenderer寫完后,我們下面去使用它
<ReactRenderclassName="lowcode-plugin-sample-preview-content"schema={schema}components={components}customCreateElement={(Component: any, props: any, children: any) => {// 給每個組件注入的上下文const ctx = {pageValue,setPageValue,getComponentRefs,}; // 當(dāng)組件配置了是否渲染為變量時,動態(tài)執(zhí)行腳本,如果腳本返回 false,則不渲染if (props?.__inner__?.condition && props?.__inner__?.condition?.type === 'variable') {if (!execScript(props?.__inner__?.condition?.script, ctx)) return ;} // 解析 propsconst newProps = parseProps(props, ctx); // 渲染組件return React.createElement(Component, newProps, newProps.children || children);}}onCompGetRef={(schema: any, ref: any) => {// 存儲每個組件的 ref實例componentRefs.current = {...componentRefs.current,[schema.id]: ref,}}}appHelper={{requestHandlersMap: {fetch: createFetchHandler()}}}/>
這里主要自定義了createElement方法,這樣我們可以在組件渲染前,更改props。
把組件實例存放到了componentRefs中,我們只要知道了組件id和方法,就可以通過componentRefs調(diào)用它的方法了。
看一下前面打開彈框的配置,知道了組件id,也知道是哪個方法,所以我們就可以調(diào)用彈框的打開方法了。
parseProps方法實現(xiàn),可以看一下代碼中的注釋
export const parseProps = (props: any, ctx: any) => { const { setPageValue } = ctx; const newProps: any = {// 給每個組件注入設(shè)置值的方法,讓它們把想要暴露出來的值設(shè)置到全局setCurPageValue: (fn: Function) => {setPageValue((prev: any) => ({...prev,[props.__id]: fn(prev[props.__id]),}))}}; Object.keys(props).forEach(key => {// 判斷是否是事件if (key.startsWith('on') && props[key]) {const eventConfig = props[key];newProps[key] = () => {const { type, value } = eventConfig || {};// 如果事件綁定的動作為流程,那么去執(zhí)行流程if (type === 'flow') {value.children && execEventFlow(value.children, ctx);}};} else if (typeof props[key] === 'object') {// 判斷是否是腳本if (props[key].type === 'script') {// 執(zhí)行腳本newProps[key] = execScript(props[key].script, ctx);} else {newProps[key] = props[key];}} else {newProps[key] = props[key];}}) return newProps;}
execScript方法,使用的是new Function方法動態(tài)執(zhí)行腳本。這里把存放組件暴露出來的值注入到了腳本的上下文中,所以腳本中可以直接獲取到某個組件暴露出來的值。
export function execScript(script: string, ctx: any) {const { pageValue } = ctx; if (!script) return; const result = script.replace(/[[(. ?)]]/g, (_: string, $2: string) => {const [fieldType, ...rest] = $2.split('.'); if (fieldType === 'C') {const keys = rest.map((t) => t.split(':')[1]);return `ctx.lodash.get(ctx.pageValue, "${keys.join('.')}")`;} return '';}); const func = new Function('ctx', 'func', `return ${result}`); const funcs = functions.reduce<any>((prev, cur) => {if (cur.handle) {prev[cur.label] = cur.handle;}return prev;}, {});const funcResult = func({pageValue,lodash,},funcs,);return funcResult;}
執(zhí)行事件綁定動作的方法,主要使用了遞歸。
import { message } from 'antd';import { execScript } from './exec-script';import { getPropValue } from './utils';const actions = [{name: 'openPage',label: '打開頁面',paramsSetter: [{name: 'url',label: 'url',type: 'input',required: true,}, {name: 'isNew',label: '新開窗口',type: 'switch',}],handler: (config: { url: string, isNew: boolean }) => {const { url, isNew = false } = config;window.open(url, isNew ? '_blank' : '_self');}},{name: 'showMessage',label: '顯示消息',paramsSetter: [{name: 'type',label: '消息類型',type: 'select',options: [{label: 'success',value: 'success',}, {label: 'error',value: 'error',}],defaultValue: 'success',required: true,}, {name: 'text',label: '消息內(nèi)容',type: 'input',required: true,}],handler: (config: { type: any, text: any }) => {const { type, text } = config; if (type === 'success' || type === 'error') {message[type as 'success' | 'error'](text);}}},];const actionMap = actions.reduce<any>((prev, cur) => {prev[cur.name] = cur.handler;return prev;}, {})async function componentMethod(actionConfig: any, ctx: any) {const componentRefs = ctx.getComponentRefs();if (!componentRefs[actionConfig.componentId]) {return Promise.reject();} // 拿到組件實例,執(zhí)行對應(yīng)的方法await componentRefs[actionConfig.componentId][actionConfig.method]();}export function execEventFlow(nodes: Node[] = [],ctx: any,) {if (!nodes.length) return; nodes.forEach(async (item: any) => {// 判斷是否是動作節(jié)點,如果是動作節(jié)點并且條件結(jié)果不為false,則執(zhí)行動作if (item.type === 'action' && item.conditionResult !== false) { const { config } = item?.config || {}; const newConfig: any = {}; Object.keys(config).forEach((key: any) => {newConfig[key] = getPropValue(config[key], ctx);}); try {if (item.config.type === 'ComponentMethod') {await componentMethod(config, ctx);} else {// 根據(jù)不同動作類型執(zhí)行不同動作await actionMap[item.config.type](newConfig,ctx,item,);} // 如果上面沒有拋出異常,執(zhí)行成功事件的后續(xù)腳本const children = item.children?.filter((o: any) => o.eventKey === 'success');execEventFlow(children, ctx);} catch {// 如果上面拋出異常,執(zhí)行失敗事件的后續(xù)腳本const children = item.children?.filter((o: any) => o.eventKey === 'error');execEventFlow(children, ctx);} finally {// 如果上面沒有拋出異常,執(zhí)行finally事件的后續(xù)腳本const children = item.children?.filter((o: any) => o.eventKey === 'finally');execEventFlow(children, ctx);}} else if (item.type === 'condition') {// 如果是條件節(jié)點,執(zhí)行條件腳本,把結(jié)果注入到子節(jié)點conditionResult屬性中const conditionResult = (item.config || []).reduce((prev: any, cur: any) => {const result = execScript(cur.condition, ctx);prev[cur.id] = result;return prev;},{}); (item.children || []).forEach((c: any) => {c.conditionResult = !!conditionResult[c.conditionId];});// 遞歸執(zhí)行子節(jié)點事件流execEventFlow(item.children, ctx);} else if (item.type === 'event') {// 如果是事件節(jié)點,執(zhí)行事件子節(jié)點事件流execEventFlow(item.children, ctx);}});}
到此整個插件核心功能介紹的差不多了,插件還沒成熟,就先不放出來了,等稍微成熟一點了,會給開源出來的。
我還在陸陸續(xù)續(xù)加一些功能,比如優(yōu)化接口調(diào)用方式,和后端表模型對接、通過AI快速開發(fā)界面等,如果有對這個插件開發(fā)感興趣的,可以在評論區(qū)留言討論。
總結(jié)
個人認為LowCodeEngine不一定是一個好的低代碼平臺,但是它絕對是一個非常強大的低代碼引擎,它定義了低代碼很多協(xié)議和規(guī)范,讓別人可以在它的基礎(chǔ)上快速孵化出一個符合自己產(chǎn)品的低代碼平臺。
作者:前端小付
鏈接:https://juejin.cn/post/7344941254236389403