diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ffb507 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ + +## 安装并开始 + +```dotnetcli +yarn +yarn setup +yarn start +``` + +本项目需要在 [低代码系统](https://github.com/react-low-code/vitis-system) 使用 + +## 写在后面 + +2020年初,为了加速重构遗留系统,我在任职的公司落地了低代码,当时的低代码及其简陋,不足为道,但派上了用场,我们只有5个前端工程师,在一个季度内,搭建了200+个页面。回忆起当时的方案,大概存在以下5个问题: +1. 搭建完成的应用无法独立部署 +2. 无渲染沙箱,当处于编辑态时,画布无纯净的运行环境 +3. 无组件市场,低代码设计器能使用的组件全部写死在项目内 +4. 用来描述低代码应用的Schema无版本管理,无法查看以前保存的版本。 +5. 开发人员无法对搭建完成的应用二次开发。 +2021 年底,机械工业出版社的图书编辑邀请我写一本与开发低代码相关的图书,今日,业已成书。本书一一解决了上述 5 个问题,同时涉及业务场景的需求分析,从开发技术层面来讲,读者将了解到下面这 5 个方面的内容: + +1)JSON Schema保存到Git仓库中,它不影响线上运行的低代码应用,只用于APP各版本的预览和重新编辑。 + +2)线上运行的应用与JSON Schema脱钩,即便低代码平台停止服务,线上的APP依然能正常运行。 + +3)引入渲染沙箱,设计器和渲染器位于不同的Frame,此时画布拥有纯净的运行环境。 + +4)设计组件规范,开发脚手架,其用于开发、调试和上传低代码组件,这使设计器能使用丰富的组件去开发应用,同时让低代码组件和低代码平台解耦。 + +5)开发低代码平台所需的基础设施,包括GitLab CI/CD、npm私有库,LDAP账号管理系统等。 + +本书将分为4大部分,其中第3部分介绍开发低代码平台涉及的各个方面,这部分难度最大。如果你是一名经验丰富的软件工程师并且对低代码已有了解,建议从第4章开始;但是,如果你对低代码了解得不多,请一定从第一章的基础理论知识开始学习。 + +第一部分是基础篇,只包含一章,它介绍后续章节使用的理论知识,涉及的知识点有React Context API、React Hooks、React Ref、Mobx和MongoDB等,要想在本地运行本图书介绍的低代码平台,你需要在自己电脑上下载MongoDB。 + +第二部分为需求分析篇,包含两章,它介绍低代码平台开发的应用要满足哪些需求,同时也介绍低代码平台的功能。 + +第三部分为实战篇,包含五章,是本图书的重点,介绍如何开发低代码平台,其中展示了大量的代码示例,涉及的内容有低代码架构策略、低代码组件、设计器、渲染器和代码生成器。 + +第四部分为基础设施篇,只包含一章。低代码平台用于创建应用程序,它本身也是应用程序,值得一提的是,它对研发体系的要求相当高。如果你手上没有一套完善的研发体系,涵盖代码托管、CI/CD、CDN,npm私有库等部分,那么妄谈开发低代码平台。基础设施篇涉及的内容有,如何使用GitLab CI/CD建立持续部署 pipeline、如何搭建npm私有库,如何搭建LDAP账号管理系统等。 +本图书提供了7个开源项目,全部源文件可以从https://github.com/react-low-code下载。 + +对《低代码平台开发实践:基于React》感兴趣的朋友可在[京东购买](https://item.jd.com/14012127.html),也可通过我的微信微信公众号——前端知识小站联系到我。 + +![](./WechatIMG78.jpg) diff --git a/WechatIMG78.jpg b/WechatIMG78.jpg new file mode 100644 index 0000000..9b1f1af Binary files /dev/null and b/WechatIMG78.jpg differ diff --git a/packages/default-ext/src/plugin/ComponentsPane/component.tsx b/packages/default-ext/src/plugin/ComponentsPane/component.tsx index 20f34d4..2af870d 100644 --- a/packages/default-ext/src/plugin/ComponentsPane/component.tsx +++ b/packages/default-ext/src/plugin/ComponentsPane/component.tsx @@ -148,7 +148,7 @@ export default class ComponentsPane extends React.Component<{},State>{ onOpenChange={this.onOpenChange} open={this.state.active} > - + } diff --git a/packages/default-ext/src/plugin/ComponentsPane/icon.tsx b/packages/default-ext/src/plugin/ComponentsPane/icon.tsx deleted file mode 100644 index 9355c90..0000000 --- a/packages/default-ext/src/plugin/ComponentsPane/icon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; - -export default function SvgComponent({ className, onClick }: {className?: string, onClick?: () => void}) { - return ( - - - - - - - - - ); -} diff --git a/packages/default-ext/src/plugin/LifeCyclesPane/component.tsx b/packages/default-ext/src/plugin/LifeCyclesPane/component.tsx index a5b6d4e..980d477 100644 --- a/packages/default-ext/src/plugin/LifeCyclesPane/component.tsx +++ b/packages/default-ext/src/plugin/LifeCyclesPane/component.tsx @@ -86,7 +86,7 @@ export default function () { onOpenChange={onOpenChange} open={active} > - + ) diff --git a/packages/default-ext/src/plugin/NetworkInterceptorsPane/component.tsx b/packages/default-ext/src/plugin/NetworkInterceptorsPane/component.tsx index 98f1da6..1122eb2 100644 --- a/packages/default-ext/src/plugin/NetworkInterceptorsPane/component.tsx +++ b/packages/default-ext/src/plugin/NetworkInterceptorsPane/component.tsx @@ -120,7 +120,7 @@ export default function () { onOpenChange={onOpenChange} open={active} > - + ) diff --git a/packages/default-ext/src/plugin/NetworkInterceptorsPane/index.less b/packages/default-ext/src/plugin/NetworkInterceptorsPane/index.less index 543684e..e2c19a3 100644 --- a/packages/default-ext/src/plugin/NetworkInterceptorsPane/index.less +++ b/packages/default-ext/src/plugin/NetworkInterceptorsPane/index.less @@ -1,6 +1,5 @@ .NetworkInterceptorsPane { .icon { - color: rgba(0, 0, 0, 0.26); font-size: 22px; cursor: pointer; diff --git a/packages/default-ext/src/plugin/PromptPane/component.tsx b/packages/default-ext/src/plugin/PromptPane/component.tsx new file mode 100644 index 0000000..33fa50c --- /dev/null +++ b/packages/default-ext/src/plugin/PromptPane/component.tsx @@ -0,0 +1,74 @@ +import { Popover, Input, Button } from 'antd'; +import React, { useState,useLayoutEffect, useEffect } from "react" +import { OpenAIOutlined } from '@ant-design/icons'; + +const BASE_URL = 'http://127.0.0.1:3001' + +export default function PromptPane() { + const [active, setActive] = useState(false); + const [height, setHeight] = useState(0); + const [keyword,setKeyword] = useState('') + const [loading, setLoading] = useState(false); + + useLayoutEffect(() => { + setHeight(document.body.clientHeight - 130) + },[]) + + const onOpenChange = () => { + setActive(!active) + } + + const onChange = (e: React.ChangeEvent) => { + setKeyword(e.target.value); + }; + + const onOK = () => { + if (!keyword) { + return ; + } + setLoading(true); + fetch(BASE_URL+'/prompt/generate/schema', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + prompt: keyword + }) + }) + .then(async (response) => { + if (!response.ok) { + throw new Error('网络请求失败: ' + response.status); + } + const res: { + data: any; + code: string; + msg: string; + } = await response.json() + console.log(window.VitisLowCodeEngine.project,'window.VitisLowCodeEngine.project') + if (window.VitisLowCodeEngine.project) { + window.VitisLowCodeEngine.project.insertSchema(res.data); + } + }) + .finally(() => { + setLoading(false); + }) + } + + return ( + + + + + } + onOpenChange={onOpenChange} + open={active} + > + + + ) +} diff --git a/packages/default-ext/src/plugin/PromptPane/index.tsx b/packages/default-ext/src/plugin/PromptPane/index.tsx new file mode 100644 index 0000000..254ed73 --- /dev/null +++ b/packages/default-ext/src/plugin/PromptPane/index.tsx @@ -0,0 +1,18 @@ +import { PluginContext } from 'vitis-lowcode-types' +import Pane from './component' + +function PromptPane(ctx: PluginContext) { + return { + init() { + ctx.skeleton.add({ + name: "PromptPane", + content: Pane, + area: "left" + }) + } + } +} + +PromptPane.pluginName = 'PromptPane' + +export default PromptPane \ No newline at end of file diff --git a/packages/default-ext/src/plugin/SchemaPane/component.tsx b/packages/default-ext/src/plugin/SchemaPane/component.tsx index 21638ce..f2ca84d 100644 --- a/packages/default-ext/src/plugin/SchemaPane/component.tsx +++ b/packages/default-ext/src/plugin/SchemaPane/component.tsx @@ -50,7 +50,7 @@ export default function() { onOpenChange={onOpenChange} open={active} > - + diff --git a/packages/default-ext/src/plugin/index.ts b/packages/default-ext/src/plugin/index.ts index 73404db..2ad3a4d 100644 --- a/packages/default-ext/src/plugin/index.ts +++ b/packages/default-ext/src/plugin/index.ts @@ -2,10 +2,12 @@ import ComponentsPanePlugin from './ComponentsPane' import LifeCyclesPane from './LifeCyclesPane' import NetworkInterceptorsPane from './NetworkInterceptorsPane' import SchemaPane from './SchemaPane' +import PromptPane from './PromptPane'; export const defaultPlugins = [ ComponentsPanePlugin, LifeCyclesPane, NetworkInterceptorsPane, - SchemaPane + SchemaPane, + PromptPane, ] \ No newline at end of file diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index d6b44b7..b38313b 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -2,6 +2,7 @@ import { createElement } from 'react' import { render } from 'react-dom' import { defaultPlugins, defaultSetters } from 'vitis-lowcode-default-ext' import { PageSchema } from 'vitis-lowcode-types' +import { fetchSchema } from './servers/ai'; import { plugins, setters, material } from './shell' import Workbench from './layout/workbench' @@ -43,6 +44,10 @@ export function init(container?: HTMLElement, options: EngineOptions = {}) { if (options.pageSchema) { observableProject.setSchema(options.pageSchema) } + + // fetchSchema().then((schema) => { + // observableProject.documentModel.insertSchema(schema); + // }) render(createElement(Root),container) } \ No newline at end of file diff --git a/packages/engine/src/node/index.ts b/packages/engine/src/node/index.ts index ca461a1..98477dd 100644 --- a/packages/engine/src/node/index.ts +++ b/packages/engine/src/node/index.ts @@ -59,7 +59,7 @@ export default class Node { this.isFormControl = !!initSchema.isFormControl this.owner = owner - this.children = initSchema.children.map(child => this.owner.createNode(child, this)) + this.children = initSchema.children?.map(child => this.owner.createNode(child, this)) || [] this.props = new Props(this, initSchema.props) this.extraProps = new Props(this, initSchema.extraProps) } diff --git a/packages/engine/src/project/documentModel.ts b/packages/engine/src/project/documentModel.ts index 9956e13..c45dbb0 100644 --- a/packages/engine/src/project/documentModel.ts +++ b/packages/engine/src/project/documentModel.ts @@ -1,4 +1,4 @@ -import { PageSchema, NodeSchema, LifeCycles, JSFunction, Interceptors } from 'vitis-lowcode-types' +import { PageSchema, NodeSchema, LifeCycles, JSFunction, Interceptors, ContainerSchema } from 'vitis-lowcode-types' import { makeAutoObservable } from 'mobx' import Node from '../node' import type Project from './index' @@ -72,6 +72,18 @@ export default class DocumentModel { this.hoveredNodeId = id } + insertSchema(schemas: NodeSchema[], parentNode: Node = this.rootNode) { + const insert = (schemas: NodeSchema[], parentNode: Node) => { + schemas.forEach((schema, index) => { + const newNode = this.createNode(schema, parentNode); + parentNode.inertChildAtIndex(newNode, parentNode.childrenSize + index); + }); + } + + insert(schemas, parentNode) + this.project.renderer?.rerender(); + } + updateLifeCycles(name: keyof LifeCycles, value: JSFunction) { this.lifeCycles[name] = value } diff --git a/packages/engine/src/project/host.ts b/packages/engine/src/project/host.ts index fcfb8d8..97032e1 100644 --- a/packages/engine/src/project/host.ts +++ b/packages/engine/src/project/host.ts @@ -14,7 +14,6 @@ export default class Host implements HostSpec { constructor(project: Project) { this.project = project - } onAssetUpdated = async (additionalPackageNames: string[]) => { @@ -62,6 +61,7 @@ export default class Host implements HostSpec { this.renderer = await this.createSimulator() + this.project.setRenderer(this.renderer); material.off(ASSET_UPDATED, this.onAssetUpdated).on(ASSET_UPDATED, this.onAssetUpdated) this.setupEvent() diff --git a/packages/engine/src/project/index.ts b/packages/engine/src/project/index.ts index fd39a1e..346a2ca 100644 --- a/packages/engine/src/project/index.ts +++ b/packages/engine/src/project/index.ts @@ -1,7 +1,7 @@ import { makeAutoObservable } from 'mobx' import Designer from './designer'; import DocumentModel from './documentModel' -import { PageSchema, ObservableProjectSpec, LifeCycles, JSFunction, Interceptors } from 'vitis-lowcode-types' +import { PageSchema, ObservableProjectSpec, LifeCycles, JSFunction, Interceptors, SimulatorSpec } from 'vitis-lowcode-types' const defaultPageSchema: PageSchema = { componentName: 'Page', @@ -75,6 +75,7 @@ const defaultPageSchema: PageSchema = { export default class Project implements ObservableProjectSpec{ readonly designer = new Designer(this) readonly documentModel: DocumentModel + renderer: SimulatorSpec get schema() { return this.documentModel.schema as PageSchema @@ -97,6 +98,10 @@ export default class Project implements ObservableProjectSpec{ this.documentModel.open(schema) } + setRenderer(renderer: SimulatorSpec) { + this.renderer = renderer; + } + updateLifeCycles = (name: keyof LifeCycles, value: JSFunction) => { this.documentModel.updateLifeCycles(name, value) } diff --git a/packages/engine/src/servers/ai.ts b/packages/engine/src/servers/ai.ts new file mode 100644 index 0000000..0389522 --- /dev/null +++ b/packages/engine/src/servers/ai.ts @@ -0,0 +1,26 @@ +import { BASE_URL } from './config'; + +export async function fetchSchema () { + return fetch(BASE_URL+'/prompt/generate/schema', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + // 在这里添加你要发送的请求数据 + prompt: '生成一个包含用户名和密码的表单' + }) + }) + .then(async (response) => { + if (!response.ok) { + throw new Error('网络请求失败: ' + response.status); + } + const res: { + data: any; + code: string; + msg: string; + } = await response.json() + + return res.data; + }) +} \ No newline at end of file diff --git a/packages/engine/src/servers/config.ts b/packages/engine/src/servers/config.ts new file mode 100644 index 0000000..ed750e4 --- /dev/null +++ b/packages/engine/src/servers/config.ts @@ -0,0 +1 @@ +export const BASE_URL = 'http://127.0.0.1:3001' \ No newline at end of file diff --git a/packages/engine/src/shell/project.ts b/packages/engine/src/shell/project.ts index 78a5d1e..65e4baf 100644 --- a/packages/engine/src/shell/project.ts +++ b/packages/engine/src/shell/project.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'eventemitter3'; import type InterProject from '../project'; -import { ProjectSpec, LifeCycles, JSFunction, Interceptors } from 'vitis-lowcode-types' +import { ProjectSpec, LifeCycles, JSFunction, Interceptors, NodeSchema } from 'vitis-lowcode-types' export default class Project extends EventEmitter implements ProjectSpec { private readonly project: InterProject @@ -28,4 +28,8 @@ export default class Project extends EventEmitter implements ProjectSpec { getSchema() { return this.project.schema } + + insertSchema(schemas: NodeSchema[]) { + this.project.documentModel.insertSchema(schemas); + } } \ No newline at end of file diff --git a/packages/types/src/project.ts b/packages/types/src/project.ts index 632e4e7..272c66a 100644 --- a/packages/types/src/project.ts +++ b/packages/types/src/project.ts @@ -1,6 +1,7 @@ import type EventEmitter from 'eventemitter3'; import { ElementType } from 'react' import { PageSchema, LifeCycles, JSFunction, Interceptors } from './schema' +import { NodeSchema } from 'vitis-lowcode-types' export interface ProjectSpec extends EventEmitter { updateLifeCycles(name: keyof LifeCycles, value: JSFunction): void @@ -8,6 +9,7 @@ export interface ProjectSpec extends EventEmitter { getInterceptors(): Interceptors | undefined updateInterceptors(name: keyof Interceptors, value: JSFunction): void getSchema(): PageSchema + insertSchema(schemas: NodeSchema[]): void; } export interface ObservableProjectSpec {