From fb5de6441dc9ba22eb6cbd96294dfc7e7be4ffb5 Mon Sep 17 00:00:00 2001 From: 1ncounter <1ncounter.100@gmail.com> Date: Tue, 19 Mar 2024 10:47:13 +0800 Subject: [PATCH] feat: add renderer-core codes --- docs/docs/specs/lowcode-spec.md | 772 +++++++++--------- package.json | 1 + packages/types/package.json | 2 +- packages/types/src/shell/type/context-menu.ts | 16 +- .../shell/type/{i8n-data.ts => i18n-data.ts} | 0 packages/types/src/shell/type/index.ts | 4 +- packages/types/src/shell/type/npm.ts | 4 +- rollup.config.mjs | 7 + runtime/react-renderer/package.json | 11 - runtime/react-renderer/src/api/create-app.tsx | 65 -- runtime/react-renderer/tsconfig.json | 3 - runtime/react-renderer/vitest.config.ts | 0 runtime/renderer-core/package.json | 6 + .../api/{create-app-function.ts => app.ts} | 60 +- ...ate-component-function.ts => component.ts} | 119 +-- runtime/renderer-core/src/boosts.ts | 24 +- runtime/renderer-core/src/code-runtime.ts | 53 +- runtime/renderer-core/src/container.ts | 166 ++++ runtime/renderer-core/src/index.ts | 15 + runtime/renderer-core/src/package.ts | 52 +- runtime/renderer-core/src/plugin.ts | 35 +- runtime/renderer-core/src/schema.ts | 52 +- runtime/renderer-core/src/types/common.ts | 3 + runtime/renderer-core/src/types/index.ts | 5 + runtime/renderer-core/src/types/material.ts | 14 + .../src/types/specs/asset-spec.ts | 104 +++ .../src/types/specs/lowcode-spec.ts | 345 ++++++++ .../src/types/specs/runtime-api.ts | 75 ++ .../renderer-core/src/{ => utils}/error.ts | 9 +- runtime/renderer-core/src/utils/hook.ts | 115 ++- .../src/utils/non-setter-proxy.ts | 13 + runtime/renderer-core/src/utils/type-guard.ts | 18 + runtime/renderer-core/src/validate/schema.ts | 11 - runtime/renderer-core/src/widget.ts | 92 +++ runtime/renderer-core/tests/api/app.spec.ts | 50 ++ .../renderer-core/tests/api/component.spec.ts | 5 + runtime/renderer-core/tests/boosts.spec.ts | 17 + .../renderer-core/tests/code-runtime.spec.ts | 1 + runtime/renderer-core/tests/package.spec.ts | 2 + runtime/renderer-core/tests/plugin.spec.ts | 12 + .../renderer-core/tests/utils/hook.spec.ts | 169 ++++ .../tests/utils/non-setter-proxy.spec.ts | 19 + runtime/renderer-core/vitest.config.ts | 7 + runtime/renderer-react/package.json | 33 + runtime/renderer-react/src/api/create-app.tsx | 62 ++ .../src/api/create-component.tsx | 99 +-- .../src/components/app.tsx | 0 .../src/components/outlet.tsx | 0 .../src/components/route.tsx | 0 .../src/components/router-view.tsx | 0 runtime/renderer-react/src/context/app.ts | 13 + runtime/renderer-react/src/context/router.ts | 33 + .../src/index.ts | 0 .../src/plugins/intl/index.ts | 6 +- .../src/plugins/intl/intl.tsx | 0 .../src/plugins/intl/parser.ts | 19 +- .../src/plugins/utils/index.ts | 0 .../src/renderer.ts | 0 .../src/router.ts | 12 +- .../src/signals.ts | 21 +- .../src/utils/element.ts | 33 +- .../src/utils/reactive.tsx | 0 runtime/renderer-react/tsconfig.json | 8 + runtime/renderer-react/vitest.config.ts | 8 + runtime/router/package.json | 12 +- runtime/router/tsconfig.json | 7 +- 66 files changed, 2001 insertions(+), 918 deletions(-) rename packages/types/src/shell/type/{i8n-data.ts => i18n-data.ts} (100%) create mode 100644 rollup.config.mjs delete mode 100644 runtime/react-renderer/package.json delete mode 100644 runtime/react-renderer/src/api/create-app.tsx delete mode 100644 runtime/react-renderer/tsconfig.json delete mode 100644 runtime/react-renderer/vitest.config.ts rename runtime/renderer-core/src/api/{create-app-function.ts => app.ts} (54%) rename runtime/renderer-core/src/api/{create-component-function.ts => component.ts} (56%) create mode 100644 runtime/renderer-core/src/container.ts create mode 100644 runtime/renderer-core/src/types/common.ts create mode 100644 runtime/renderer-core/src/types/index.ts create mode 100644 runtime/renderer-core/src/types/material.ts create mode 100644 runtime/renderer-core/src/types/specs/asset-spec.ts create mode 100644 runtime/renderer-core/src/types/specs/lowcode-spec.ts create mode 100644 runtime/renderer-core/src/types/specs/runtime-api.ts rename runtime/renderer-core/src/{ => utils}/error.ts (58%) create mode 100644 runtime/renderer-core/src/utils/non-setter-proxy.ts create mode 100644 runtime/renderer-core/src/utils/type-guard.ts delete mode 100644 runtime/renderer-core/src/validate/schema.ts create mode 100644 runtime/renderer-core/src/widget.ts create mode 100644 runtime/renderer-core/tests/api/app.spec.ts create mode 100644 runtime/renderer-core/tests/api/component.spec.ts create mode 100644 runtime/renderer-core/tests/boosts.spec.ts create mode 100644 runtime/renderer-core/tests/code-runtime.spec.ts create mode 100644 runtime/renderer-core/tests/package.spec.ts create mode 100644 runtime/renderer-core/tests/plugin.spec.ts create mode 100644 runtime/renderer-core/tests/utils/hook.spec.ts create mode 100644 runtime/renderer-core/tests/utils/non-setter-proxy.spec.ts create mode 100644 runtime/renderer-react/package.json create mode 100644 runtime/renderer-react/src/api/create-app.tsx rename runtime/{react-renderer => renderer-react}/src/api/create-component.tsx (82%) rename runtime/{react-renderer => renderer-react}/src/components/app.tsx (100%) rename runtime/{react-renderer => renderer-react}/src/components/outlet.tsx (100%) rename runtime/{react-renderer => renderer-react}/src/components/route.tsx (100%) rename runtime/{react-renderer => renderer-react}/src/components/router-view.tsx (100%) create mode 100644 runtime/renderer-react/src/context/app.ts create mode 100644 runtime/renderer-react/src/context/router.ts rename runtime/{react-renderer => renderer-react}/src/index.ts (100%) rename runtime/{react-renderer => renderer-react}/src/plugins/intl/index.ts (93%) rename runtime/{react-renderer => renderer-react}/src/plugins/intl/intl.tsx (100%) rename runtime/{react-renderer => renderer-react}/src/plugins/intl/parser.ts (86%) rename runtime/{react-renderer => renderer-react}/src/plugins/utils/index.ts (100%) rename runtime/{react-renderer => renderer-react}/src/renderer.ts (100%) rename runtime/{react-renderer => renderer-react}/src/router.ts (74%) rename runtime/{react-renderer => renderer-react}/src/signals.ts (88%) rename runtime/{react-renderer => renderer-react}/src/utils/element.ts (80%) rename runtime/{react-renderer => renderer-react}/src/utils/reactive.tsx (100%) create mode 100644 runtime/renderer-react/tsconfig.json create mode 100644 runtime/renderer-react/vitest.config.ts diff --git a/docs/docs/specs/lowcode-spec.md b/docs/docs/specs/lowcode-spec.md index 480698354..5e2755ff7 100644 --- a/docs/docs/specs/lowcode-spec.md +++ b/docs/docs/specs/lowcode-spec.md @@ -16,16 +16,14 @@ sidebar_position: 0 - 定义搭建基础协议国际化多语言支持规范(AA) - 定义搭建基础协议无障碍访问规范(AAA) - ### 1.2 协议草案起草人 - 撰写:月飞、康为、林熠 - 审阅:大果、潕量、九神、元彦、戊子、屹凡、金禅、前道、天晟、游鹿、光弘、力皓 - ### 1.3 版本号 -1.1.0 +1.2.0 ### 1.4 协议版本号规范(A) @@ -35,7 +33,6 @@ sidebar_position: 0 - minor 是小版本号:用于发布向下兼容的协议功能新增 - patch 是补丁号:用于发布向下兼容的协议问题修正 - ### 1.5 协议中子规范 Level 定义 | 规范等级 | 实现要求 | @@ -44,7 +41,6 @@ sidebar_position: 0 | AA | 推荐规范,推荐实现;遵守此类规范有助于业务未来的扩展性和跨团队合作研发效率的提升。 | | AAA | 参考规范,根据业务场景实际诉求实现;是集团层面鼓励的技术实现引导。 | - ### 1.6 名词术语 #### 1.6.1 物料系统名词 @@ -58,13 +54,12 @@ sidebar_position: 0 - **页面(Page)**:由组件 + 区块组合而成。由页面容器组件包裹,可描述页面级的状态管理和公共函数。 - **模板(Template)**:特定垂直业务领域内的业务组件、区块可组合为单个页面,或者是再配合路由组合为多个页面集,统称为模板。 - #### 1.6.2 低代码搭建系统名词 - **搭建编辑器**:使用可视化的方式实现页面搭建,支持组件 UI 编排、属性编辑、事件绑定、数据绑定,最终产出符合搭建基础协议规范的数据。 - - **属性面板**:低代码编辑器内部用于组件、区块、页面的属性编辑、事件绑定、数据绑定的操作面板。 - - **画布面板**:低代码编辑器内部用于 UI 编排的操作面板。 - - **大纲面板**:低代码编辑器内部用于页面组件树展示的面板。 + - **属性面板**:低代码编辑器内部用于组件、区块、页面的属性编辑、事件绑定、数据绑定的操作面板。 + - **画布面板**:低代码编辑器内部用于 UI 编排的操作面板。 + - **大纲面板**:低代码编辑器内部用于页面组件树展示的面板。 - **编辑器框架**:搭建编辑器的基础框架,包含主题配置机制、插件机制、setter 控件机制、快捷键管理、扩展点管理等底层基础设施。 - **入料模块**:专注于物料接入,能自动扫描、解析源码组件,并最终产出一份符合《低代码引擎物料协议规范》的 Schema JSON。 - **编排模块**:专注于 Schema 可视化编排,以可视化的交互方式提供页面结构编排服务,并最终产出一份符合《低代码搭建基础协议规范》的 Schema JSON。 @@ -76,7 +71,7 @@ sidebar_position: 0 ### 1.7 背景 -- **协议目标**:通过约束低代码引擎的搭建协议规范,让上层低代码编辑器的产出物(低代码业务组件、区块、应用)保持一致性,可跨低代码研发平台进行流通而提效,亦不阻碍集团业务间融合的发展。  +- **协议目标**:通过约束低代码引擎的搭建协议规范,让上层低代码编辑器的产出物(低代码业务组件、区块、应用)保持一致性,可跨低代码研发平台进行流通而提效,亦不阻碍集团业务间融合的发展。 - **协议通**: - 协议顶层结构统一 - 协议 schema 具备有完整的描述能力,包含版本、国际化、组件树、组件映射关系等; @@ -109,7 +104,6 @@ sidebar_position: 0 - **面向多端**:不能仅面向 React,还有小程序等多端。 - **支持国际化&无障碍访问标准的实现** - ## 2 协议结构 协议最顶层结构如下: @@ -276,12 +270,10 @@ sidebar_position: 0 定义当前协议 schema 的版本号,不同的版本号对应不同的渲染 SDK,以保障不同版本搭建协议产物的正常渲染; - | 根属性名称 | 类型 | 说明 | 变量支持 | 默认值 | | ---------- | ------ | ---------- | -------- | ------ | | version | String | 协议版本号 | - | 1.0.0 | - 描述示例: ```javascript @@ -294,65 +286,70 @@ sidebar_position: 0 协议中用于描述 componentName 到公域组件映射关系的规范。 - -| 参数 | 说明 | 类型 | 变量支持 | 默认值 | -| --------------- | ---------------------- | ------------------------- | -------- | ------ | +| 参数 | 说明 | 类型 | 变量支持 | 默认值 | +| --------------- | ---------------------- | ------------------ | -------- | ------ | | componentsMap[] | 描述组件映射关系的集合 | **ComponentMap**[] | - | null | **ComponentMap 结构描述**如下: -| 参数 | 说明 | 类型 | 变量支持 | 默认值 | -| ------------- | ------------------------------------------------------------------------------------------------------ | ------- | -------- | ------ | +| 参数 | 说明 | 类型 | 变量支持 | 默认值 | +| ------------- | ------------------------------------------------------------------------------------------ | ------- | -------- | ------ | | componentName | 协议中的组件名,唯一性,对应包导出的组件名,是一个有效的 **JS 标识符**,而且是大写字母打头 | String | - | - | -| package | npm 公域的 package name | String | - | - | -| version | package version | String | - | - | -| destructuring | 使用解构方式对模块进行导出 | Boolean | - | - | -| exportName | 包导出的组件名 | String | - | - | -| subName | 下标子组件名称 | String | - | | -| main | 包导出组件入口文件路径 | String | - | - | - +| package | npm 公域的 package name | String | - | - | +| version | package version | String | - | - | +| destructuring | 使用解构方式对模块进行导出 | Boolean | - | - | +| exportName | 包导出的组件名 | String | - | - | +| subName | 下标子组件名称 | String | - | | +| main | 包导出组件入口文件路径 | String | - | - | 描述示例: ```json { - "componentsMap": [{ - "componentName": "Button", - "package": "@alifd/next", - "version": "1.0.0", - "destructuring": true - }, { - "componentName": "MySelect", - "package": "@alifd/next", - "version": "1.0.0", - "destructuring": true, - "exportName": "Select" - }, { - "componentName": "ButtonGroup", - "package": "@alifd/next", - "version": "1.0.0", - "destructuring": true, - "exportName": "Button", - "subName": "Group" - }, { - "componentName": "RadioGroup", - "package": "@alifd/next", - "version": "1.0.0", - "destructuring": true, - "exportName": "Radio", - "subName": "Group" - }, { - "componentName": "CustomCard", - "package": "@ali/custom-card", - "version": "1.0.0" - }, { - "componentName": "CustomInput", - "package": "@ali/custom", - "version": "1.0.0", - "main": "/lib/input", - "destructuring": true, - "exportName": "Input" - }] + "componentsMap": [ + { + "componentName": "Button", + "package": "@alifd/next", + "version": "1.0.0", + "destructuring": true + }, + { + "componentName": "MySelect", + "package": "@alifd/next", + "version": "1.0.0", + "destructuring": true, + "exportName": "Select" + }, + { + "componentName": "ButtonGroup", + "package": "@alifd/next", + "version": "1.0.0", + "destructuring": true, + "exportName": "Button", + "subName": "Group" + }, + { + "componentName": "RadioGroup", + "package": "@alifd/next", + "version": "1.0.0", + "destructuring": true, + "exportName": "Radio", + "subName": "Group" + }, + { + "componentName": "CustomCard", + "package": "@ali/custom-card", + "version": "1.0.0" + }, + { + "componentName": "CustomInput", + "package": "@ali/custom", + "version": "1.0.0", + "main": "/lib/input", + "destructuring": true, + "exportName": "Input" + } + ] } ``` @@ -377,13 +374,10 @@ import CustomCard from '@ali/custom-card'; // 使用特定路径进行导出 import { Input as CustomInput } from '@ali/custom/lib/input'; - ``` - ### 2.3 组件树描述(A) - 协议中用于描述搭建出来的组件树结构的规范,整个组件树的描述由**组件结构**&**容器结构**两种结构嵌套构成。 - 组件结构:描述单个组件的名称、属性、子集的结构; @@ -402,14 +396,13 @@ import { Input as CustomInput } from '@ali/custom/lib/input'; ##### 2.3.1.1 Props 结构描述 -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | -| ----------- | ------------ | ------ | -------- | ------ | ------------------------------------- | -| id | 组件 ID | String | ✅ | - | 系统属性 | -| className | 组件样式类名 | String | ✅ | - | 系统属性,支持变量表达式 | -| style | 组件内联样式 | Object | ✅ | - | 系统属性,单个内联样式属性值 | -| ref | 组件 ref 名称 | String | ✅ | - | 可通过 `this.$(ref)` 获取组件实例 | -| extendProps | 组件继承属性 | 变量 | ✅ | - | 仅支持变量绑定,常用于继承属性对象 | -| ... | 组件私有属性 | - | - | - | | +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | +| --------- | ------------- | ------ | -------- | ------ | --------------------------------- | +| id | 组件 ID | String | ✅ | - | 系统属性 | +| className | 组件样式类名 | String | ✅ | - | 系统属性,支持变量表达式 | +| style | 组件内联样式 | Object | ✅ | - | 系统属性,单个内联样式属性值 | +| ref | 组件 ref 名称 | String | ✅ | - | 可通过 `this.$(ref)` 获取组件实例 | +| ... | 组件私有属性 | - | - | - | | ##### 2.3.1.2 css/less/scss 样式描述 @@ -427,25 +420,25 @@ import { Input as CustomInput } from '@ali/custom/lib/input'; ##### 2.3.1.3 ComponentDataSource 对象描述 -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | -| ----------- | ---------------------- | -------------------------------------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------- | -| list[] | 数据源列表 | **ComponentDataSourceItem**[] | - | - | 成为为单个请求配置, 内容定义详见 [ComponentDataSourceItem 对象描述](#2314-componentdatasourceitem-对象描述) | -| dataHandler | 所有请求数据的处理函数 | Function | - | - | 详见 [dataHandler Function 描述](#2317-datahandler-function-描述) | +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | +| ----------- | ---------------------- | ----------------------------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------- | +| list[] | 数据源列表 | **ComponentDataSourceItem**[] | - | - | 成为为单个请求配置, 内容定义详见 [ComponentDataSourceItem 对象描述](#2314-componentdatasourceitem-对象描述) | +| dataHandler | 所有请求数据的处理函数 | Function | - | - | 详见 [dataHandler Function 描述](#2317-datahandler-function-描述) | ##### 2.3.1.4 ComponentDataSourceItem 对象描述 | 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | -| -------------- | ---------------------------- | ---------------------------------------------------- | -------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | 数据请求 ID 标识 | String | - | - | | -| isInit | 是否为初始数据 | Boolean | ✅ | true | 值为 true 时,将在组件初始化渲染时自动发送当前数据请求 | -| isSync | 是否需要串行执行 | Boolean | ✅ | false | 值为 true 时,当前请求将被串行执行 | +| -------------- | ---------------------------- | ---------------------------------------------------- | -------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| id | 数据请求 ID 标识 | String | - | - | | +| isInit | 是否为初始数据 | Boolean | ✅ | true | 值为 true 时,将在组件初始化渲染时自动发送当前数据请求 | +| isSync | 是否需要串行执行 | Boolean | ✅ | false | 值为 true 时,当前请求将被串行执行 | | type | 数据请求类型 | String | - | fetch | 支持四种类型:fetch/mtop/jsonp/custom | -| shouldFetch | 本次请求是否可以正常请求 | (options: ComponentDataSourceItemOptions) => boolean | - | ```() => true``` | function 参数参考 [ComponentDataSourceItemOptions 对象描述](#2315-componentdatasourceitemoptions-对象描述) | +| shouldFetch | 本次请求是否可以正常请求 | (options: ComponentDataSourceItemOptions) => boolean | - | `() => true` | function 参数参考 [ComponentDataSourceItemOptions 对象描述](#2315-componentdatasourceitemoptions-对象描述) | | willFetch | 单个数据结果请求参数处理函数 | Function | - | options => options | 只接受一个参数(options),返回值作为请求的 options,当处理异常时,使用原 options。也可以返回一个 Promise,resolve 的值作为请求的 options,reject 时,使用原 options | -| requestHandler | 自定义扩展的外部请求处理器 | Function | - | - | 仅 type='custom' 时生效 | -| dataHandler | request 成功后的回调函数 | Function | - | `response => response.data`| 参数:请求成功后 promise 的 value 值 || +| requestHandler | 自定义扩展的外部请求处理器 | Function | - | - | 仅 type='custom' 时生效 | +| dataHandler | request 成功后的回调函数 | Function | - | `response => response.data` | 参数:请求成功后 promise 的 value 值 | | | errorHandler | request 失败后的回调函数 | Function | - | - | 参数:请求出错 promise 的 error 内容 | -| options {} | 请求参数 | **ComponentDataSourceItemOptions**| - | - | 每种请求类型对应不同参数,详见 | 每种请求类型对应不同参数,详见 [ComponentDataSourceItemOptions 对象描述](#2315-componentdatasourceitemoptions-对象描述) | +| options {} | 请求参数 | **ComponentDataSourceItemOptions** | - | - | 每种请求类型对应不同参数,详见 | 每种请求类型对应不同参数,详见 [ComponentDataSourceItemOptions 对象描述](#2315-componentdatasourceitemoptions-对象描述) | **关于 dataHandler 与 errorHandler 的细节说明:** @@ -462,34 +455,33 @@ try { dataSourceItem.status = 'error'; } ``` + **注意:** + - dataHandler 和 errorHandler 只会走其中的一个回调 - 它们都有修改 promise 状态的机会,意味着可以修改当前数据源最终状态 - 最后返回的结果会被认为是当前数据源的最终结果,如果被 catch 了,那么会认为数据源请求出错 - dataHandler 会有默认值,考虑到返回结果入参都是 response 完整对象,默认值会返回 `response.data`,errorHandler 没有默认值 - ##### 2.3.1.5 ComponentDataSourceItemOptions 对象描述 -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | -| ------- | ------------ | ------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------- | -| uri | 请求地址 | String | ✅ | - | | -| params | 请求参数 | Object | ✅ | {} | 当前数据源默认请求参数(在运行时会被实际的 load 方法的参数替换,如果 load 的 params 没有则会使用当前 params) | -| method | 请求方法 | String | ✅ | GET | | -| isCors | 是否支持跨域 | Boolean | ✅ | true | 对应 `credentials = 'include'` | -| timeout | 超时时长 | Number | ✅ | 5000 | 单位 ms | -| headers | 请求头信息 | Object | ✅ | - | 自定义请求头 | - - +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | +| ------- | ------------ | ------- | -------- | ------ | ------------------------------------------------------------------------------------------------------------ | +| uri | 请求地址 | String | ✅ | - | | +| params | 请求参数 | Object | ✅ | {} | 当前数据源默认请求参数(在运行时会被实际的 load 方法的参数替换,如果 load 的 params 没有则会使用当前 params) | +| method | 请求方法 | String | ✅ | GET | | +| isCors | 是否支持跨域 | Boolean | ✅ | true | 对应 `credentials = 'include'` | +| timeout | 超时时长 | Number | ✅ | 5000 | 单位 ms | +| headers | 请求头信息 | Object | ✅ | - | 自定义请求头 | ##### 2.3.1.6 ComponentLifeCycles 对象描述 生命周期对象,schema 面向多端,不同 DSL 有不同的生命周期方法: - React:对于中后台 PC 物料,已明确使用 React 作为最终渲染框架,因此提案采用 [React16 标准生命周期方法](https://reactjs.org/docs/react-component.html)标准来定义生命周期方法,降低理解成本,支持生命周期如下: - - constructor(props, context)  + - constructor(props, context) - 说明:初始化渲染时执行,常用于设置 state 值。 - - render()  + - render() - 说明:执行于容器组件 React Class 的 render 方法最前,常用于计算变量挂载到 this 对象上,供 props 上属性绑定。此 render() 方法不需要设置 return 返回值。 - componentDidMount() - 说明:组件已加载 @@ -520,7 +512,6 @@ try { }, ``` - ##### 2.3.1.7 dataHandler Function 描述 - 参数:为 dataMap 对象,包含字段如下: @@ -530,14 +521,15 @@ try { ##### 2.3.1.8 ComponentPropDefinition 对象描述 -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | -| ------------ | ---------- | -------------- | -------- | --------- | ----------------------------------------------------------------------------------------------------------------- | -| name | 属性名称 | String | - | - | | +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | +| ------------ | ---------- | -------------- | -------- | --------- | ---------------------------------------------------------------------------------------------------------- | +| name | 属性名称 | String | - | - | | | propType | 属性类型 | String\|Object | - | - | 具体值内容结构,参考《低代码引擎物料协议规范》内的“2.2.2.3 组件属性信息”中描述的**基本类型**和**复合类型** | -| description | 属性描述 | String | - | '' | | -| defaultValue | 属性默认值 | Any | - | undefined | 当 defaultValue 和 defaultProps 中存在同一个 prop 的默认值时,优先使用 defaultValue。 | +| description | 属性描述 | String | - | '' | | +| defaultValue | 属性默认值 | Any | - | undefined | 当 defaultValue 和 defaultProps 中存在同一个 prop 的默认值时,优先使用 defaultValue。 | 范例: + ```json { "propDefinitions": [{ @@ -556,17 +548,15 @@ try { 对应生成源码开发体系中 render 函数返回的 jsx 代码,主要描述有以下属性: - -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | -| ------------- | ---------------------- | ---------------- | -------- | ----------------- | ---------------------------------------------------------------------------------------------------------- | +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | +| ------------- | ---------------------- | ---------------- | -------- | ----------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- | | id | 组件唯一标识 | String | - | | 可选,组件 id 由引擎随机生成(UUID),并保证唯一性,消费方为上层应用平台,在组件发生移动等场景需保持 id 不变 | -| componentName | 组件名称 | String | - | Div | 必填,首字母大写,同 [componentsMap](#22-组件映射关系a) 中的要求 | -| props {} | 组件属性对象 | **Props**| - | {} | 必填,详见 | 必填,详见 [Props 结构描述](#2311-props-结构描述) | -| condition | 渲染条件 | Boolean | ✅ | true | 选填,根据表达式结果判断是否渲染物料;支持变量表达式 | -| loop | 循环数据 | Array | ✅ | - | 选填,默认不进行循环渲染;支持变量表达式 | -| loopArgs | 循环迭代对象、索引名称 | [String, String] | | ["item", "index"] | 选填,仅支持字符串 | -| children | 子组件 | Array | | | 选填,支持变量表达式 | - +| componentName | 组件名称 | String | - | Div | 必填,首字母大写,同 [componentsMap](#22-组件映射关系a) 中的要求 | +| props | 组件属性对象 | **Props** | - | {} | 必填,详见 | 必填,详见 [Props 结构描述](#2311-props-结构描述) | +| condition | 渲染条件 | Boolean | ✅ | true | 选填,根据表达式结果判断是否渲染物料;支持变量表达式 | +| loop | 循环数据 | Array | ✅ | - | 选填,默认不进行循环渲染;支持变量表达式 | +| loopArgs | 循环迭代对象、索引名称 | [String, String] | | ["item", "index"] | 选填,仅支持字符串 | +| children | 子组件 | Array | | | 选填,支持变量表达式 | 描述举例: @@ -597,7 +587,6 @@ try { } ``` - #### 2.3.3 容器结构描述 (A)  容器是一类特殊的组件,在组件能力基础上增加了对生命周期对象、自定义方法、样式文件、数据源等信息的描述。包含**低代码业务组件容器 Component**、**区块容器 Block**、**页面容器 Page** 3 种。主要描述有以下属性: @@ -612,26 +601,22 @@ try { - 条件渲染:condition - 样式文件:css/less/scss - 详细描述: -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | -| --------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------- | -| componentName | 组件名称 | 枚举类型,包括`'Page'` (代表页面容器)、`'Block'` (代表区块容器)、`'Component'` (代表低代码业务组件容器) | - | 'Div' | 必填,首字母大写 | -| fileName | 文件名称 | String | - | - | 必填,英文 | -| props { } | 组件属性对象 | **Props** | - | {} | 必填,详见 [Props 结构描述](#2311-props-结构描述) | -| static | 低代码业务组件类的静态对象 | | | | | -| defaultProps | 低代码业务组件默认属性 | Object | - | - | 选填,仅用于定义低代码业务组件的默认属性 | -| propDefinitions | 低代码业务组件属性类型定义 | **ComponentPropDefinition**[] | - | - | 选填,仅用于定义低代码业务组件的属性数据类型。详见 [ComponentPropDefinition 对象描述](#2318-componentpropdefinition-对象描述) | -| condition | 渲染条件 | Boolean | ✅ | true | 选填,根据表达式结果判断是否渲染物料;支持变量表达式 | -| state | 容器初始数据 | Object | ✅ | - | 选填,支持变量表达式 | -| children | 子组件 | Array | - | | 选填,支持变量表达式 | -| css/less/scss | 样式属性 | String | ✅ | - | 选填,详见 [css/less/scss 样式描述](#2312-csslessscss 样式描述) | -| lifeCycles | 生命周期对象 | **ComponentLifeCycles** | - | - | 详见 [ComponentLifeCycles 对象描述](#2316-componentlifecycles-对象描述) | -| methods | 自定义方法对象 | Object | - | - | 选填,对象成员为函数类型 | -| dataSource {} | 数据源对象 | **ComponentDataSource**| - | - | 选填,异步数据源,详见 | - | - | 选填,异步数据源,详见 [ComponentDataSource 对象描述](#2313-componentdatasource-对象描述) | - - +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | +| --------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------- | --- | --- | ----------------------------------------------------------------------------------------- | +| componentName | 组件名称 | 枚举类型,包括`'Page'` (代表页面容器)、`'Block'` (代表区块容器)、`'Component'` (代表低代码业务组件容器) | - | 'Div' | 必填,首字母大写 | +| fileName | 文件名称 | String | - | - | 必填,英文 | +| props | 组件属性对象 | **Props** | - | {} | 必填,详见 [Props 结构描述](#2311-props-结构描述) | +| defaultProps | 低代码业务组件默认属性 | Object | - | - | 选填,仅用于定义低代码业务组件的默认属性 | +| propDefinitions | 低代码业务组件属性类型定义 | **ComponentPropDefinition**[] | - | - | 选填,仅用于定义低代码业务组件的属性数据类型。详见 [ComponentPropDefinition 对象描述](#2318-componentpropdefinition-对象描述) | +| condition | 渲染条件 | Boolean | ✅ | true | 选填,根据表达式结果判断是否渲染物料;支持变量表达式 | +| state | 容器初始数据 | Object | ✅ | - | 选填,支持变量表达式 | +| children | 子组件 | Array | - | | 选填,支持变量表达式 | +| css/less/scss | 样式属性 | String | ✅ | - | 选填,详见 [css/less/scss 样式描述](#2312-csslessscss 样式描述) | +| lifeCycles | 生命周期对象 | **ComponentLifeCycles** | - | - | 详见 [ComponentLifeCycles 对象描述](#2316-componentlifecycles-对象描述) | +| methods | 自定义方法对象 | Object | - | - | 选填,对象成员为函数类型 | +| dataSource | 数据源对象 | **ComponentDataSource** | - | - | 选填,异步数据源,详见 | - | - | 选填,异步数据源,详见 [ComponentDataSource 对象描述](#2313-componentdatasource-对象描述) | #### 完整描述示例 @@ -647,15 +632,17 @@ try { "background": "#dd2727" } }, - "children": [{ - "componentName": "Button", - "props": { - "text": { - "type": "JSExpression", - "value": "this.state.btnText" + "children": [ + { + "componentName": "Button", + "props": { + "text": { + "type": "JSExpression", + "value": "this.state.btnText" + } } } - }], + ], "state": { "btnText": "submit" }, @@ -683,23 +670,25 @@ try { } }, "dataSource": { - "list": [{ - "id": "list", - "isInit": true, - "type": "fetch/mtop/jsonp", - "options": { - "uri": "", - "params": {}, - "method": "GET", - "isCors": true, - "timeout": 5000, - "headers": {} - }, - "dataHandler": { - "type": "JSFunction", - "value": "function(data, err) {}" + "list": [ + { + "id": "list", + "isInit": true, + "type": "fetch/mtop/jsonp", + "options": { + "uri": "", + "params": {}, + "method": "GET", + "isCors": true, + "timeout": 5000, + "headers": {} + }, + "dataHandler": { + "type": "JSFunction", + "value": "function(data, err) {}" + } } - }], + ], "dataHandler": { "type": "JSFunction", "value": "function(dataMap) { }" @@ -763,12 +752,11 @@ try { **ReactNode** 描述: -| 参数 | 说明 | 值类型 | 默认值 | 备注 | -| ----- | ---------- | --------------------- | -------- | -------------------------------------------------------------- | -| type | 值类型描述 | String | 'JSSlot' | 固定值 | +| 参数 | 说明 | 值类型 | 默认值 | 备注 | +| ----- | ---------- | -------------------------- | -------- | ------------------------------------------------------------------ | +| type | 值类型描述 | String | 'JSSlot' | 固定值 | | value | 具体的值 | NodeSchema \| NodeSchema[] | null | 内容为 NodeSchema 类型,详见[组件结构描述](#232-组件结构描述(A)) | - 举例描述:如 **Card** 的 **title** 属性 ```json @@ -791,15 +779,13 @@ try { ``` - **Function-Return-ReactNode** 描述: -| 参数 | 说明 | 值类型 | 默认值 | 备注 | -| ------ | ---------- | --------------------- | -------- | -------------------------------------------------------------- | -| type | 值类型描述 | String | 'JSSlot' | 固定值 | +| 参数 | 说明 | 值类型 | 默认值 | 备注 | +| ------ | ---------- | -------------------------- | -------- | ------------------------------------------------------------------ | +| type | 值类型描述 | String | 'JSSlot' | 固定值 | | value | 具体的值 | NodeSchema \| NodeSchema[] | null | 内容为 NodeSchema 类型,详见[组件结构描述](#232-组件结构描述(A)) | -| params | 函数的参数 | String[] | null | 函数的入参,其子节点可以通过 `this[参数名]` 来获取对应的参数。 | - +| params | 函数的参数 | String[] | null | 函数的入参,其子节点可以通过 `this[参数名]` 来获取对应的参数。 | 举例描述:如 **Table.Column** 的 **cell** 属性 @@ -868,18 +854,20 @@ try { }" } }, - "children": [{ - "componentName": "Button", - "props": { - "text": "按钮", - "onClick": { - "type": "JSFunction", - "value": "function(e) {\ + "children": [ + { + "componentName": "Button", + "props": { + "text": "按钮", + "onClick": { + "type": "JSFunction", + "value": "function(e) {\ console.log(e.target.innerText);\ }" + } } } - }] + ] } ``` @@ -889,7 +877,6 @@ try { 变量**类型**的属性值描述如下: - - return 数字类型 ```json @@ -898,6 +885,7 @@ try { "value": "this.state.num" } ``` + - return 数字类型 ```json @@ -906,6 +894,7 @@ try { "value": "this.state.num - this.state.num2" } ``` + - return "8 万" 字符串类型 ```json @@ -914,6 +903,7 @@ try { "value": "`${this.state.num}万`" } ``` + - return "8 万" 字符串类型 ```json @@ -922,6 +912,7 @@ try { "value": "this.state.num + '万'" } ``` + - return 13 数字类型 ```json @@ -930,6 +921,7 @@ try { "value": "getNum(this.state.num, this.state.num2)" } ``` + - return true 布尔类型 ```json @@ -958,19 +950,21 @@ try { }" } }, - "children": [{ - "componentName": "Button", - "props": { - "text": { + "children": [ + { + "componentName": "Button", + "props": { + "text": { + "type": "JSExpression", + "value": "this.getNum(this.state.num, this.state.num2) + '万'" + } + }, + "condition": { "type": "JSExpression", - "value": "this.getNum(this.state.num, this.state.num2) + '万'" + "value": "this.state.num > this.state.num2" } - }, - "condition": { - "type": "JSExpression", - "value": "this.state.num > this.state.num2" } - }] + ] } ``` @@ -984,13 +978,14 @@ try { type Ti18n = { type: 'i18n'; key: string; // i18n 结构中字段的 key 标识符 - params?: Record; // 模版型 i18n 文案的入参,JSDataType 指代传统 JS 值类型 -} + params?: Record; +}; ``` 其中 `key` 对应协议 `i18n` 内容的语料键值,`params` 为语料为字符串模板时的变量内容。 假设协议已加入如下 i18n 内容: + ```json { "i18n": { @@ -1041,29 +1036,28 @@ type Ti18n = { } ``` - #### 2.3.5 上下文 API 描述(A) -在上述**事件类型描述**和**变量类型描述**中,在函数或 JS 表达式内,均可以通过 **this** 对象获取当前组件所在容器(React Class)的实例化对象,在搭建场景下的渲染模块和出码模块实现上,统一约定了该实例化 **this** 对象下所挂载的最小 API 集合,以保障搭建协议具备有一致的**数据流**和**事件上下文**。  +在上述**事件类型描述**和**变量类型描述**中,在函数或 JS 表达式内,均可以通过 **this** 对象获取当前组件所在容器(React Class)的实例化对象,在搭建场景下的渲染模块和出码模块实现上,统一约定了该实例化 **this** 对象下所挂载的最小 API 集合,以保障搭建协议具备有一致的**数据流**和**事件上下文**。 ##### 2.3.5.1 容器 API: -| 参数 | 说明 | 类型 | 备注 | -| ----------------------------------- | --------------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------- | -| **this {}** | 当前区块容器的实例对象 | Class Instance | - | -| *this*.state | 三种容器实例的数据对象 state | Object | - | -| *this*.setState(newState, callback) | 三种容器实例更新数据的方法 | Function | 这个 setState 通常会异步执行,详见下文 [setState](#setstate) | -| *this*.customMethod() | 三种容器实例的自定义方法 | Function | - | -| *this*.dataSourceMap {} | 三种容器实例的数据源对象 Map | Object | 单个请求的 id 为 key, value 详见下文 [DataSourceMapItem 结构描述](#datasourcemapitem-结构描述) | -| *this*.reloadDataSource() | 三种容器实例的初始化异步数据请求重载 | Function | 返回 \ | -| **this.page {}** | 当前页面容器的实例对象 | Class Instance | | -| *this.page*.props | 读取页面路由,参数等相关信息 | Object | query 查询参数 { key: value } 形式;path 路径;uri 页面唯一标识;其它扩展字段 | -| *this.page*.xxx | 继承 this 对象所有 API | | 此处 `xxx` 代指 `this.page` 中的其他 API | -| **this.component {}** | 当前低代码业务组件容器的实例对象 | Class Instance | | -| *this.component*.props | 读取低代码业务组件容器的外部传入的 props | Object | | -| *this.component*.xxx | 继承 this 对象所有 API | | 此处 `xxx` 代指 `this.component` 中的其他 API | -| **this.$(ref)** | 获取组件的引用(单个) | Component Instance | `ref` 对应组件上配置的 `ref` 属性,用于唯一标识一个组件;若有同名的,则会返回第一个匹配的。 | -| **this.$$(ref)** | 获取组件的引用(所有同名的) | Array of Component Instances | `ref` 对应组件上配置的 `ref` 属性,用于唯一标识一个组件;总是返回一个数组,里面是所有匹配 `ref` 的组件的引用。 | +| 参数 | 说明 | 类型 | 备注 | +| ----------------------------------- | ---------------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------- | +| **this** | 当前区块容器的实例对象 | Class Instance | - | +| _this_.state | 三种容器实例的数据对象 state | Object | - | +| _this_.setState(newState, callback) | 三种容器实例更新数据的方法 | Function | 这个 setState 通常会异步执行,详见下文 [setState](#setstate) | +| _this_.customMethod() | 三种容器实例的自定义方法 | Function | - | +| _this_.dataSourceMap | 三种容器实例的数据源对象 Map | Object | 单个请求的 id 为 key, value 详见下文 [DataSourceMapItem 结构描述](#datasourcemapitem-结构描述) | +| _this_.reloadDataSource | 三种容器实例的初始化异步数据请求重载 | Function | 返回 Promise | +| **this.page** | 当前页面容器的实例对象 | Class Instance | | +| _this.page_.props | 读取页面路由,参数等相关信息 | Object | query 查询参数 { key: value } 形式;path 路径;uri 页面唯一标识;其它扩展字段 | +| _this.page_.xxx | 继承 this 对象所有 API | | 此处 `xxx` 代指 `this.page` 中的其他 API | +| **this.component** | 当前低代码业务组件容器的实例对象 | Class Instance | | +| _this.component_.props | 读取低代码业务组件容器的外部传入的 props | Object | | +| _this.component_.xxx | 继承 this 对象所有 API | | 此处 `xxx` 代指 `this.component` 中的其他 API | +| **this.$(ref)** | 获取组件的引用(单个) | Component Instance | `ref` 对应组件上配置的 `ref` 属性,用于唯一标识一个组件;若有同名的,则会返回第一个匹配的。 | +| **this.$$(ref)** | 获取组件的引用(所有同名的) | Array of Component Instances | `ref` 对应组件上配置的 `ref` 属性,用于唯一标识一个组件;总是返回一个数组,里面是所有匹配 `ref` 的组件的引用。 | ##### setState @@ -1092,24 +1086,21 @@ this.setState((prevState) => ({ count: prevState.count + 1 })); 为了方便更新部分状态,`setState` 会将 `newState` 浅合并到新的 `state` 上。 - ##### DataSourceMapItem 结构描述 -| 参数 | 说明 | 类型 | 备注 | -| ------------ | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | -| load(params) | 调用单个数据源 | Function | 当前参数 params 会替换 [ComponentDataSourceItemOptions 对象描述](#2315-componentdatasourceitemoptions-对象描述)中的 params 内容 | -| status | 获取单个数据源上次请求状态 | String | loading、loaded、error、init | -| data | 获取上次请求成功后的数据 | Any | | -| error | 获取上次请求失败的错误对象 | Error 对象 | | +| 参数 | 说明 | 类型 | 备注 | +| ------------ | -------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------- | +| load(params) | 调用单个数据源 | Function | 当前参数 params 会替换 [ComponentDataSourceItemOptions 对象描述](#2315-componentdatasourceitemoptions-对象描述)中的 params 内容 | +| status | 获取单个数据源上次请求状态 | String | loading、loaded、error、init | +| data | 获取上次请求成功后的数据 | Any | | +| error | 获取上次请求失败的错误对象 | Error 对象 | | 备注:如果组件没有在区块容器内,而是直接在页面内,那么 `this === this.page` - ##### 2.3.5.2 循环数据 API 获取在循环场景下的数据对象。举例:上层组件设置了 loop 循环数据,且设置了 `loopArgs:["item", "index"]`,当前组件的属性表达式或绑定的事件函数中,可以通过 this 上下文获取所在循环的数据环境;默认值为 `['item','index']` ,如有多层循环,需要自定义不同 loopArgs,同样通过 `this[自定义循环别名]` 获取对应的循环数据和序号; - | 参数 | 说明 | 类型 | 可选值 | | ---------- | --------------------------------- | ------ | ------ | | this.item | 获取当前 index 对应的循环体数据; | Any | - | @@ -1119,47 +1110,51 @@ this.setState((prevState) => ({ count: prevState.count + 1 })); 用于描述物料开发过程中,自定义扩展或引入的第三方工具类(例如:lodash 及 moment),增强搭建基础协议的扩展性,提供通用的工具类方法的配置方案及调用 API。 -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | -| ------------------ | ------------------ | ---------------------------------------------------------------------------------------------------------------- | -------- | ------ | -| utils[] | 工具类扩展映射关系 | **UtilItem**[] | - | | -| *UtilItem*.name | 工具类扩展项名称 | String | - | | -| *UtilItem*.type | 工具类扩展项类型 | 枚举, `'npm'` (代表公网 npm 类型) / `'tnpm'` (代表阿里巴巴内部 npm 类型) / `'function'` (代表 Javascript 函数类型) | - | | -| *UtilItem*.content | 工具类扩展项内容 | [ComponentMap 类型](#22-组件映射关系a) 或 [JSFunction](#2342事件函数类型a) | - | | +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | +| ------- | ---------------- | ---------------------------------------------------------------------------- | -------- | ------ | +| name | 工具类扩展项名称 | String | - | | +| type | 工具类扩展项类型 | 枚举, `'npm'` (代表 npm 类型) / `'function'` (代表 Javascript 函数类型) | - | | +| content | 工具类扩展项内容 | [ComponentMap 类型](#22-组件映射关系a) 或 [JSFunction](#2342事件函数类型a) | - | | 描述示例: ```javascript { - utils: [{ - name: 'clone', - type: 'npm', - content: { - package: 'lodash', - version: '0.0.1', - exportName: 'clone', - subName: '', - destructuring: false, - main: '/lib/clone' - } - }, { - name: 'moment', - type: 'npm', - content: { - package: '@alifd/next', - version: '0.0.1', - exportName: 'Moment', - subName: '', - destructuring: true, - main: '' - } - }, { - name: 'recordEvent', - type: 'function', - content: { - type: 'JSFunction', - value: "function(logkey, gmkey, gokey, reqMethod) {\n goldlog.record('/xxx.event.' + logkey, gmkey, gokey, reqMethod);\n}" - } - }] + utils: [ + { + name: 'clone', + type: 'npm', + content: { + package: 'lodash', + version: '0.0.1', + exportName: 'clone', + subName: '', + destructuring: false, + main: '/lib/clone', + }, + }, + { + name: 'moment', + type: 'npm', + content: { + package: '@alifd/next', + version: '0.0.1', + exportName: 'Moment', + subName: '', + destructuring: true, + main: '', + }, + }, + { + name: 'recordEvent', + type: 'function', + content: { + type: 'JSFunction', + value: + "function(logkey, gmkey, gokey, reqMethod) {\n goldlog.record('/xxx.event.' + logkey, gmkey, gokey, reqMethod);\n}", + }, + }, + ]; } ``` @@ -1194,12 +1189,10 @@ export const recordEvent = function(logkey, gmkey, gokey, reqMethod) { 协议中用于描述国际化语料和组件引用国际化语料的规范,遵循集团国际化中台关于国际化语料规范定义。 - | 参数 | 说明 | 类型 | 可选值 | 默认值 | | ---- | -------------- | ------ | ------ | ------ | | i18n | 国际化语料信息 | Object | - | null | - 描述示例: ```json @@ -1247,6 +1240,7 @@ export const recordEvent = function(logkey, gmkey, gokey, reqMethod) { ``` 使用举例(已废弃) + ```json { "componentName": "Button", @@ -1292,13 +1286,12 @@ export const recordEvent = function(logkey, gmkey, gokey, reqMethod) { 路由配置的结构说明: -| 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 | -| ----------- | ---------------------- | ------------------------------- | ------ | --------- | ------ | -| baseName | 应用根路径 | String | - | '/' | 选填| | -| historyMode | history 模式 | 枚举类型,包括'browser'、'hash' | - | 'browser' | 选填| | +| 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 | +| ----------- | ---------------------------------- | ------------------------------- | ------ | --------- | ------ | +| baseName | 应用根路径 | String | - | '/' | 选填| | +| historyMode | history 模式 | 枚举类型,包括'browser'、'hash' | - | 'browser' | 选填| | | routes | 路由对象组,路径与页面的关系对照组 | Route[] | - | - | 必填| | - ##### 2.11.2 Route (路由记录)结构描述 路由记录,路径与页面的关系对照。Route 的结构说明: @@ -1306,11 +1299,9 @@ export const recordEvent = function(logkey, gmkey, gokey, reqMethod) { | 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 | | -------- | ---------------------------- | ---------------------------- | ------ | ------ | ---------------------------------------------------------------------- | | name | 该路径项的名称 | String | - | - | 选填 | -| path | 路径 | String | - | - | 必填,路径规则详见下面说明 | -| query | 路径的 query 参数 | Object | - | - | 选填 | +| path | 路径 | String | - | - | 必填,路径规则详见下面说明 | | page | 路径对应的页面 ID | String | - | - | 选填,page 与 redirect 字段中必须要有有一个存在 | | redirect | 此路径需要重定向到的路由信息 | String \| Object \| Function | - | - | 选填,page 与 redirect 字段中必须要有有一个存在,详见下文 **redirect** | -| meta | 路由元数据 | Object | - | - | 选填 | | children | 子路由 | Route[] | - | - | 选填 | 以上结构仅说明了路由记录需要的必需字段,如果需要更多的信息字段可以自行实现。 @@ -1325,6 +1316,7 @@ path(页面路径)是浏览器URL的组成部分,同时大部分网站的 路径规则是路由配置的重要组成部分,我们希望一个路径配置的基本能力需要支持具体的路径(/xxx)与路径参数 (/:abc)。 以一个 `/one/:two?/three/:four?/:five?` 路径为例,它能够解析以下路径: + - `/one/three` - `/one/:two/three` - `/one/three/:four` @@ -1339,6 +1331,7 @@ path(页面路径)是浏览器URL的组成部分,同时大部分网站的 关于 **redirect** 字段的详细说明: **redirect** 字段有三种填入类型,分别是 `String`、`Object`、`Function`: + 1. 字符串(`String`)格式下默认处理为重定向到路径,支持传入 '/xxx'、'/xxx?ab=c'。 2. 对象(`String`)格式下可传入路由对象,如 { name: 'xxx' }、{ path: '/xxx' },可重定向到对应的路由对象。 3. 函数`Function`格式为`(to) => Route`,它的入参为当前路由项信息,支持返回一个 Route 对象或者字符串,存在一些特殊情况,在重定向的时候需要对重定向之后的路径进行处理的情况下,需要使用函数声明。 @@ -1347,14 +1340,14 @@ path(页面路径)是浏览器URL的组成部分,同时大部分网站的 { "redirect": { "type": "JSFunction", - "value": "(to) => { return { path: '/a', query: { fromPath: to.path } } }", + "value": "(to) => { return { path: '/a', query: { fromPath: to.path } } }" } } ``` ##### 完整描述示例 -``` json +```json { "router": { "baseName": "/", @@ -1388,21 +1381,20 @@ path(页面路径)是浏览器URL的组成部分,同时大部分网站的 用于描述当前应用的页面信息,比如页面对应的低代码搭建内容、页面标题、页面配置等。 在一些比较复杂的场景下,允许声明一层页面映射关系,以支持页面声明更多信息与配置,同时能够支持不同类型的产物。 -| 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 | -| ------- | --------------------- | ------ | ------ | ------ | -------------------------------------------------------- | -| id | 页面 id | String | - | - | 必填 | -| type | 页面类型 | String | - | - | 选填,可用来区分页面的类型 | -| treeId | 对应的低代码树中的 id | String | - | - | 选填,页面对应的 componentsTree 中的子项 id | -| packageId | 对应的资产包对象 | String | - | - | 选填,页面对应的资产包对象,一般用于微应用场景下,当路由匹配到当前页面的时候,会加载 `packageId` 对应的微应用进行渲染。 | -| meta | 页面元信息 | Object | - | - | 选填,用于描述当前应用的配置信息 | -| config | 页面配置 | Object | - | - | 选填,用于描述当前应用的元数据信息 | - +| 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 | +| --------- | ---------- | ------ | ------ | ------ | ----------------------------------------------------------------------------- | +| id | 页面 id | String | - | - | 必填 | +| type | 页面类型 | String | - | - | 选填,可用来区分页面的类型,如 componentsTree,package,默认为 componentsTree | +| mappingId | 映射 id | String | - | - | 必填,根据页面的类型 type 判断及确定目标的 id | +| meta | 页面元信息 | Object | - | - | 选填,用于描述当前应用的配置信息 | +| config | 页面配置 | Object | - | - | 选填,用于描述当前应用的元数据信息 | #### 2.12.1 微应用(低代码+)相关说明 在开发过程中,我们经常会遇到一些特殊的情况,比如一个低代码应用想要集成一些别的系统的页面或者系统中的一些页面只能是源码开发(与低代码相对的纯工程代码形式),为了满足更多的使用场景,应用级渲染引擎引入了微应用(微前端)的概念,使低代码页面与其他的页面结合成为可能。 微应用对象通过资产包加载,需要暴露两个生命周期方法: + - mount(container: HTMLElement, props: any) - 说明:微应用挂载到 container(dom 节点)的调用方法,会在渲染微应用时调用 - unmout(container: HTMLElement, props: any) @@ -1469,7 +1461,6 @@ path(页面路径)是浏览器URL的组成部分,同时大部分网站的 ] ``` - ## 3 应用描述 ### 3.1 文件目录 @@ -1477,168 +1468,161 @@ path(页面路径)是浏览器URL的组成部分,同时大部分网站的 以下是推荐的应用目录结构,与标准源码 build-scripts 对齐,这里的目录结构是帮助理解应用级协议的设计,不做强约束 ```html -├── META/ # 低代码元数据信息,用于多分支冲突解决、数据回滚等功能 -├── public/ # 静态文件,构建时会 copy 到 build/ 目录 -│ ├── index.html # 应用入口 HTML -│ └── favicon.png # Favicon -├── src/ -│ ├── components/ # 应用内的低代码业务组件 -│ │ └── guide-component/ -│ │ ├── index.js # 组件入口 -│ │ ├── components.js # 组件依赖的其他组件 -│ │ ├── schema.js # schema 描述 -│ │ └── index.scss # css 样式 -│ ├── pages/ # 页面 -│ │ └── home/ # Home 页面 -│ │ ├── index.js # 页面入口 -│ │ └── index.scss # css 样式 -│ ├── layouts/ -│ │ └── basic-layout/ # layout 组件名称 -│ │ ├── index.js # layout 入口 -│ │ ├── components.js # layout 组件依赖的其他组件 -│ │ ├── schema.js # layout schema 描述 -│ │ └── index.scss # layout css 样式 -│ ├── config/ # 配置信息 -│ │ ├── components.js # 应用上下文所有组件 -│ │ ├── routes.js # 页面路由列表 -│ │ └── app.js # 应用配置文件 -│ ├── utils/ # 工具库 -│ │ └── index.js # 应用第三方扩展函数 -│ ├── locales/ # [可选] 国际化资源 -│ │ ├── en-US -│ │ └── zh-CN -│ ├── global.scss # 全局样式 -│ └── index.jsx # 应用入口脚本,依赖 config/routes.js 的路由配置动态生成路由; -├── webpack.config.js # 项目工程配置,包含插件配置及自定义 webpack 配置等 -├── README.md -├── package.json -├── .editorconfig -├── .eslintignore -├── .eslintrc.js -├── .gitignore -├── .stylelintignore -└── .stylelintrc.js +├── META/ # 低代码元数据信息,用于多分支冲突解决、数据回滚等功能 ├── public/ # 静态文件,构建时会 +copy 到 build/ 目录 │ ├── index.html # 应用入口 HTML │ └── favicon.png # Favicon ├── src/ │ ├── +components/ # 应用内的低代码业务组件 │ │ └── guide-component/ │ │ ├── index.js # 组件入口 │ │ ├── +components.js # 组件依赖的其他组件 │ │ ├── schema.js # schema 描述 │ │ └── index.scss # css 样式 │ +├── pages/ # 页面 │ │ └── home/ # Home 页面 │ │ ├── index.js # 页面入口 │ │ └── index.scss # css +样式 │ ├── layouts/ │ │ └── basic-layout/ # layout 组件名称 │ │ ├── index.js # layout 入口 │ │ ├── +components.js # layout 组件依赖的其他组件 │ │ ├── schema.js # layout schema 描述 │ │ └── index.scss +# layout css 样式 │ ├── config/ # 配置信息 │ │ ├── components.js # 应用上下文所有组件 │ │ ├── +routes.js # 页面路由列表 │ │ └── app.js # 应用配置文件 │ ├── utils/ # 工具库 │ │ └── index.js # +应用第三方扩展函数 │ ├── locales/ # [可选] 国际化资源 │ │ ├── en-US │ │ └── zh-CN │ ├── global.scss +# 全局样式 │ └── index.jsx # 应用入口脚本,依赖 config/routes.js 的路由配置动态生成路由; ├── +webpack.config.js # 项目工程配置,包含插件配置及自定义 webpack 配置等 ├── README.md ├── package.json +├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .stylelintignore └── +.stylelintrc.js ``` ### 3.2 应用级别 APIs + > 下文中 `xxx` 代指任意 API + #### 3.2.1 路由 Router API - - this.location.`xxx` 「不推荐,推荐统一通过 this.router api」 - - this.history.`xxx` 「不推荐,推荐统一通过 this.router api」 - - this.match.`xxx` 「不推荐,推荐统一通过 this.router api」 - - this.router.`xxx` + +- this.location.`xxx` 「不推荐,推荐统一通过 this.router api」 +- this.history.`xxx` 「不推荐,推荐统一通过 this.router api」 +- this.match.`xxx` 「不推荐,推荐统一通过 this.router api」 +- this.router.`xxx` ##### Router 结构说明 -| API | 函数签名 | 说明 | -| -------------- | ---------------------------------------------------------- | -------------------------------------------------------------- | -| getCurrentRoute | () => RouteLocation | 获取当前解析后的路由信息,RouteLocation 结构详见下面说明 | -| push | (target: string \| Route) => void | 路由跳转方法,跳转到指定的路径或者 Route | -| replace | (target: string \| Route) => void | 路由跳转方法,与 `push` 的区别在于不会增加一条历史记录而是替换当前的历史记录 | -| beforeRouteLeave | (guard: (to: RouteLocation, from: RouteLocation) => boolean \| Route) => void | 路由跳转前的守卫方法,详见下面说明 | -| afterRouteChange | (fn: (to: RouteLocation, from: RouteLocation) => void) => void | 路由跳转后的钩子函数,会在每次路由改变后执行 | +| API | 函数签名 | 说明 | +| ---------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| getCurrentRoute | () => RouteLocation | 获取当前解析后的路由信息,RouteLocation 结构详见下面说明 | +| push | (target: string \| Route) => void | 路由跳转方法,跳转到指定的路径或者 Route | +| replace | (target: string \| Route) => void | 路由跳转方法,与 `push` 的区别在于不会增加一条历史记录而是替换当前的历史记录 | +| beforeRouteLeave | (guard: (to: RouteLocation, from: RouteLocation) => boolean \| Route) => void | 路由跳转前的守卫方法,详见下面说明 | +| afterRouteChange | (fn: (to: RouteLocation, from: RouteLocation) => void) => void | 路由跳转后的钩子函数,会在每次路由改变后执行 | ##### 3.2.1.1 RouteLocation(路由信息)结构说明 **RouteLocation** 是路由控制器匹配到对应的路由记录后进行解析产生的对象,它的结构如下: -| 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 | -| -------------- | ---------------------- | ------ | ------ | ------ | ------ | -| path | 当前解析后的路径 | String | - | - | 必填 | -| hash | 当前路径的 hash 值,以 # 开头 | String | - | - | 必填 | -| href | 当前的全部路径 | String | - | - | 必填 | -| params | 匹配到的路径参数 | Object | - | - | 必填 | -| query | 当前的路径 query 对象 | Object | - | - | 必填,代表当前地址的 search 属性的对象 | -| name | 匹配到的路由记录名 | String | - | - | 选填 | -| meta | 匹配到的路由记录元数据 | Object | - | - | 选填 | -| redirectedFrom | 原本指向向的路由记录 | Route | - | - | 选填,在重定向到当前地址之前,原先想访问的地址 | -| fullPath | 包括 search 和 hash 在内的完整地址 | String | - | - | 选填 | - +| 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 | +| -------------- | ---------------------------------- | ------ | ------ | ------ | ---------------------------------------------- | +| path | 当前解析后的路径 | String | - | - | 必填 | +| hash | 当前路径的 hash 值,以 # 开头 | String | - | - | 必填 | +| href | 当前的全部路径 | String | - | - | 必填 | +| params | 匹配到的路径参数 | Object | - | - | 必填 | +| query | 当前的路径 query 对象 | Object | - | - | 必填,代表当前地址的 search 属性的对象 | +| name | 匹配到的路由记录名 | String | - | - | 选填 | +| meta | 匹配到的路由记录元数据 | Object | - | - | 选填 | +| redirectedFrom | 原本指向向的路由记录 | Route | - | - | 选填,在重定向到当前地址之前,原先想访问的地址 | +| fullPath | 包括 search 和 hash 在内的完整地址 | String | - | - | 选填 | ##### beforeRouteLeave + 通过 beforeRouteLeave 注册的路由守卫方法会在每次路由跳转前执行。该方法一般会在应用鉴权,路由重定向等场景下使用。 > `beforeRouteLeave` 只在 `router.push/replace` 的方法调用时生效。 传入守卫的入参为: -* to: 即将要进入的目标路由(RouteLocation) -* from: 当前导航正要离开的路由(RouteLocation) + +- to: 即将要进入的目标路由(RouteLocation) +- from: 当前导航正要离开的路由(RouteLocation) 该守卫返回一个 `boolean` 或者路由对象来告知路由控制器接下来的行为。 -* 如果返回 `false`, 则停止跳转 -* 如果返回 `true`,则继续跳转 -* 如果返回路由对象,则重定向至对应的路由 + +- 如果返回 `false`, 则停止跳转 +- 如果返回 `true`,则继续跳转 +- 如果返回路由对象,则重定向至对应的路由 **使用范例:** ```json { - "componentsTree": [{ - "componentName": "Page", - "fileName": "Page1", - "props": {}, - "children": [{ - "componentName": "Div", + "componentsTree": [ + { + "componentName": "Page", + "fileName": "Page1", "props": {}, - "children": [{ - "componentName": "Button", - "props": { - "text": "跳转到首页", - "onClick": { - "type": "JSFunction", - "value": "function () { this.router.push('/home'); }" - } - }, - }] - }], - }] + "children": [ + { + "componentName": "Div", + "props": {}, + "children": [ + { + "componentName": "Button", + "props": { + "text": "跳转到首页", + "onClick": { + "type": "JSFunction", + "value": "function () { this.router.push('/home'); }" + } + } + } + ] + } + ] + } + ] } ``` - #### 3.2.2 应用级别的公共函数或第三方扩展 - - this.utils.`xxx` + +- this.utils.`xxx` #### 3.2.3 国际化相关 API -| API | 函数签名 | 说明 | -| -------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------ | + +| API | 函数签名 | 说明 | +| -------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | this.i18n | (i18nKey: string, params?: { [paramName: string]: string; }) => string | i18nKey 是语料的标识符,params 可选,是用来做模版字符串替换的。返回语料字符串 | -| this.getLocale | () => string | 返回当前环境语言 code | -| this.setLocale | (locale: string) => void | 设置当前环境语言 code | +| this.getLocale | () => string | 返回当前环境语言 code | +| this.setLocale | (locale: string) => void | 设置当前环境语言 code | **使用范例:** + ```json { - "componentsTree": [{ - "componentName": "Page", - "fileName": "Page1", - "props": {}, - "children": [{ - "componentName": "Div", + "componentsTree": [ + { + "componentName": "Page", + "fileName": "Page1", "props": {}, - "children": [{ - "componentName": "Button", - "props": { - "children": { - "type": "JSExpression", - "value": "this.i18n('i18n-hello')" - }, - "onClick": { - "type": "JSFunction", - "value": "function () { this.setLocale('en-US'); }" - } - }, - }, { - "componentName": "Button", - "props": { - "children": { - "type": "JSExpression", - "value": "this.i18n('i18n-chicken', { count: this.state.count })" - }, - }, - }] - }], - }], + "children": [ + { + "componentName": "Div", + "props": {}, + "children": [ + { + "componentName": "Button", + "props": { + "children": { + "type": "JSExpression", + "value": "this.i18n('i18n-hello')" + }, + "onClick": { + "type": "JSFunction", + "value": "function () { this.setLocale('en-US'); }" + } + } + }, + { + "componentName": "Button", + "props": { + "children": { + "type": "JSExpression", + "value": "this.i18n('i18n-chicken', { count: this.state.count })" + } + } + } + ] + } + ] + } + ], "i18n": { "zh-CN": { "i18n-hello": "你好", diff --git a/package.json b/package.json index dae20d21f..d9f5efbf4 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "lerna": "^4.0.0", "typescript": "^5.4.2", "yarn": "^1.22.17", + "prettier": "^3.2.5", "rimraf": "^3.0.2", "rollup": "^4.13.0", "vite": "^5.1.6", diff --git a/packages/types/package.json b/packages/types/package.json index 5651d427d..7b3a7dbad 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-types", - "version": "1.3.2", + "version": "1.3.3", "description": "Types for Ali lowCode engine", "files": [ "es", diff --git a/packages/types/src/shell/type/context-menu.ts b/packages/types/src/shell/type/context-menu.ts index dd6d583c2..8b58284f2 100644 --- a/packages/types/src/shell/type/context-menu.ts +++ b/packages/types/src/shell/type/context-menu.ts @@ -1,16 +1,19 @@ import { IPublicEnumContextMenuType } from '../enum'; import { IPublicModelNode } from '../model'; -import { IPublicTypeI18nData } from './i8n-data'; +import { IPublicTypeI18nData } from './i18n-data'; import { IPublicTypeHelpTipConfig } from './widget-base-config'; -export interface IPublicTypeContextMenuItem extends Omit { +export interface IPublicTypeContextMenuItem + extends Omit< + IPublicTypeContextMenuAction, + 'condition' | 'disabled' | 'items' + > { disabled?: boolean; items?: Omit[]; } export interface IPublicTypeContextMenuAction { - /** * 动作的唯一标识符 * Unique identifier for the action @@ -41,7 +44,11 @@ export interface IPublicTypeContextMenuAction { * 子菜单项或生成子节点的函数,可选,仅支持两级 * Sub-menu items or function to generate child node, optional */ - items?: Omit[] | ((nodes?: IPublicModelNode[]) => Omit[]); + items?: + | Omit[] + | (( + nodes?: IPublicModelNode[], + ) => Omit[]); /** * 显示条件函数 @@ -60,4 +67,3 @@ export interface IPublicTypeContextMenuAction { */ help?: IPublicTypeHelpTipConfig; } - diff --git a/packages/types/src/shell/type/i8n-data.ts b/packages/types/src/shell/type/i18n-data.ts similarity index 100% rename from packages/types/src/shell/type/i8n-data.ts rename to packages/types/src/shell/type/i18n-data.ts diff --git a/packages/types/src/shell/type/index.ts b/packages/types/src/shell/type/index.ts index 76dd38925..c72cf93e4 100644 --- a/packages/types/src/shell/type/index.ts +++ b/packages/types/src/shell/type/index.ts @@ -24,7 +24,7 @@ export * from './widget-base-config'; export * from './node-data'; export * from './icon-type'; export * from './transformed-component-metadata'; -export * from './i8n-data'; +export * from './i18n-data'; export * from './npm-info'; export * from './drag-node-data-object'; export * from './drag-node-object'; @@ -93,4 +93,4 @@ export * from './scrollable'; export * from './simulator-renderer'; export * from './config-transducer'; export * from './context-menu'; -export * from './command'; \ No newline at end of file +export * from './command'; diff --git a/packages/types/src/shell/type/npm.ts b/packages/types/src/shell/type/npm.ts index 2d1396be4..736c723b1 100644 --- a/packages/types/src/shell/type/npm.ts +++ b/packages/types/src/shell/type/npm.ts @@ -12,5 +12,7 @@ export interface IPublicTypeLowCodeComponent { } export type IPublicTypeProCodeComponent = IPublicTypeNpmInfo; -export type IPublicTypeComponentMap = IPublicTypeProCodeComponent | IPublicTypeLowCodeComponent; +export type IPublicTypeComponentMap = + | IPublicTypeProCodeComponent + | IPublicTypeLowCodeComponent; export type IPublicTypeComponentsMap = IPublicTypeComponentMap[]; diff --git a/rollup.config.mjs b/rollup.config.mjs new file mode 100644 index 000000000..ca9733bfe --- /dev/null +++ b/rollup.config.mjs @@ -0,0 +1,7 @@ +export default { + input: 'src/main.js', + output: { + file: 'bundle.js', + format: 'cjs' + } +}; \ No newline at end of file diff --git a/runtime/react-renderer/package.json b/runtime/react-renderer/package.json deleted file mode 100644 index bc92b6dcd..000000000 --- a/runtime/react-renderer/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@alilc/react-renderer", - "version": "2.0.0-beta.0", - "description": "", - "type": "module", - "bugs": "https://github.com/alibaba/lowcode-engine/issues", - "homepage": "https://github.com/alibaba/lowcode-engine/#readme", - "dependencies": { - "@vue/reactivity": "^3.4.21" - } -} \ No newline at end of file diff --git a/runtime/react-renderer/src/api/create-app.tsx b/runtime/react-renderer/src/api/create-app.tsx deleted file mode 100644 index 97f5ac7b7..000000000 --- a/runtime/react-renderer/src/api/create-app.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - type App, - type RenderBase, - createAppFunction, - type AppOptionsBase, -} from '@alilc/runtime-core'; -import { type DataSourceCreator } from '@alilc/runtime-shared'; -import { type ComponentType } from 'react'; -import { type Root, createRoot } from 'react-dom/client'; -import { createRenderer } from '../renderer'; -import AppComponent from '../components/app'; -import { intlPlugin } from '../plugins/intl'; -import { globalUtilsPlugin } from '../plugins/utils'; -import { initRouter } from '../router'; - -export interface AppOptions extends AppOptionsBase { - dataSourceCreator: DataSourceCreator; - faultComponent?: ComponentType; -} - -export interface ReactRender extends RenderBase {} - -export type ReactApp = App; - -export const createApp = createAppFunction( - async (context, options) => { - const renderer = createRenderer(); - const appContext = { ...context, renderer }; - - initRouter(appContext); - - options.plugins ??= []; - options.plugins!.unshift(globalUtilsPlugin, intlPlugin); - - // set config - if (options.faultComponent) { - context.config.set('faultComponent', options.faultComponent); - } - context.config.set('dataSourceCreator', options.dataSourceCreator); - - let root: Root | undefined; - - const reactRender: ReactRender = { - async mount(el) { - if (root) { - return; - } - - root = createRoot(el); - root.render(); - }, - unmount() { - if (root) { - root.unmount(); - root = undefined; - } - }, - }; - - return { - renderBase: reactRender, - renderer, - }; - } -); diff --git a/runtime/react-renderer/tsconfig.json b/runtime/react-renderer/tsconfig.json deleted file mode 100644 index 7460ef428..000000000 --- a/runtime/react-renderer/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../tsconfig.json" -} \ No newline at end of file diff --git a/runtime/react-renderer/vitest.config.ts b/runtime/react-renderer/vitest.config.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/runtime/renderer-core/package.json b/runtime/renderer-core/package.json index 17462bca8..2be5985c5 100644 --- a/runtime/renderer-core/package.json +++ b/runtime/renderer-core/package.json @@ -5,6 +5,12 @@ "type": "module", "bugs": "https://github.com/alibaba/lowcode-engine/issues", "homepage": "https://github.com/alibaba/lowcode-engine/#readme", + "license": "MIT", + "scripts": { + "build": "", + "test": "vitest --run", + "test:watch": "vitest" + }, "dependencies": { "@alilc/lowcode-types": "1.3.2", "lodash-es": "^4.17.21" diff --git a/runtime/renderer-core/src/api/create-app-function.ts b/runtime/renderer-core/src/api/app.ts similarity index 54% rename from runtime/renderer-core/src/api/create-app-function.ts rename to runtime/renderer-core/src/api/app.ts index f2fa210f7..25c97157b 100644 --- a/runtime/renderer-core/src/api/create-app-function.ts +++ b/runtime/renderer-core/src/api/app.ts @@ -1,26 +1,18 @@ -import { - type ProjectSchema, - type Package, - type AnyObject, -} from '@alilc/runtime-shared'; -import { type PackageManager, createPackageManager } from '../core/package'; -import { createPluginManager, type Plugin } from '../core/plugin'; -import { createScope, type CodeScope } from '../core/codeRuntime'; -import { - appBoosts, - type AppBoosts, - type AppBoostsManager, -} from '../core/boosts'; -import { type AppSchema, createAppSchema } from '../core/schema'; +import type { Project, Package, PlainObject } from '../types'; +import { type PackageManager, createPackageManager } from '../package'; +import { createPluginManager, type Plugin } from '../plugin'; +import { createScope, type CodeScope } from '../code-runtime'; +import { appBoosts, type AppBoosts, type AppBoostsManager } from '../boosts'; +import { type AppSchema, createAppSchema } from '../schema'; export interface AppOptionsBase { - schema: ProjectSchema; + schema: Project; packages?: Package[]; plugins?: Plugin[]; - appScopeValue?: AnyObject; + appScopeValue?: PlainObject; } -export interface RenderBase { +export interface AppBase { mount: (el: HTMLElement) => void | Promise; unmount: () => void | Promise; } @@ -36,13 +28,13 @@ export interface AppContext { boosts: AppBoostsManager; } -type AppCreator = ( +type AppCreator = ( appContext: Omit, - appOptions: O -) => Promise<{ renderBase: T; renderer?: any }>; + appOptions: O, +) => Promise<{ appBase: T; renderer?: any }>; -export type App = { - schema: ProjectSchema; +export type App = { + schema: Project; config: Map; readonly boosts: AppBoosts; @@ -55,11 +47,14 @@ export type App = { * @param options * @returns */ -export function createAppFunction< - O extends AppOptionsBase, - T extends RenderBase = RenderBase ->(appCreator: AppCreator): (options: O) => Promise> { - return async options => { +export function createAppFunction( + appCreator: AppCreator, +): (options: O) => Promise> { + if (typeof appCreator !== 'function') { + throw Error('The first parameter must be a function.'); + } + + return async (options) => { const { schema, appScopeValue = {} } = options; const appSchema = createAppSchema(schema); const appConfig = new Map(); @@ -77,14 +72,19 @@ export function createAppFunction< boosts: appBoosts, }; - const { renderBase, renderer } = await appCreator(appContext, options); + const { appBase, renderer } = await appCreator(appContext, options); + + if (!('mount' in appBase) || !('unmount' in appBase)) { + throw Error('appBase 必须返回 mount 和 unmount 方法'); + } + const pluginManager = createPluginManager({ ...appContext, renderer, }); if (options.plugins?.length) { - await Promise.all(options.plugins.map(p => pluginManager.add(p))); + await Promise.all(options.plugins.map((p) => pluginManager.add(p))); } if (options.packages?.length) { @@ -100,7 +100,7 @@ export function createAppFunction< return appBoosts.value; }, }, - renderBase + appBase, ); }; } diff --git a/runtime/renderer-core/src/api/create-component-function.ts b/runtime/renderer-core/src/api/component.ts similarity index 56% rename from runtime/renderer-core/src/api/create-component-function.ts rename to runtime/renderer-core/src/api/component.ts index 790098dd1..15ad19b1b 100644 --- a/runtime/renderer-core/src/api/create-component-function.ts +++ b/runtime/renderer-core/src/api/component.ts @@ -1,104 +1,27 @@ -import { isJsFunction } from '@alilc/runtime-shared'; -import { - type CodeRuntime, - createCodeRuntime, - type CodeScope, - createScope, -} from '../core/codeRuntime'; -import { throwRuntimeError } from '../core/error'; -import { type ComponentTreeNode, createNode } from '../helper/treeNode'; -import { validateContainerSchema } from '../helper/validator'; - -import type { - RootSchema, - DataSourceEngine, - DataSourceCreator, - AnyObject, - Package, -} from '@alilc/runtime-shared'; - -export interface StateContext { - /** 组件状态 */ - readonly state: AnyObject; - /** 状态设置方法 */ - setState: (newState: AnyObject) => void; -} - -interface ContainerInstanceScope - extends StateContext, - DataSourceEngine { - readonly props: AnyObject | undefined; - - $(ref: string): C | undefined; - - [key: string]: any; -} - -type LifeCycleName = - | 'constructor' - | 'render' - | 'componentDidMount' - | 'componentDidUpdate' - | 'componentWillUnmount' - | 'componentDidCatch'; - -export interface ContainerInstance { - readonly id?: string; - readonly cssText: string | undefined; - readonly codeScope: CodeScope; - - /** 调用生命周期方法 */ - triggerLifeCycle(lifeCycleName: LifeCycleName, ...args: any[]): void; - /** - * 设置 ref 对应的组件实例, 提供给 scope.$() 方式使用 - */ - setRefInstance(ref: string, instance: C): void; - removeRefInstance(ref: string, instance?: C): void; - /** 获取子节点内容 渲染使用 */ - getComponentTreeNodes(): ComponentTreeNode[]; - - destory(): void; -} - -export interface Container { - readonly codeScope: CodeScope; - readonly codeRuntime: CodeRuntime; - - createInstance( - componentsTree: RootSchema, - extraProps?: AnyObject - ): ContainerInstance; -} +import { CreateContainerOptions, createContainer } from '../container'; +import { createCodeRuntime, createScope } from '../code-runtime'; +import { throwRuntimeError } from '../utils/error'; +import { validateContainerSchema } from '../validator/schema'; export interface ComponentOptionsBase { componentsTree: RootSchema; componentsRecord: Record; - supCodeScope?: CodeScope; - initScopeValue?: AnyObject; dataSourceCreator: DataSourceCreator; } -export function createComponentFunction< - C, - O extends ComponentOptionsBase ->(options: { +export function createComponentFunction>(options: { stateCreator: (initState: AnyObject) => StateContext; componentCreator: (container: Container, componentOptions: O) => C; defaultOptions?: Partial; }): (componentOptions: O) => C { const { stateCreator, componentCreator, defaultOptions = {} } = options; - return componentOptions => { + return (componentOptions) => { const finalOptions = Object.assign({}, defaultOptions, componentOptions); - const { - supCodeScope, - initScopeValue = {}, - dataSourceCreator, - } = finalOptions; + const { supCodeScope, initScopeValue = {}, dataSourceCreator } = finalOptions; const codeRuntimeScope = - supCodeScope?.createSubScope(initScopeValue) ?? - createScope(initScopeValue); + supCodeScope?.createSubScope(initScopeValue) ?? createScope(initScopeValue); const codeRuntime = createCodeRuntime(codeRuntimeScope); const container: Container = { @@ -116,9 +39,7 @@ export function createComponentFunction< const mapRefToComponentInstance: Map = new Map(); - const initialState = codeRuntime.parseExprOrFn( - componentsTree.state ?? {} - ); + const initialState = codeRuntime.parseExprOrFn(componentsTree.state ?? {}); const stateContext = stateCreator(initialState); codeRuntimeScope.setValue( @@ -135,13 +56,10 @@ export function createComponentFunction< }, stateContext, dataSourceCreator - ? dataSourceCreator( - componentsTree.dataSource ?? ({ list: [] } as any), - stateContext - ) - : {} + ? dataSourceCreator(componentsTree.dataSource ?? ({ list: [] } as any), stateContext) + : {}, ) as ContainerInstanceScope, - true + true, ); if (componentsTree.methods) { @@ -155,10 +73,7 @@ export function createComponentFunction< triggerLifeCycle('constructor'); - function triggerLifeCycle( - lifeCycleName: LifeCycleName, - ...args: any[] - ) { + function triggerLifeCycle(lifeCycleName: LifeCycleNameT, ...args: any[]) { // keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象 if ( !componentsTree.lifeCycles || @@ -169,9 +84,7 @@ export function createComponentFunction< const lifeCycleSchema = componentsTree.lifeCycles[lifeCycleName]; if (isJsFunction(lifeCycleSchema)) { - const lifeCycleFn = codeRuntime.createFnBoundScope( - lifeCycleSchema.value - ); + const lifeCycleFn = codeRuntime.createFnBoundScope(lifeCycleSchema.value); if (lifeCycleFn) { lifeCycleFn.apply(codeRuntime.getScope().value, args); } @@ -202,8 +115,8 @@ export function createComponentFunction< ? componentsTree.children : [componentsTree.children] : []; - const treeNodes = childNodes.map(item => { - return createNode(item, undefined); + const treeNodes = childNodes.map((item) => { + return createComponentTreeNode(item, undefined); }); return treeNodes; diff --git a/runtime/renderer-core/src/boosts.ts b/runtime/renderer-core/src/boosts.ts index a3a4f1fa9..ad4607c0b 100644 --- a/runtime/renderer-core/src/boosts.ts +++ b/runtime/renderer-core/src/boosts.ts @@ -1,8 +1,11 @@ -import { type AnyFunction } from '@alilc/runtime-shared'; -import { createHooks, type Hooks } from '../helper/hook'; -import { type RuntimeError } from './error'; +import { type AnyFunction } from './types'; +import { createHookStore, type HookStore } from './utils/hook'; +import { nonSetterProxy } from './utils/non-setter-proxy'; +import { type RuntimeError } from './utils/error'; -export interface AppBoosts {} +export interface AppBoosts { + [key: string]: any; +} export interface RuntimeHooks { 'app:error': (error: RuntimeError) => void; @@ -11,22 +14,29 @@ export interface RuntimeHooks { } export interface AppBoostsManager { - hooks: Hooks; + hookStore: HookStore; readonly value: AppBoosts; add(name: PropertyKey, value: any, force?: boolean): void; + remove(name: PropertyKey): void; } const boostsValue: AppBoosts = {}; +const proxyBoostsValue = nonSetterProxy(boostsValue); export const appBoosts: AppBoostsManager = { - hooks: createHooks(), + hookStore: createHookStore(), get value() { - return boostsValue; + return proxyBoostsValue; }, add(name: PropertyKey, value: any, force = false) { if ((boostsValue as any)[name] && !force) return; (boostsValue as any)[name] = value; }, + remove(name) { + if ((boostsValue as any)[name]) { + delete (boostsValue as any)[name]; + } + }, }; diff --git a/runtime/renderer-core/src/code-runtime.ts b/runtime/renderer-core/src/code-runtime.ts index ea65ed0d2..d28a3fb2a 100644 --- a/runtime/renderer-core/src/code-runtime.ts +++ b/runtime/renderer-core/src/code-runtime.ts @@ -1,24 +1,22 @@ -import { - type AnyFunction, - type AnyObject, - JSExpression, - JSFunction, - isJsExpression, - isJsFunction, -} from '@alilc/runtime-shared'; -import { processValue } from '../utils/value'; +import type { AnyFunction, PlainObject, JSExpression, JSFunction } from './types'; +import { isJSExpression, isJSFunction } from './utils/type-guard'; +import { processValue } from './utils/value'; export interface CodeRuntime { run(code: string): T | undefined; createFnBoundScope(code: string): AnyFunction | undefined; - parseExprOrFn(value: AnyObject): any; + parseExprOrFn(value: PlainObject): any; bindingScope(scope: CodeScope): void; getScope(): CodeScope; } -export function createCodeRuntime(scope?: CodeScope): CodeRuntime { - let runtimeScope = scope ?? createScope({}); +const SYMBOL_SIGN = '__code__scope'; + +export function createCodeRuntime(scopeOrValue: PlainObject = {}): CodeRuntime { + let runtimeScope = scopeOrValue[Symbol.for(SYMBOL_SIGN)] + ? (scopeOrValue as CodeScope) + : createScope(scopeOrValue); function run(code: string): T | undefined { if (!code) return undefined; @@ -26,16 +24,11 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime { try { return new Function( 'scope', - `"use strict";return (function(){return (${code})}).bind(scope)();` + `"use strict";return (function(){return (${code})}).bind(scope)();`, )(runtimeScope.value) as T; } catch (err) { - console.log( - '%c eval error', - 'font-size:13px; background:pink; color:#bf2c9f;', - code, - scope.value, - err - ); + // todo + console.error('%c eval error', code, runtimeScope.value, err); return undefined; } } @@ -46,11 +39,11 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime { return fn.bind(runtimeScope.value); } - function parseExprOrFn(value: AnyObject) { + function parseExprOrFn(value: PlainObject) { return processValue( value, - data => { - return isJsExpression(data) || isJsFunction(data); + (data) => { + return isJSExpression(data) || isJSFunction(data); }, (node: JSExpression | JSFunction) => { let v; @@ -65,7 +58,7 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime { return (node as any).mock; } return v; - } + }, ); } @@ -84,14 +77,14 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime { } export interface CodeScope { - readonly value: AnyObject; + readonly value: PlainObject; inject(name: string, value: any, force?: boolean): void; - setValue(value: AnyObject, replace?: boolean): void; - createSubScope(initValue: AnyObject): CodeScope; + setValue(value: PlainObject, replace?: boolean): void; + createSubScope(initValue?: PlainObject): CodeScope; } -export function createScope(initValue: AnyObject): CodeScope { +export function createScope(initValue: PlainObject = {}): CodeScope { const innerScope = { value: initValue }; const proxyValue = new Proxy(Object.create(null), { set(target, p, newValue, receiver) { @@ -120,7 +113,7 @@ export function createScope(initValue: AnyObject): CodeScope { innerScope.value[name] = value; } - function createSubScope(initValue: AnyObject) { + function createSubScope(initValue: PlainObject = {}) { const childScope = createScope(initValue); (childScope as any).__raw.__parent = innerScope; @@ -144,6 +137,8 @@ export function createScope(initValue: AnyObject): CodeScope { createSubScope, }; + Object.defineProperty(scope, Symbol.for(SYMBOL_SIGN), { get: () => true }); + // development env Object.defineProperty(scope, '__raw', { get: () => innerScope }); return scope; diff --git a/runtime/renderer-core/src/container.ts b/runtime/renderer-core/src/container.ts new file mode 100644 index 000000000..5a55479f6 --- /dev/null +++ b/runtime/renderer-core/src/container.ts @@ -0,0 +1,166 @@ +import type { + InstanceApi, + PlainObject, + ComponentTree, + InstanceDataSourceApi, + InstanceStateApi, +} from './types'; +import { type CodeScope, type CodeRuntime, createCodeRuntime, createScope } from './code-runtime'; +import { isJSFunction } from './utils/type-guard'; +import { type TextWidget, type ComponentWidget, createWidget } from './widget'; + +/** + * 根据低代码搭建协议的容器组件描述生成的容器实例 + */ +export interface Container { + readonly codeRuntime: CodeRuntime; + readonly instanceApiObject: InstanceApi; + + /** + * 获取协议中的 css 内容 + */ + getCssText(): string | undefined; + /** + * 调用生命周期方法 + */ + triggerLifeCycle(lifeCycleName: LifeCycleNameT, ...args: any[]): void; + /** + * 设置 ref 对应的组件实例, 提供给 scope.$() 方式使用 + */ + setInstance(ref: string, instance: InstanceT): void; + /** + * 移除 ref 对应的组件实例 + */ + removeInstance(ref: string, instance?: InstanceT): void; + + createWidgets(): (TextWidget | ComponentWidget)[]; +} + +export interface CreateContainerOptions { + supCodeScope?: CodeScope; + initScopeValue?: PlainObject; + componentsTree: ComponentTree; + stateCreator: (initalState: PlainObject) => InstanceStateApi; + // type todo + dataSourceCreator: (...args: any[]) => InstanceDataSourceApi; +} + +export function createContainer( + options: CreateContainerOptions, +): Container { + const { componentsTree, supCodeScope, initScopeValue, stateCreator, dataSourceCreator } = options; + + validContainerSchema(componentsTree); + + const instancesMap = new Map(); + const subScope = supCodeScope + ? supCodeScope.createSubScope(initScopeValue) + : createScope(initScopeValue); + const codeRuntime = createCodeRuntime(subScope); + + const initalState = codeRuntime.parseExprOrFn(componentsTree.state ?? {}); + const initalProps = codeRuntime.parseExprOrFn(componentsTree.props ?? {}); + + const stateApi = stateCreator(initalState); + const dataSourceApi = dataSourceCreator(componentsTree.dataSource, stateApi); + + const instanceApiObject: InstanceApi = Object.assign( + { + props: initalProps, + $(ref: string) { + const insArr = instancesMap.get(ref); + if (!insArr) return undefined; + + return insArr[0]; + }, + $$(ref: string) { + return instancesMap.get(ref) ?? []; + }, + }, + stateApi, + dataSourceApi, + ); + + if (componentsTree.methods) { + for (const [key, fn] of Object.entries(componentsTree.methods)) { + const customMethod = codeRuntime.createFnBoundScope(fn.value); + if (customMethod) { + instanceApiObject[key] = customMethod; + } + } + } + + const containerCodeScope = subScope.createSubScope(instanceApiObject); + + codeRuntime.bindingScope(containerCodeScope); + + function setInstanceByRef(ref: string, ins: InstanceT) { + let insArr = instancesMap.get(ref); + if (!insArr) { + insArr = []; + instancesMap.set(ref, insArr); + } + insArr!.push(ins); + } + + function removeInstanceByRef(ref: string, ins?: InstanceT) { + const insArr = instancesMap.get(ref); + if (insArr) { + if (ins) { + const idx = insArr.indexOf(ins); + if (idx > 0) insArr.splice(idx, 1); + } else { + instancesMap.delete(ref); + } + } + } + + function triggerLifeCycle(lifeCycleName: LifeCycleNameT, ...args: any[]) { + // keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象 + if ( + !componentsTree.lifeCycles || + !Object.keys(componentsTree.lifeCycles).includes(lifeCycleName) + ) { + return; + } + + const lifeCycleSchema = componentsTree.lifeCycles[lifeCycleName]; + if (isJSFunction(lifeCycleSchema)) { + const lifeCycleFn = codeRuntime.createFnBoundScope(lifeCycleSchema.value); + if (lifeCycleFn) { + lifeCycleFn.apply(containerCodeScope.value, args); + } + } + } + + return { + get codeRuntime() { + return codeRuntime; + }, + get instanceApiObject() { + return containerCodeScope.value as InstanceApi; + }, + + getCssText() { + return componentsTree.css; + }, + triggerLifeCycle, + + setInstance: setInstanceByRef, + removeInstance: removeInstanceByRef, + + createWidgets() { + if (!componentsTree.children) return []; + + return componentsTree.children.map((item) => createWidget(item)); + }, + }; +} + +const CONTAINTER_NAME = ['Page', 'Block', 'Component']; + +function validContainerSchema(schema: ComponentTree) { + if (!CONTAINTER_NAME.includes(schema.componentName)) { + throw Error('container schema not valid'); + } +} diff --git a/runtime/renderer-core/src/index.ts b/runtime/renderer-core/src/index.ts index e69de29bb..79822e6bb 100644 --- a/runtime/renderer-core/src/index.ts +++ b/runtime/renderer-core/src/index.ts @@ -0,0 +1,15 @@ +/* --------------- api -------------------- */ +export * from './api/app'; +export * from './api/component'; +export { createCodeRuntime, createScope } from './code-runtime'; +export { definePlugin } from './plugin'; +export { createWidget } from './widget'; +export { createContainer } from './container'; + +/* --------------- types ---------------- */ +export * from './types'; +export type { CodeRuntime, CodeScope } from './code-runtime'; +export type { Plugin, PluginSetupContext } from './plugin'; +export type { PackageManager, PackageLoader } from './package'; +export type { Container } from './container'; +export type { Widget, TextWidget, ComponentWidget } from './widget'; diff --git a/runtime/renderer-core/src/package.ts b/runtime/renderer-core/src/package.ts index e85bd55a7..2a756401e 100644 --- a/runtime/renderer-core/src/package.ts +++ b/runtime/renderer-core/src/package.ts @@ -1,13 +1,6 @@ -import { - type Package, - type ProCodeComponent, - type ComponentMap, - type LowCodeComponent, - isLowCodeComponentPackage, -} from '@alilc/runtime-shared'; +import { type Package, type ComponentMap, type LowCodeComponent } from './types'; -const packageStore: Map = ((window as any).__PACKAGE_STORE__ ??= - new Map()); +const packageStore: Map = ((window as any).__PACKAGE_STORE__ ??= new Map()); export interface PackageLoader { name?: string; @@ -31,9 +24,7 @@ export interface PackageManager { /** 解析组件映射 */ resolveComponentMaps(componentMaps: ComponentMap[]): void; /** 获取组件映射对象,key = componentName value = component */ - getComponentsNameRecord( - componentMaps?: ComponentMap[] - ): Record; + getComponentsNameRecord(componentMaps?: ComponentMap[]): Record; /** 通过组件名获取对应的组件 */ getComponent(componentName: string): C | undefined; /** 注册组件 */ @@ -48,8 +39,10 @@ export function createPackageManager(): PackageManager { async function addPackages(packages: Package[]) { for (const item of packages) { - const newId = item.package ?? item.id; - const isExist = packagesRef.some(_ => { + if (!item.package && !item.id) continue; + + const newId = item.package ?? item.id!; + const isExist = packagesRef.some((_) => { const itemId = _.package ?? _.id; return itemId === newId; }); @@ -58,7 +51,7 @@ export function createPackageManager(): PackageManager { packagesRef.push(item); if (!packageStore.has(newId)) { - const loader = packageLoaders.find(loader => loader.active(item)); + const loader = packageLoaders.find((loader) => loader.active(item)); if (!loader) continue; try { @@ -73,14 +66,14 @@ export function createPackageManager(): PackageManager { } function getPackageInfo(packageName: string) { - return packagesRef.find(p => p.package === packageName); + return packagesRef.find((p) => p.package === packageName); } function getLibraryByPackageName(packageName: string) { const packageInfo = getPackageInfo(packageName); if (packageInfo) { - return packageStore.get(packageInfo.package ?? packageInfo.id); + return packageStore.get(packageInfo.package ?? packageInfo.id!); } } @@ -90,31 +83,26 @@ export function createPackageManager(): PackageManager { function resolveComponentMaps(componentMaps: ComponentMap[]) { for (const map of componentMaps) { - if ((map as LowCodeComponent).devMode === 'lowCode') { - const packageInfo = packagesRef.find(_ => { + if (map.devMode === 'lowCode') { + const packageInfo = packagesRef.find((_) => { return _.id === (map as LowCodeComponent).id; }); - if (isLowCodeComponentPackage(packageInfo)) { + if (packageInfo) { componentsRecord[map.componentName] = packageInfo; } } else { - const npmInfo = map as ProCodeComponent; - - if (packageStore.has(npmInfo.package)) { - const library = packageStore.get(npmInfo.package); + if (packageStore.has(map.package!)) { + const library = packageStore.get(map.package!); // export { exportName } from xxx exportName === global.libraryName.exportName // export exportName from xxx exportName === global.libraryName.default || global.libraryName // export { exportName as componentName } from package // if exportName == null exportName === componentName; // const componentName = exportName.subName, if exportName empty subName donot use - const paths = - npmInfo.exportName && npmInfo.subName - ? npmInfo.subName.split('.') - : []; - const exportName = npmInfo.exportName ?? npmInfo.componentName; + const paths = map.exportName && map.subName ? map.subName.split('.') : []; + const exportName = map.exportName ?? map.componentName; - if (npmInfo.destructuring) { + if (map.destructuring) { paths.unshift(exportName); } @@ -123,7 +111,7 @@ export function createPackageManager(): PackageManager { result = result[path] || result; } - const recordName = npmInfo.componentName ?? npmInfo.exportName; + const recordName = map.componentName ?? map.exportName; componentsRecord[recordName] = result; } } @@ -152,7 +140,7 @@ export function createPackageManager(): PackageManager { getLibraryByPackageName, setLibraryByPackageName, addPackageLoader(loader) { - if (!loader.name || !packageLoaders.some(_ => _.name === loader.name)) { + if (!loader.name || !packageLoaders.some((_) => _.name === loader.name)) { packageLoaders.push(loader); } }, diff --git a/runtime/renderer-core/src/plugin.ts b/runtime/renderer-core/src/plugin.ts index 087f1ac48..849b2a08d 100644 --- a/runtime/renderer-core/src/plugin.ts +++ b/runtime/renderer-core/src/plugin.ts @@ -1,4 +1,5 @@ -import { type AppContext } from '../api/create-app-function'; +import { type AppContext } from './api/app'; +import { nonSetterProxy } from './utils/non-setter-proxy'; export interface Plugin { name: string; // 插件的 name 作为唯一标识,并不可重复。 @@ -14,24 +15,12 @@ export function createPluginManager(context: PluginSetupContext) { const installedPlugins: Plugin[] = []; let readyToInstallPlugins: Plugin[] = []; - const setupContext = new Proxy(context, { - get(target, p, receiver) { - return Reflect.get(target, p, receiver); - }, - set() { - return false; - }, - has(target, p) { - return Reflect.has(target, p); - }, - }); + const setupContext = nonSetterProxy(context); async function install(plugin: Plugin) { - if (installedPlugins.some(p => p.name === plugin.name)) return; + if (installedPlugins.some((p) => p.name === plugin.name)) return; - if ( - plugin.dependsOn?.some(dep => !installedPlugins.some(p => p.name === dep)) - ) { + if (plugin.dependsOn?.some((dep) => !installedPlugins.some((p) => p.name === dep))) { readyToInstallPlugins.push(plugin); return; } @@ -41,24 +30,22 @@ export function createPluginManager(context: PluginSetupContext) { // 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装 for (const item of readyToInstallPlugins) { - if ( - item.dependsOn?.every(dep => installedPlugins.some(p => p.name === dep)) - ) { + if (item.dependsOn?.every((dep) => installedPlugins.some((p) => p.name === dep))) { await item.setup(setupContext); installedPlugins.push(item); } } if (readyToInstallPlugins.length) { - readyToInstallPlugins = readyToInstallPlugins.filter(item => - installedPlugins.some(p => p.name === item.name) + readyToInstallPlugins = readyToInstallPlugins.filter((item) => + installedPlugins.some((p) => p.name === item.name), ); } } return { async add(plugin: Plugin) { - if (installedPlugins.find(item => item.name === plugin.name)) { + if (installedPlugins.find((item) => item.name === plugin.name)) { console.warn('该插件已安装'); return; } @@ -68,8 +55,6 @@ export function createPluginManager(context: PluginSetupContext) { }; } -export function definePlugin>( - plugin: P -) { +export function definePlugin>(plugin: P) { return plugin; } diff --git a/runtime/renderer-core/src/schema.ts b/runtime/renderer-core/src/schema.ts index 71c28dd95..83fb57e98 100644 --- a/runtime/renderer-core/src/schema.ts +++ b/runtime/renderer-core/src/schema.ts @@ -1,43 +1,33 @@ -import type { - ProjectSchema, - RootSchema, - ComponentMap, - PageSchema, -} from '@alilc/runtime-shared'; -import { throwRuntimeError } from './error'; +import type { Project, ComponentTree, ComponentMap, PageConfig } from './types'; +import { throwRuntimeError } from './utils/error'; import { set, get } from 'lodash-es'; -type AppSchemaType = ProjectSchema; - export interface AppSchema { - getComponentsTrees(): RootSchema[]; - addComponentsTree(tree: RootSchema): void; + getComponentsTrees(): ComponentTree[]; + addComponentsTree(tree: ComponentTree): void; removeComponentsTree(id: string): void; getComponentsMaps(): ComponentMap[]; addComponentsMap(componentName: ComponentMap): void; removeComponentsMap(componentName: string): void; - getPages(): PageSchema[]; - addPage(page: PageSchema): void; + getPages(): PageConfig[]; + addPage(page: PageConfig): void; removePage(id: string): void; - getByKey(key: K): AppSchemaType[K] | undefined; - updateByKey( + getByKey(key: K): Project[K] | undefined; + updateByKey( key: K, - updater: AppSchemaType[K] | ((value: AppSchemaType[K]) => AppSchemaType[K]) + updater: Project[K] | ((value: Project[K]) => Project[K]), ): void; getByPath(path: string | string[]): any; - updateByPath( - path: string | string[], - updater: any | ((value: any) => any) - ): void; + updateByPath(path: string | string[], updater: any | ((value: any) => any)): void; - find(predicate: (schema: AppSchemaType) => any): any; + find(predicate: (schema: Project) => any): any; } -export function createAppSchema(schema: ProjectSchema): AppSchema { +export function createAppSchema(schema: Project): AppSchema { if (!schema.version.startsWith('1.')) { throwRuntimeError('core', 'schema version must be 1.x.x'); } @@ -93,21 +83,13 @@ export function createAppSchema(schema: ProjectSchema): AppSchema { return get(schemaRef, path); }, updateByPath(path, updater) { - set( - schemaRef, - path, - typeof updater === 'function' ? updater(this.getByPath(path)) : updater - ); + set(schemaRef, path, typeof updater === 'function' ? updater(this.getByPath(path)) : updater); }, }; } -function addArrayItem>( - target: T[], - item: T, - comparison: string -) { - const idx = target.findIndex(_ => _[comparison] === item[comparison]); +function addArrayItem>(target: T[], item: T, comparison: string) { + const idx = target.findIndex((_) => _[comparison] === item[comparison]); if (idx > -1) { target.splice(idx, 1, item); } else { @@ -118,8 +100,8 @@ function addArrayItem>( function removeArrayItem>( target: T[], comparison: string, - comparisonValue: any + comparisonValue: any, ) { - const idx = target.findIndex(item => item[comparison] === comparisonValue); + const idx = target.findIndex((item) => item[comparison] === comparisonValue); if (idx > -1) target.splice(idx, 1); } diff --git a/runtime/renderer-core/src/types/common.ts b/runtime/renderer-core/src/types/common.ts new file mode 100644 index 000000000..0a3d55393 --- /dev/null +++ b/runtime/renderer-core/src/types/common.ts @@ -0,0 +1,3 @@ +export type AnyFunction = (...args: any[]) => any; + +export type PlainObject = Record; diff --git a/runtime/renderer-core/src/types/index.ts b/runtime/renderer-core/src/types/index.ts new file mode 100644 index 000000000..e23e98cf4 --- /dev/null +++ b/runtime/renderer-core/src/types/index.ts @@ -0,0 +1,5 @@ +export * from './common'; +export * from './material'; +export * from './specs/asset-spec'; +export * from './specs/lowcode-spec'; +export * from './specs/runtime-api'; diff --git a/runtime/renderer-core/src/types/material.ts b/runtime/renderer-core/src/types/material.ts new file mode 100644 index 000000000..a55513f7d --- /dev/null +++ b/runtime/renderer-core/src/types/material.ts @@ -0,0 +1,14 @@ +import { Package } from './specs/asset-spec'; +import { Project, ComponentMap } from './specs/lowcode-spec'; + +export interface ProCodeComponent extends Package { + package: string; + type: 'proCode'; +} + +export interface LowCodeComponent extends Package { + id: string; + type: 'lowCode'; + componentName: string; + schema: Project; +} diff --git a/runtime/renderer-core/src/types/specs/asset-spec.ts b/runtime/renderer-core/src/types/specs/asset-spec.ts new file mode 100644 index 000000000..22e28ec6e --- /dev/null +++ b/runtime/renderer-core/src/types/specs/asset-spec.ts @@ -0,0 +1,104 @@ +/** + * https://lowcode-engine.cn/site/docs/specs/assets-spec + * 低代码引擎资产包协议规范 for runtime + */ +import { Project } from './lowcode-spec'; + +export interface Package { + /** + * 唯一标识,与 package 必须要有一个存在值,如果为空,则以 package 为唯一标识 + */ + id?: string; + /** + * npm 包唯一标识,与 id 必须要有一个存在值 + */ + package?: string; + /** + * 包版本号 + */ + version: string; + /** + * 资源类型,默认为 proCode + */ + type?: 'proCode' | 'lowCode'; + /** + * 组件渲染态视图打包后的 CDN url 列表,包含 js 和 css + */ + urls?: string[]; + /** + * 组件多个渲染态视图打包后的 CDN url 列表,包含 js 和 css,优先级高于 urls + */ + advancedUrls?: ComplexUrls; + /** + * 组件编辑态视图打包后的 CDN url 列表,包含 js 和 css + */ + editUrls?: string[]; + /** + * 组件多个编辑态视图打包后的 CDN url 列表,包含 js 和 css,优先级高于 editUrls + */ + advancedEditUrls?: ComplexUrls; + /** + * 低代码组件的 schema 内容 + */ + schema?: Project; + /** + * 当前资源所依赖的其他资源包的 id 列表 + */ + deps?: string[]; + /** + * 指定当前资源加载的环境 + */ + loadEnv?: LoadEnv[]; + /** + * 当前资源是否是 external 资源 + */ + external?: boolean; + /** + * 作为全局变量引用时的名称,和 webpack output.library 字段含义一样,用来定义全局变量名 + */ + library: string; + /** + * 组件描述导出名字,可以通过 window[exportName] 获取到组件描述的 Object 内容; + */ + exportName?: string; + /** + * 标识当前 package 资源加载在 window.library 上的是否是一个异步对象 + */ + async?: boolean; + /** + * 标识当前 package 从其他 package 的导出方式 + */ + exportMode?: string; + /** + * 标识当前 package 内容是从哪个 package 导出来的 + */ + exportSourceId?: string; + /** + * 标识当前 package 是从 window 上的哪个属性导出来的 + */ + exportSourceLibrary?: string; +} + +/** + * 复杂 urls 结构,同时兼容简单结构和多模态结构 + */ +export type ComplexUrls = string[] | MultiModeUrls; + +/** + * 多模态资源 + */ +export interface MultiModeUrls { + /** + * 默认的资源 url + */ + default: string[]; + /** + * 其他模态资源的 url + */ + [mode: string]: string[]; +} + +/** + * 资源加载环境种类 + */ +export type LoadEnv = 'design' | 'runtime'; diff --git a/runtime/renderer-core/src/types/specs/lowcode-spec.ts b/runtime/renderer-core/src/types/specs/lowcode-spec.ts new file mode 100644 index 000000000..c8e31a348 --- /dev/null +++ b/runtime/renderer-core/src/types/specs/lowcode-spec.ts @@ -0,0 +1,345 @@ +/** + * https://lowcode-engine.cn/site/docs/specs/lowcode-spec + * 低代码引擎搭建协议规范 + */ + +/** + * https://lowcode-engine.cn/site/docs/specs/lowcode-spec#2-%E5%8D%8F%E8%AE%AE%E7%BB%93%E6%9E%84 + * 应用协议 + */ +export interface Project { + /** + * 当前协议版本号 + */ + version: string; + /** + * 组件映射关系 + */ + componentsMap: ComponentMap[]; + /** + * 描述模版/页面/区块/低代码业务组件的组件树 + */ + componentsTree: ComponentTree[]; + /** + * 工具类扩展映射关系 + */ + utils?: Util[]; + /** + * 国际化语料 + */ + i18n?: I18nMap; + /** + * 应用范围内的全局常量 + */ + constants?: ConstantsMap; + /** + * 应用范围内的全局样式 + * 用于描述在应用范围内的全局样式,比如 reset.css 等。 + */ + css?: string; + /** + * 当前应用配置信息 + */ + config?: Record; + /** + * 当前应用元数据信息 + */ + meta?: Record; + /** + * 当前应用的公共数据源 + * @deprecated + */ + dataSource?: unknown; + /** + * 当前应用的路由配置信息 + */ + router?: RouterConfig; + /** + * 当前应用的所有页面信息 + */ + pages?: PageConfig[]; +} + +/** + * https://lowcode-engine.cn/site/docs/specs/lowcode-spec#22-%E7%BB%84%E4%BB%B6%E6%98%A0%E5%B0%84%E5%85%B3%E7%B3%BBa + * 协议中用于描述 componentName 到公域组件映射关系的规范。 + */ +export interface ComponentMap { + /** + * 协议中的组件名,唯一性,对应包导出的组件名,是一个有效的 JS 标识符,而且是大写字母打头 + */ + componentName: string; + /** + * npm 公域的 package name + */ + package?: string; + /** + * package version + */ + version?: string; + /** + * 使用解构方式对模块进行导出 + */ + destructuring?: boolean; + /** + * 包导出的组件名 + */ + exportName?: string; + /** + * 下标子组件名称 + */ + subName?: string; + /** + * 包导出组件入口文件路径 + */ + main?: string; + /** + * proCode or lowCode + */ + devMode?: string; +} + +/** + * 组件树描述 + * 协议中用于描述搭建出来的组件树结构的规范,整个组件树的描述由组件结构&容器结构两种结构嵌套构成。 + */ +export type ComponentTree = + ComponentTreeContainer; + +/** + * 容器结构描述 (A) + * 容器是一类特殊的组件,在组件能力基础上增加了对生命周期对象、自定义方法、样式文件、数据源等信息的描述。 + */ +export interface ComponentTreeContainer + extends Omit { + componentName: 'Page' | 'Block' | 'Component'; + /** + * 文件名称 + */ + fileName: string; + /** + * 容器初始数据 + */ + state?: Record; + /** + * 样式属性 + */ + css?: string; + /** + * 生命周期对象 + */ + lifeCycles?: { + [name in LifeCycleNameT]: JSFunction; + }; + /** + * 自定义方法对象 + */ + methods?: { + [name: string]: JSFunction; + }; + /** + * 数据源对象 + * type todo + */ + dataSource?: any; +} + +/** + * 组件结构描述(A) + */ +export interface ComponentTreeNode { + /** + * 组件唯一标识 + */ + id?: string; + /** + * 组件名称 + */ + componentName: string; + /** + * 组件属性对象 + */ + props?: ComponentTreeNodeProps; + /** + * 选填,根据表达式结果判断是否渲染物料; + */ + condition?: boolean | JSExpression; + /** + * 循环数据 + */ + loop?: any[] | JSExpression; + /** + * 循环迭代对象、索引名称,默认为 ["item", "index"] + */ + loopArgs?: [string, string]; + /** + * 子组件 + */ + children?: NodeType[]; +} + +/** + * Props 结构描述 + */ +export interface ComponentTreeNodeProps { + /** 组件 ID */ + id?: string | JSExpression; + /** 组件样式类名 */ + className?: string; + /** 组件内联样式 */ + style?: JSONObject | JSExpression; + /** 组件 ref 名称 */ + ref?: string | JSExpression; + + [key: string]: any; +} + +/** + * https://lowcode-engine.cn/site/docs/specs/lowcode-spec#24-%E5%B7%A5%E5%85%B7%E7%B1%BB%E6%89%A9%E5%B1%95%E6%8F%8F%E8%BF%B0aa + * 用于描述物料开发过程中,自定义扩展或引入的第三方工具类(例如:lodash 及 moment),增强搭建基础协议的扩展性,提供通用的工具类方法的配置方案及调用 API。 + */ +export interface Util { + name: string; + type: 'npm' | 'function'; + content: ComponentMap | JSFunction; +} + +/** + * https://lowcode-engine.cn/site/docs/specs/lowcode-spec#25-%E5%9B%BD%E9%99%85%E5%8C%96%E5%A4%9A%E8%AF%AD%E8%A8%80%E6%94%AF%E6%8C%81aa + * 国际化多语言支持 + */ +export interface I18nMap { + [locale: string]: Record; +} + +/** + * 应用范围内的全局常量(AA) + * 用于描述在整个应用内通用的全局常量,比如请求 API 的域名、环境等。 + */ +export interface ConstantsMap { + [key: string]: JSONValue; +} + +/** + * https://lowcode-engine.cn/site/docs/specs/lowcode-spec#211-%E5%BD%93%E5%89%8D%E5%BA%94%E7%94%A8%E7%9A%84%E8%B7%AF%E7%94%B1%E4%BF%A1%E6%81%AFaa + * 当前应用的路由信息(AA) + * 用于描述当前应用的路径 - 页面的关系。通过声明路由信息,应用能够在不同的路径里显示对应的页面。 + */ +export interface RouterConfig { + /** + * 应用根路径 + */ + baseName: string; + /** + * 路由的 history 模式 + */ + historyMode: 'browser' | 'hash' | 'memory'; + /** + * 路由对象组,路径与页面的关系对照组 + */ + routes: RouteRecord[]; +} + +/** + * Route (路由记录)结构描述 + * 路由记录,路径与页面的关系对照。Route 的结构说明: + */ +export interface RouteRecord { + /** + * 该路径项的名称 + */ + name?: string; + /** + * 路径 + */ + path: string; + /** + * 路径对应的页面 ID,page 与 redirect 字段中必须要有有一个存在 + */ + page?: string; + /** + * 此路径需要重定向到的路由信息,page 与 redirect 字段中必须要有有一个存在 + */ + redirect?: string | object | JSFunction; + /** + * 子路由 + */ + children?: RouteRecord[]; +} + +/** + * 当前应用的页面信息(AA) + * 用于描述当前应用的页面信息,比如页面对应的低代码搭建内容、页面标题、页面配置等。 + * 在一些比较复杂的场景下,声明一层页面映射关系,以支持页面声明更多信息与配置,同时能够支持不同类型的产物。 + */ +export interface PageConfig { + /** + * 页面 id + */ + id: string; + /** + * 页面类型,如 componentsTree,package,默认为 componentsTree + */ + type?: string; + /** + * 映射的 id,根据页面的类型 type 判断及确定目标的 id + */ + mappingId: string; + /** + * 页面元信息 + */ + meta?: JSONObject; + /** + * 页面配置 + */ + config?: JSONObject; +} + +export type JSONValue = number | string | boolean | null; + +export interface JSONObject { + [key: string]: JSONValue | JSONObject | JSONObject[]; +} + +/** + * 节点类型(A) + * 通常用于描述组件的某一个属性为 ReactNode 或 Function-Return-ReactNode 的场景。 + */ +export interface JSSlot { + type: 'JSSlot'; + value: 1; + params?: string[]; +} + +/** + * 事件函数类型(A) + */ +export interface JSFunction { + type: 'JSFunction'; + value: string; +} + +/** + * 变量类型(A) + */ +export interface JSExpression { + type: 'JSExpression'; + value: string; +} + +/** + * 国际化多语言类型(AA) + */ +export interface I18nNode { + type: 'i18n'; + /** + * i18n 结构中字段的 key 标识符 + */ + key: string; + /** + * 语料为字符串模板时的变量内容 + */ + params?: Record; +} + +export type NodeType = string | JSExpression | I18nNode | ComponentTreeNode; diff --git a/runtime/renderer-core/src/types/specs/runtime-api.ts b/runtime/renderer-core/src/types/specs/runtime-api.ts new file mode 100644 index 000000000..d092dc8c2 --- /dev/null +++ b/runtime/renderer-core/src/types/specs/runtime-api.ts @@ -0,0 +1,75 @@ +import { AnyFunction, PlainObject } from '../common'; + +/** + * 在上述事件类型描述和变量类型描述中,在函数或 JS 表达式内,均可以通过 this 对象获取当前组件所在容器的实例化对象 + * 在搭建场景下的渲染模块和出码模块实现上,统一约定了该实例化 this 对象下所挂载的最小 API 集合, + * 以保障搭建协议具备有一致的数据流和事件上下文 + */ +export interface InstanceApi extends InstanceStateApi, InstanceDataSourceApi { + /** + * 容器的 props 对象 + */ + props?: PlainObject; + /** + * ref 对应组件上配置的 ref 属性,用于唯一标识一个组件;若有同名的,则会返回第一个匹配的。 + * @param ref 组件标识 + */ + $(ref: string): InstanceT | undefined; + /** + * ref 对应组件上配置的 ref 属性,用于唯一标识一个组件;总是返回一个数组,里面是所有匹配 ref 的组件的引用。 + * @param ref 组件标识 + */ + $$(ref: string): InstanceT[]; + + [methodName: string]: any; +} + +export interface InstanceStateApi { + /** + * 实例的数据对象 state + */ + state: Readonly; + /** + * 实例更新数据的方法 + * like React.Component.setState + */ + setState( + newState: ((prevState: Readonly) => Pick | S | null) | (Pick | S | null), + callback?: () => void, + ): void; +} + +export interface InstanceDataSourceApi { + /** + * 实例的数据源对象 Map + */ + dataSourceMap: any; + /** + * 实例的初始化异步数据请求重载 + */ + reloadDataSource: () => void; +} + +export interface UtilsApi { + utils: Record; +} + +export interface IntlApi { + /** + * 返回语料字符串 + * @param i18nKey 语料的标识符 + * @param params 可选,是用来做模版字符串替换 + */ + i18n(i18nKey: string, params?: Record): string; + /** + * 返回当前环境语言 + */ + getLocale(): string; + /** + * 设置当前环境语言 + * @param locale 环境语言 + */ + setLocale(locale: string): void; +} + +export interface RouterApi {} diff --git a/runtime/renderer-core/src/error.ts b/runtime/renderer-core/src/utils/error.ts similarity index 58% rename from runtime/renderer-core/src/error.ts rename to runtime/renderer-core/src/utils/error.ts index 83805ec07..73e338daa 100644 --- a/runtime/renderer-core/src/error.ts +++ b/runtime/renderer-core/src/utils/error.ts @@ -1,11 +1,14 @@ -import { appBoosts } from './boosts'; +import { appBoosts } from '../boosts'; export type ErrorType = string; export class RuntimeError extends Error { - constructor(public type: ErrorType, message: string) { + constructor( + public type: ErrorType, + message: string, + ) { super(message); - appBoosts.hooks.call(`app:error`, this); + appBoosts.hookStore.call(`app:error`, this); } } diff --git a/runtime/renderer-core/src/utils/hook.ts b/runtime/renderer-core/src/utils/hook.ts index c0215e1b6..d47e5031f 100644 --- a/runtime/renderer-core/src/utils/hook.ts +++ b/runtime/renderer-core/src/utils/hook.ts @@ -1,11 +1,43 @@ -import { useCallbacks, type Callback } from '@alilc/runtime-shared'; +import type { AnyFunction } from '../types'; + +export type EventName = string | number | symbol; + +export function useEvent() { + let events: T[] = []; + + function add(fn: T) { + events.push(fn); + + return () => { + events = events.filter((e) => e !== fn); + }; + } + + function remove(fn: T) { + events = events.filter((f) => fn !== f); + } + + function list() { + return [...events]; + } + + return { + add, + remove, + list, + clear() { + events.length = 0; + }, + }; +} + +export type Event = ReturnType>; + +export type HookCallback = (...args: any) => Promise | any; -export type HookCallback = (...args: any) => Promise | void; type HookKeys = keyof T & PropertyKey; -type InferCallback = HT[HN] extends HookCallback - ? HT[HN] - : never; +type InferCallback = HT[HN] extends HookCallback ? HT[HN] : never; declare global { interface Console { @@ -18,19 +50,16 @@ declare global { // https://developer.chrome.com/blog/devtools-modern-web-debugging/#linked-stack-traces type CreateTask = typeof console.createTask; -const defaultTask: ReturnType = { run: fn => fn() }; +const defaultTask: ReturnType = { run: (fn) => fn() }; const _createTask: CreateTask = () => defaultTask; -const createTask = - typeof console.createTask !== 'undefined' ? console.createTask : _createTask; +const createTask = typeof console.createTask !== 'undefined' ? console.createTask : _createTask; -export interface Hooks< +export interface HookStore< HooksT extends Record = Record, - HookNameT extends HookKeys = HookKeys + HookNameT extends HookKeys = HookKeys, > { - hook( - name: NameT, - fn: InferCallback - ): () => void; + hook(name: NameT, fn: InferCallback): () => void; + call( name: NameT, ...args: Parameters> @@ -43,29 +72,28 @@ export interface Hooks< name: NameT, ...args: Parameters> ): Promise; - remove( - name: NameT, - fn?: InferCallback - ): void; + + remove(name: NameT, fn?: InferCallback): void; + + clear(name?: NameT): void; + + getHooks(name: NameT): InferCallback[] | undefined; } -export function createHooks< +export function createHookStore< HooksT extends Record = Record, - HookNameT extends HookKeys = HookKeys ->(): Hooks { - const hooksMap = new Map>(); + HookNameT extends HookKeys = HookKeys, +>(): HookStore { + const hooksMap = new Map>(); - function hook( - name: NameT, - fn: InferCallback - ) { + function hook(name: NameT, fn: InferCallback) { if (!name || typeof fn !== 'function') { return () => {}; } let hooks = hooksMap.get(name); if (!hooks) { - hooks = useCallbacks(); + hooks = useEvent(); hooksMap.set(name, hooks); } @@ -92,9 +120,8 @@ export function createHooks< const task = createTask(name.toString()); return hooks.reduce( - (promise, hookFunction) => - promise.then(() => task.run(() => hookFunction(...args))), - Promise.resolve() + (promise, hookFunction) => promise.then(() => task.run(() => hookFunction(...args))), + Promise.resolve(), ); } @@ -104,19 +131,16 @@ export function createHooks< ) { const hooks = hooksMap.get(name)?.list() ?? []; const task = createTask(name.toString()); - return Promise.all(hooks.map(hook => task.run(() => hook(...args)))); + return Promise.all(hooks.map((hook) => task.run(() => hook(...args)))); } - function remove( - name: NameT, - fn?: InferCallback - ) { + function remove(name: NameT, fn?: InferCallback) { const hooks = hooksMap.get(name); if (!hooks) return; if (fn) { hooks.remove(fn); - if (hooks.list.length === 0) { + if (hooks.list().length === 0) { hooksMap.delete(name); } } else { @@ -124,11 +148,30 @@ export function createHooks< } } + function clear(name?: NameT) { + if (name) { + remove(name); + } else { + hooksMap.clear(); + } + } + + function getHooks( + name: NameT, + ): InferCallback[] | undefined { + return hooksMap.get(name)?.list() as InferCallback[] | undefined; + } + return { hook, + call, callAsync, callParallel, + remove, + clear, + + getHooks, }; } diff --git a/runtime/renderer-core/src/utils/non-setter-proxy.ts b/runtime/renderer-core/src/utils/non-setter-proxy.ts new file mode 100644 index 000000000..28314ee62 --- /dev/null +++ b/runtime/renderer-core/src/utils/non-setter-proxy.ts @@ -0,0 +1,13 @@ +export function nonSetterProxy(target: T) { + return new Proxy(target, { + get(target, p, receiver) { + return Reflect.get(target, p, receiver); + }, + set() { + return false; + }, + has(target, p) { + return Reflect.has(target, p); + }, + }); +} diff --git a/runtime/renderer-core/src/utils/type-guard.ts b/runtime/renderer-core/src/utils/type-guard.ts new file mode 100644 index 000000000..8cfa4e499 --- /dev/null +++ b/runtime/renderer-core/src/utils/type-guard.ts @@ -0,0 +1,18 @@ +import type { JSExpression, JSFunction, I18nNode } from '../types'; +import { isPlainObject } from 'lodash-es'; + +export function isJSExpression(v: unknown): v is JSExpression { + return ( + isPlainObject(v) && (v as any).type === 'JSExpression' && typeof (v as any).value === 'string' + ); +} + +export function isJSFunction(v: unknown): v is JSFunction { + return ( + isPlainObject(v) && (v as any).type === 'JSFunction' && typeof (v as any).value === 'string' + ); +} + +export function isI18nNode(v: unknown): v is I18nNode { + return isPlainObject(v) && (v as any).type === 'i18n' && typeof (v as any).key === 'string'; +} diff --git a/runtime/renderer-core/src/validate/schema.ts b/runtime/renderer-core/src/validate/schema.ts deleted file mode 100644 index 7d850cc65..000000000 --- a/runtime/renderer-core/src/validate/schema.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type RootSchema } from '@alilc/runtime-shared'; - -const CONTAINTER_NAME = ['Page', 'Block', 'Component']; - -export function validateContainerSchema(schema: RootSchema): boolean { - if (!CONTAINTER_NAME.includes(schema.componentName)) { - return false; - } - - return true; -} diff --git a/runtime/renderer-core/src/widget.ts b/runtime/renderer-core/src/widget.ts new file mode 100644 index 000000000..be3c06308 --- /dev/null +++ b/runtime/renderer-core/src/widget.ts @@ -0,0 +1,92 @@ +import type { NodeType, ComponentTreeNode, ComponentTreeNodeProps } from './types'; +import { isJSExpression, isI18nNode } from './utils/type-guard'; + +export class Widget { + protected _raw: Data; + protected proxyElements: Element[] = []; + protected renderObject: Element | undefined; + + constructor(data: Data) { + this._raw = data; + this.init(); + } + + protected init() {} + + get raw() { + return this._raw; + } + + setRenderObject(el: Element) { + this.renderObject = el; + } + getRenderObject() { + return this.renderObject; + } + + addProxyELements(el: Element) { + this.proxyElements.push(el); + } + + build(builder: (elements: Element[]) => Element) { + return builder(this.proxyElements); + } +} + +export type TextWidgetData = Exclude; +export type TextWidgetType = 'string' | 'expression' | 'i18n'; + +export class TextWidget extends Widget { + type: TextWidgetType = 'string'; + + protected init() { + if (isJSExpression(this.raw)) { + this.type = 'expression'; + } else if (isI18nNode(this.raw)) { + this.type = 'i18n'; + } + } +} + +export class ComponentWidget extends Widget { + private _children: (TextWidget | ComponentWidget)[] = []; + private _propsValue: ComponentTreeNodeProps = {}; + + protected init() { + if (this._raw.props) { + this._propsValue = this._raw.props; + } + if (this._raw.children) { + this._children = this._raw.children.map((child) => createWidget(child)); + } + } + + get componentName() { + return this.raw.componentName; + } + get props() { + return this._propsValue; + } + get children() { + return this._children; + } + get condition() { + return this._raw.condition ?? true; + } + get loop() { + return this._raw.loop; + } + get loopArgs() { + return this._raw.loopArgs ?? ['item', 'index']; + } +} + +export function createWidget(data: NodeType) { + if (typeof data === 'string' || isJSExpression(data) || isI18nNode(data)) { + return new TextWidget(data); + } else if (data.componentName) { + return new ComponentWidget(data); + } + + throw Error(`unknown node data: ${JSON.stringify(data)}`); +} diff --git a/runtime/renderer-core/tests/api/app.spec.ts b/runtime/renderer-core/tests/api/app.spec.ts new file mode 100644 index 000000000..fb746fd31 --- /dev/null +++ b/runtime/renderer-core/tests/api/app.spec.ts @@ -0,0 +1,50 @@ +import { describe, it, expect, vi } from 'vitest'; +import { createAppFunction } from '../../src/api/app'; +import { definePlugin } from '../../src/plugin'; + +describe('createAppFunction', () => { + it('should require a function argument that returns an render object.', () => { + expect(() => createAppFunction(undefined as any)).rejects.toThrowError(); + }); + + it('should return a function', () => { + const createApp = createAppFunction(async () => { + return { + appBase: { + mount(el) {}, + unmount() {}, + }, + }; + }); + + expect({ createApp }).toEqual({ createApp: expect.any(Function) }); + }); + + it('should construct app object', () => { + expect('').toBe(''); + }); + + it('should plugin inited when app created', async () => { + const plugin = definePlugin({ + name: 'test', + setup() {}, + }); + const spy = vi.spyOn(plugin, 'setup'); + + const createApp = createAppFunction(async () => { + return { + appBase: { + mount(el) {}, + unmount() {}, + }, + }; + }); + + await createApp({ + schema: {}, + plugins: [plugin], + }); + + expect(spy).toHaveBeenCalled(); + }); +}); diff --git a/runtime/renderer-core/tests/api/component.spec.ts b/runtime/renderer-core/tests/api/component.spec.ts new file mode 100644 index 000000000..ad100a576 --- /dev/null +++ b/runtime/renderer-core/tests/api/component.spec.ts @@ -0,0 +1,5 @@ +import { describe, it, expect } from 'vitest'; + +describe('createComponentFunction', () => { + it('', () => {}); +}); diff --git a/runtime/renderer-core/tests/boosts.spec.ts b/runtime/renderer-core/tests/boosts.spec.ts new file mode 100644 index 000000000..3486eb29e --- /dev/null +++ b/runtime/renderer-core/tests/boosts.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; +import { appBoosts } from '../src/boosts'; + +describe('appBoosts', () => { + it('should add value successfully', () => { + appBoosts.add('test', 1); + expect(appBoosts.value.test).toBe(1); + }); + + it('should clear removed value', () => { + appBoosts.add('test', 1); + expect(appBoosts.value.test).toBe(1); + + appBoosts.remove('test'); + expect(appBoosts.value.test).toBeUndefined(); + }); +}); diff --git a/runtime/renderer-core/tests/code-runtime.spec.ts b/runtime/renderer-core/tests/code-runtime.spec.ts new file mode 100644 index 000000000..81470aec3 --- /dev/null +++ b/runtime/renderer-core/tests/code-runtime.spec.ts @@ -0,0 +1 @@ +import {} from 'vitest'; diff --git a/runtime/renderer-core/tests/package.spec.ts b/runtime/renderer-core/tests/package.spec.ts new file mode 100644 index 000000000..e130a9d59 --- /dev/null +++ b/runtime/renderer-core/tests/package.spec.ts @@ -0,0 +1,2 @@ +import { expect } from 'vitest'; +import { createPackageManager } from '../src/package'; diff --git a/runtime/renderer-core/tests/plugin.spec.ts b/runtime/renderer-core/tests/plugin.spec.ts new file mode 100644 index 000000000..0af367ee7 --- /dev/null +++ b/runtime/renderer-core/tests/plugin.spec.ts @@ -0,0 +1,12 @@ +import { describe, it, expect, expectTypeOf } from 'vitest'; +import { definePlugin, type Plugin, createPluginManager } from '../src/plugin'; + +describe('createPluginManager', () => { + it('should install plugin successfully', () => {}); + + it('should install plugins when deps installed', () => {}); +}); + +describe('definePlugin', () => { + it('should return a plugin', () => {}); +}); diff --git a/runtime/renderer-core/tests/utils/hook.spec.ts b/runtime/renderer-core/tests/utils/hook.spec.ts new file mode 100644 index 000000000..266632ce4 --- /dev/null +++ b/runtime/renderer-core/tests/utils/hook.spec.ts @@ -0,0 +1,169 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { useEvent, createHookStore, type HookStore } from '../../src/utils/hook'; + +describe('event', () => { + it("event's listener ops", () => { + const event = useEvent(); + const fn = () => {}; + event.add(fn); + + expect(event.list().includes(fn)).toBeTruthy(); + + event.remove(fn); + + expect(event.list().includes(fn)).toBeFalsy(); + + event.add(fn); + + expect(event.list().includes(fn)).toBeTruthy(); + + event.clear(); + + expect(event.list().includes(fn)).toBeFalsy(); + }); +}); + +describe('hooks', () => { + let hookStore: HookStore; + + beforeEach(() => { + hookStore = createHookStore(); + }); + + it('should register hook successfully', () => { + const fn = () => {}; + hookStore.hook('test', fn); + + expect(hookStore.getHooks('test')).toContain(fn); + }); + + it('should ignore empty hook', () => { + hookStore.hook('', () => {}); + hookStore.hook(undefined as any, () => {}); + + expect(hookStore.getHooks('')).toBeUndefined(); + expect(hookStore.getHooks(undefined as any)).toBeUndefined(); + }); + + it('should ignore not function hook', () => { + hookStore.hook('test', 1 as any); + hookStore.hook('test', undefined as any); + + expect(hookStore.getHooks('test')).toBeUndefined(); + }); + + it('should call registered hook', () => { + const spy = vi.fn(); + + hookStore.hook('test', spy); + hookStore.call('test'); + + expect(spy).toHaveBeenCalled(); + }); + + it('callAsync: should sequential call registered async hook', async () => { + let count = 0; + const counts: number[] = []; + const fn = async () => { + counts.push(count++); + }; + + hookStore.hook('test', fn); + hookStore.hook('test', fn); + + await hookStore.callAsync('test'); + + expect(counts).toEqual([0, 1]); + }); + + it('callParallel: should parallel call registered async hook', async () => { + let count = 0; + + const sleep = (delay: number) => { + return new Promise((resolve) => { + setTimeout(resolve, delay); + }); + }; + + hookStore.hook('test', () => { + count++; + }); + hookStore.hook('test', async () => { + await sleep(500); + count++; + }); + hookStore.hook('test', async () => { + await sleep(1000); + expect(count).toBe(2); + }); + + await hookStore.callParallel('test'); + }); + + it('should throw hook error', async () => { + const error = new Error('Hook Error'); + hookStore.hook('test', () => { + throw error; + }); + expect(() => hookStore.call('test')).toThrow(error); + }); + + it('should return a self-removal function', async () => { + const spy = vi.fn(); + const remove = hookStore.hook('test', spy); + + hookStore.call('test'); + + expect(spy).toBeCalledTimes(1); + + remove(); + + hookStore.call('test'); + + expect(spy).toBeCalledTimes(1); + }); + + it('should clear removed hooks', () => { + const result: number[] = []; + + const fn1 = () => result.push(1); + const fn2 = () => result.push(2); + + hookStore.hook('test', fn1); + hookStore.hook('test', fn2); + hookStore.call('test'); + + expect(result).toHaveLength(2); + expect(result).toEqual([1, 2]); + + hookStore.remove('test', fn1); + hookStore.call('test'); + + expect(result).toHaveLength(3); + expect(result).toEqual([1, 2, 2]); + + hookStore.remove('test'); + hookStore.call('test'); + + expect(result).toHaveLength(3); + expect(result).toEqual([1, 2, 2]); + }); + + it('should clear ops works', () => { + hookStore.hook('test1', () => {}); + hookStore.hook('test2', () => {}); + + expect(hookStore.getHooks('test1')).toHaveLength(1); + expect(hookStore.getHooks('test2')).toHaveLength(1); + + hookStore.clear('test1'); + + expect(hookStore.getHooks('test1')).toBeUndefined(); + expect(hookStore.getHooks('test2')).toHaveLength(1); + + hookStore.clear(); + + expect(hookStore.getHooks('test1')).toBeUndefined(); + expect(hookStore.getHooks('test2')).toBeUndefined(); + }); +}); diff --git a/runtime/renderer-core/tests/utils/non-setter-proxy.spec.ts b/runtime/renderer-core/tests/utils/non-setter-proxy.spec.ts new file mode 100644 index 000000000..3e6f871c4 --- /dev/null +++ b/runtime/renderer-core/tests/utils/non-setter-proxy.spec.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from 'vitest'; +import { nonSetterProxy } from '../../src/utils/non-setter-proxy'; + +describe('nonSetterProxy', () => { + it('should non setter on proxy', () => { + const target = { a: 1 }; + const proxy = nonSetterProxy(target); + + expect(() => ((proxy as any).b = 1)).toThrowError(/trap returned falsish for property 'b'/); + }); + + it('should correct value when getter', () => { + const target = { a: 1 }; + const proxy = nonSetterProxy(target); + + expect(proxy.a).toBe(1); + expect('a' in proxy).toBeTruthy(); + }); +}); diff --git a/runtime/renderer-core/vitest.config.ts b/runtime/renderer-core/vitest.config.ts index e69de29bb..116f3131e 100644 --- a/runtime/renderer-core/vitest.config.ts +++ b/runtime/renderer-core/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['tests/**/*.spec.ts'] + } +}) diff --git a/runtime/renderer-react/package.json b/runtime/renderer-react/package.json new file mode 100644 index 000000000..6ca770984 --- /dev/null +++ b/runtime/renderer-react/package.json @@ -0,0 +1,33 @@ +{ + "name": "@alilc/renderer-react", + "version": "2.0.0-beta.0", + "description": "react renderer for ali lowcode engine", + "type": "module", + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme", + "license": "MIT", + "scripts": { + "build": "", + "test": "vitest" + }, + "dependencies": { + "@vue/reactivity": "^3.4.21", + "@alilc/renderer-core": "^2.0.0-beta.0", + "lodash-es": "^4.17.21", + "hoist-non-react-statics": "^3.3.2", + "use-sync-external-store": "^1.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@testing-library/react": "^14.2.0", + "@types/lodash-es": "^4.17.12", + "@types/react": "^18.2.67", + "@types/react-dom": "^18.2.22", + "jsdom": "^24.0.0" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +} diff --git a/runtime/renderer-react/src/api/create-app.tsx b/runtime/renderer-react/src/api/create-app.tsx new file mode 100644 index 000000000..c03af5d36 --- /dev/null +++ b/runtime/renderer-react/src/api/create-app.tsx @@ -0,0 +1,62 @@ +import { + type App, + type RenderBase, + createAppFunction, + type AppOptionsBase, +} from '@alilc/renderer-core'; +import { type ComponentType } from 'react'; +import { type Root, createRoot } from 'react-dom/client'; +import { createRenderer } from '../renderer'; +import AppComponent from '../components/app'; +import { intlPlugin } from '../plugins/intl'; +import { globalUtilsPlugin } from '../plugins/utils'; +import { initRouter } from '../router'; + +export interface AppOptions extends AppOptionsBase { + dataSourceCreator: DataSourceCreator; + faultComponent?: ComponentType; +} + +export interface ReactRender extends RenderBase {} + +export type ReactApp = App; + +export const createApp = createAppFunction(async (context, options) => { + const renderer = createRenderer(); + const appContext = { ...context, renderer }; + + initRouter(appContext); + + options.plugins ??= []; + options.plugins!.unshift(globalUtilsPlugin, intlPlugin); + + // set config + if (options.faultComponent) { + context.config.set('faultComponent', options.faultComponent); + } + context.config.set('dataSourceCreator', options.dataSourceCreator); + + let root: Root | undefined; + + const reactRender: ReactRender = { + async mount(el) { + if (root) { + return; + } + + root = createRoot(el); + root.render(); + }, + unmount() { + if (root) { + root.unmount(); + root = undefined; + } + }, + }; + + return { + renderBase: reactRender, + renderer, + }; +}); diff --git a/runtime/react-renderer/src/api/create-component.tsx b/runtime/renderer-react/src/api/create-component.tsx similarity index 82% rename from runtime/react-renderer/src/api/create-component.tsx rename to runtime/renderer-react/src/api/create-component.tsx index f1350c97a..3567ba46e 100644 --- a/runtime/react-renderer/src/api/create-component.tsx +++ b/runtime/renderer-react/src/api/create-component.tsx @@ -11,12 +11,12 @@ import { type CodeRuntime, createCodeRuntime, } from '@alilc/runtime-core'; +import { isPlainObject } from 'lodash-es'; import { type AnyObject, type Package, type JSSlot, type JSFunction, - isPlainObject, isJsExpression, isJsSlot, isLowCodeComponentPackage, @@ -79,8 +79,7 @@ export interface ConvertedTreeNode { setReactNode(element: ReactNode): void; } -export interface CreateComponentOptions> - extends ComponentOptionsBase { +export interface CreateComponentOptions> extends ComponentOptionsBase { displayName?: string; beforeNodeCreateComponent?(convertedNode: ConvertedTreeNode): void; @@ -124,7 +123,7 @@ export const createComponent = createComponentFunction< function getComponentByName( componentName: string, - componentsRecord: Record | Package> + componentsRecord: Record | Package>, ) { const Component = componentsRecord[componentName]; if (!Component) { @@ -154,7 +153,7 @@ export const createComponent = createComponentFunction< function createConvertedTreeNode( rawNode: ComponentTreeNode, - codeRuntime: CodeRuntime + codeRuntime: CodeRuntime, ): ConvertedTreeNode { let elementValue: ReactNode = null; @@ -172,10 +171,7 @@ export const createComponent = createComponentFunction< }; if (rawNode.type === 'component') { - node.rawComponent = getComponentByName( - rawNode.data.componentName, - componentsRecord - ); + node.rawComponent = getComponentByName(rawNode.data.componentName, componentsRecord); } return node; @@ -185,7 +181,7 @@ export const createComponent = createComponentFunction< node: ComponentTreeNode, codeRuntime: CodeRuntime, instance: ContainerInstance, - componentsRecord: Record | Package> + componentsRecord: Record | Package>, ) { const convertedNode = createConvertedTreeNode(node, codeRuntime); @@ -206,7 +202,7 @@ export const createComponent = createComponentFunction< target: { text: rawValue, }, - valueGetter: node => codeRuntime.parseExprOrFn(node), + valueGetter: (node) => codeRuntime.parseExprOrFn(node), }); convertedNode.setReactNode(); @@ -235,7 +231,7 @@ export const createComponent = createComponentFunction< props: AnyObject, codeRuntime: CodeRuntime, key: string, - children: ReactNode[] = [] + children: ReactNode[] = [], ) { const { ref, ...componentProps } = props; @@ -249,45 +245,29 @@ export const createComponent = createComponentFunction< // 先将 jsslot, jsFunction 对象转换 const finalProps = processValue( componentProps, - node => isJsSlot(node) || isJsFunction(node), + (node) => isJsSlot(node) || isJsFunction(node), (node: JSSlot | JSFunction) => { if (isJsSlot(node)) { if (node.value) { - const nodes = ( - Array.isArray(node.value) ? node.value : [node.value] - ).map(n => createNode(n, undefined)); + const nodes = (Array.isArray(node.value) ? node.value : [node.value]).map( + (n) => createNode(n, undefined), + ); if (node.params?.length) { return (...args: any[]) => { - const params = node.params!.reduce( - (prev, cur, idx) => { - return (prev[cur] = args[idx]); - }, - {} as AnyObject - ); - const subCodeScope = codeRuntime - .getScope() - .createSubScope(params); - const subCodeRuntime = - createCodeRuntime(subCodeScope); + const params = node.params!.reduce((prev, cur, idx) => { + return (prev[cur] = args[idx]); + }, {} as AnyObject); + const subCodeScope = codeRuntime.getScope().createSubScope(params); + const subCodeRuntime = createCodeRuntime(subCodeScope); - return nodes.map(n => - createReactElement( - n, - subCodeRuntime, - instance, - componentsRecord - ) + return nodes.map((n) => + createReactElement(n, subCodeRuntime, instance, componentsRecord), ); }; } else { - return nodes.map(n => - createReactElement( - n, - codeRuntime, - instance, - componentsRecord - ) + return nodes.map((n) => + createReactElement(n, codeRuntime, instance, componentsRecord), ); } } @@ -296,7 +276,7 @@ export const createComponent = createComponentFunction< } return null; - } + }, ); if (someValue(finalProps, isJsExpression)) { @@ -308,14 +288,14 @@ export const createComponent = createComponentFunction< key, ref: refFunction, }, - children + children, ); } Props.displayName = 'Props'; const Reactived = reactive(Props, { target: finalProps, - valueGetter: node => codeRuntime.parseExprOrFn(node), + valueGetter: (node) => codeRuntime.parseExprOrFn(node), }); return ; @@ -327,7 +307,7 @@ export const createComponent = createComponentFunction< key, ref: refFunction, }, - children + children, ); } } @@ -339,9 +319,9 @@ export const createComponent = createComponentFunction< nodeProps, codeRuntime, currentComponentKey, - rawNode.children?.map(n => - createReactElement(n, codeRuntime, instance, componentsRecord) - ) + rawNode.children?.map((n) => + createReactElement(n, codeRuntime, instance, componentsRecord), + ), ); if (loop) { @@ -360,14 +340,9 @@ export const createComponent = createComponentFunction< nodeProps, subCodeRuntime, `loop-${currentComponentKey}-${idx}`, - rawNode.children?.map(n => - createReactElement( - n, - subCodeRuntime, - instance, - componentsRecord - ) - ) + rawNode.children?.map((n) => + createReactElement(n, subCodeRuntime, instance, componentsRecord), + ), ); }); }; @@ -386,7 +361,7 @@ export const createComponent = createComponentFunction< target: { loop, }, - valueGetter: expr => codeRuntime.parseExprOrFn(expr), + valueGetter: (expr) => codeRuntime.parseExprOrFn(expr), }); element = createElement(ReactivedLoop, { @@ -410,7 +385,7 @@ export const createComponent = createComponentFunction< target: { condition, }, - valueGetter: expr => codeRuntime.parseExprOrFn(expr), + valueGetter: (expr) => codeRuntime.parseExprOrFn(expr), }); return createElement(ReactivedCondition, { @@ -434,7 +409,7 @@ export const createComponent = createComponentFunction< const LowCodeComponent = forwardRef(function ( props: LowCodeComponentProps, - ref: ForwardedRef + ref: ForwardedRef, ) { const { id, className, style, ...extraProps } = props; const isMounted = useRef(false); @@ -451,7 +426,7 @@ export const createComponent = createComponentFunction< scopeValue.reloadDataSource(); if (instance.cssText) { - appendExternalStyle(instance.cssText).then(el => { + appendExternalStyle(instance.cssText).then((el) => { styleEl = el; }); } @@ -484,9 +459,7 @@ export const createComponent = createComponentFunction<
{instance .getComponentTreeNodes() - .map(n => - createReactElement(n, codeRuntime, instance, componentsRecord) - )} + .map((n) => createReactElement(n, codeRuntime, instance, componentsRecord))}
); }); diff --git a/runtime/react-renderer/src/components/app.tsx b/runtime/renderer-react/src/components/app.tsx similarity index 100% rename from runtime/react-renderer/src/components/app.tsx rename to runtime/renderer-react/src/components/app.tsx diff --git a/runtime/react-renderer/src/components/outlet.tsx b/runtime/renderer-react/src/components/outlet.tsx similarity index 100% rename from runtime/react-renderer/src/components/outlet.tsx rename to runtime/renderer-react/src/components/outlet.tsx diff --git a/runtime/react-renderer/src/components/route.tsx b/runtime/renderer-react/src/components/route.tsx similarity index 100% rename from runtime/react-renderer/src/components/route.tsx rename to runtime/renderer-react/src/components/route.tsx diff --git a/runtime/react-renderer/src/components/router-view.tsx b/runtime/renderer-react/src/components/router-view.tsx similarity index 100% rename from runtime/react-renderer/src/components/router-view.tsx rename to runtime/renderer-react/src/components/router-view.tsx diff --git a/runtime/renderer-react/src/context/app.ts b/runtime/renderer-react/src/context/app.ts new file mode 100644 index 000000000..ed43149b4 --- /dev/null +++ b/runtime/renderer-react/src/context/app.ts @@ -0,0 +1,13 @@ +import { createContext, useContext } from 'react'; +import { type AppContext as AppContextType } from '@alilc/runtime-core'; +import { type ReactRenderer } from '../renderer'; + +export interface AppContextObject extends AppContextType { + renderer: ReactRenderer; +} + +export const AppContext = createContext({} as any); + +AppContext.displayName = 'RootContext'; + +export const useAppContext = () => useContext(AppContext); diff --git a/runtime/renderer-react/src/context/router.ts b/runtime/renderer-react/src/context/router.ts new file mode 100644 index 000000000..3ecabef2f --- /dev/null +++ b/runtime/renderer-react/src/context/router.ts @@ -0,0 +1,33 @@ +import { type Router, type RouteLocation } from '@alilc/runtime-router'; +import { type PageSchema } from '@alilc/runtime-shared'; +import { createContext, useContext } from 'react'; + +export const RouterContext = createContext({} as any); + +RouterContext.displayName = 'RouterContext'; + +export const useRouter = () => useContext(RouterContext); + +export const RouteLocationContext = createContext({ + name: undefined, + path: '/', + query: {}, + params: {}, + hash: '', + fullPath: '/', + redirectedFrom: undefined, + matched: [], + meta: {}, +}); + +RouteLocationContext.displayName = 'RouteLocationContext'; + +export const useRouteLocation = () => useContext(RouteLocationContext); + +export const PageSchemaContext = createContext( + undefined +); + +PageSchemaContext.displayName = 'PageContext'; + +export const usePageSchema = () => useContext(PageSchemaContext); diff --git a/runtime/react-renderer/src/index.ts b/runtime/renderer-react/src/index.ts similarity index 100% rename from runtime/react-renderer/src/index.ts rename to runtime/renderer-react/src/index.ts diff --git a/runtime/react-renderer/src/plugins/intl/index.ts b/runtime/renderer-react/src/plugins/intl/index.ts similarity index 93% rename from runtime/react-renderer/src/plugins/intl/index.ts rename to runtime/renderer-react/src/plugins/intl/index.ts index 3e2191778..962e1c052 100644 --- a/runtime/react-renderer/src/plugins/intl/index.ts +++ b/runtime/renderer-react/src/plugins/intl/index.ts @@ -3,12 +3,12 @@ import { someValue } from '@alilc/runtime-core'; import { isJsExpression } from '@alilc/runtime-shared'; import { definePlugin } from '../../renderer'; import { PAGE_EVENTS } from '../../events'; -import { reactive } from '../../helper/reactive'; +import { reactive } from '../../utils/reactive'; import { createIntl } from './intl'; export { createIntl }; -declare module '@alilc/runtime-core' { +declare module '@alilc/renderer-core' { interface AppBoosts { intl: ReturnType; } @@ -24,7 +24,7 @@ export const intlPlugin = definePlugin({ appScope.setValue(intl); boosts.add('intl', intl); - boosts.hooks.hook(PAGE_EVENTS.COMPONENT_BEFORE_NODE_CREATE, node => { + boosts.hooks.hook(PAGE_EVENTS.COMPONENT_BEFORE_NODE_CREATE, (node) => { if (node.type === 'i18n') { const { key, params } = node.raw.data; diff --git a/runtime/react-renderer/src/plugins/intl/intl.tsx b/runtime/renderer-react/src/plugins/intl/intl.tsx similarity index 100% rename from runtime/react-renderer/src/plugins/intl/intl.tsx rename to runtime/renderer-react/src/plugins/intl/intl.tsx diff --git a/runtime/react-renderer/src/plugins/intl/parser.ts b/runtime/renderer-react/src/plugins/intl/parser.ts similarity index 86% rename from runtime/react-renderer/src/plugins/intl/parser.ts rename to runtime/renderer-react/src/plugins/intl/parser.ts index 7f20da264..2289cf9cc 100644 --- a/runtime/react-renderer/src/plugins/intl/parser.ts +++ b/runtime/renderer-react/src/plugins/intl/parser.ts @@ -1,4 +1,4 @@ -import { isObject } from '@alilc/runtime-shared'; +import { isObject } from 'lodash-es'; const RE_TOKEN_LIST_VALUE: RegExp = /^(?:\d)+/; const RE_TOKEN_NAMED_VALUE: RegExp = /^(?:\w)+/; @@ -32,8 +32,8 @@ export function parse(format: string): Array { const type = RE_TOKEN_LIST_VALUE.test(sub) ? 'list' : isClosed && RE_TOKEN_NAMED_VALUE.test(sub) - ? 'named' - : 'unknown'; + ? 'named' + : 'unknown'; tokens.push({ value: sub, type }); } else if (char === '%') { // when found rails i18n syntax, skip text capture @@ -50,18 +50,11 @@ export function parse(format: string): Array { return tokens; } -export function compile( - tokens: Token[], - values: Record | any[] = {} -): string[] { +export function compile(tokens: Token[], values: Record | any[] = {}): string[] { const compiled: string[] = []; let index: number = 0; - const mode: string = Array.isArray(values) - ? 'list' - : isObject(values) - ? 'named' - : 'unknown'; + const mode: string = Array.isArray(values) ? 'list' : isObject(values) ? 'named' : 'unknown'; if (mode === 'unknown') { return compiled; } @@ -81,7 +74,7 @@ export function compile( } else { if (process.env.NODE_ENV !== 'production') { console.warn( - `Type of token '${token.type}' and format of value '${mode}' don't match!` + `Type of token '${token.type}' and format of value '${mode}' don't match!`, ); } } diff --git a/runtime/react-renderer/src/plugins/utils/index.ts b/runtime/renderer-react/src/plugins/utils/index.ts similarity index 100% rename from runtime/react-renderer/src/plugins/utils/index.ts rename to runtime/renderer-react/src/plugins/utils/index.ts diff --git a/runtime/react-renderer/src/renderer.ts b/runtime/renderer-react/src/renderer.ts similarity index 100% rename from runtime/react-renderer/src/renderer.ts rename to runtime/renderer-react/src/renderer.ts diff --git a/runtime/react-renderer/src/router.ts b/runtime/renderer-react/src/router.ts similarity index 74% rename from runtime/react-renderer/src/router.ts rename to runtime/renderer-react/src/router.ts index eb4446cad..617f38473 100644 --- a/runtime/react-renderer/src/router.ts +++ b/runtime/renderer-react/src/router.ts @@ -1,13 +1,9 @@ -import { - type Router, - type RouterOptions, - createRouter, -} from '@alilc/runtime-router'; +import { type Router, type RouterOptions, createRouter } from '@alilc/runtime-router'; import { createRouterProvider } from './components/router-view'; import RouteOutlet from './components/outlet'; import { type ReactRendererSetupContext } from './renderer'; -declare module '@alilc/runtime-core' { +declare module '@alilc/renderer-core' { interface AppBoosts { router: Router; } @@ -21,9 +17,7 @@ const defaultRouterOptions: RouterOptions = { export function initRouter(context: ReactRendererSetupContext) { const { schema, boosts, appScope, renderer } = context; - const router = createRouter( - schema.getByKey('router') ?? defaultRouterOptions - ); + const router = createRouter(schema.getByKey('router') ?? defaultRouterOptions); appScope.inject('router', router); boosts.add('router', router); diff --git a/runtime/react-renderer/src/signals.ts b/runtime/renderer-react/src/signals.ts similarity index 88% rename from runtime/react-renderer/src/signals.ts rename to runtime/renderer-react/src/signals.ts index d95b0ff6c..6e8fac9ec 100644 --- a/runtime/react-renderer/src/signals.ts +++ b/runtime/renderer-react/src/signals.ts @@ -10,13 +10,7 @@ import { isReactive, isShallow, } from '@vue/reactivity'; -import { - noop, - isObject, - isPlainObject, - isSet, - isMap, -} from '@alilc/runtime-shared'; +import { noop, isObject, isPlainObject, isSet, isMap } from 'lodash-es'; export { ref as createSignal, computed, effect }; export type { Ref as Signal, ComputedRef as ComputedSignal }; @@ -32,7 +26,7 @@ export function watch( }: { deep?: boolean; immediate?: boolean; - } = {} + } = {}, ) { let getter: () => any; let forceTrigger = false; @@ -42,9 +36,7 @@ export function watch( forceTrigger = isShallow(source); } else if (isReactive(source)) { getter = () => { - return deep === true - ? source - : traverse(source, deep === false ? 1 : undefined); + return deep === true ? source : traverse(source, deep === false ? 1 : undefined); }; forceTrigger = true; } else { @@ -93,12 +85,7 @@ export function watch( return unwatch; } -function traverse( - value: unknown, - depth?: number, - currentDepth = 0, - seen?: Set -) { +function traverse(value: unknown, depth?: number, currentDepth = 0, seen?: Set) { if (!isObject(value)) { return value; } diff --git a/runtime/react-renderer/src/utils/element.ts b/runtime/renderer-react/src/utils/element.ts similarity index 80% rename from runtime/react-renderer/src/utils/element.ts rename to runtime/renderer-react/src/utils/element.ts index ace87eb36..cf19e1702 100644 --- a/runtime/react-renderer/src/utils/element.ts +++ b/runtime/renderer-react/src/utils/element.ts @@ -1,4 +1,6 @@ -import { addLeadingSlash } from '@alilc/runtime-shared'; +const addLeadingSlash = (path: string): string => { + return path.charAt(0) === '/' ? path : `/${path}`; +}; export function getElementById(id: string, tag: string = 'div') { let el = document.getElementById(id); @@ -44,13 +46,13 @@ export async function loadPackageUrls(urls: string[]) { } } - await Promise.all(styles.map(item => appendExternalCss(item))); - await Promise.all(scripts.map(item => appendExternalScript(item))); + await Promise.all(styles.map((item) => appendExternalCss(item))); + await Promise.all(scripts.map((item) => appendExternalScript(item))); } async function appendExternalScript( url: string, - root: HTMLElement = document.body + root: HTMLElement = document.body, ): Promise { if (url) { const el = getIfExistAssetByUrl(url, 'script'); @@ -73,9 +75,9 @@ async function appendExternalScript( () => { resolve(scriptElement); }, - false + false, ); - scriptElement.addEventListener('error', error => { + scriptElement.addEventListener('error', (error) => { if (root.contains(scriptElement)) { root.removeChild(scriptElement); } @@ -88,7 +90,7 @@ async function appendExternalScript( async function appendExternalCss( url: string, - root: HTMLElement = document.head + root: HTMLElement = document.head, ): Promise { if (url) { const el = getIfExistAssetByUrl(url, 'link'); @@ -105,9 +107,9 @@ async function appendExternalCss( () => { resolve(el); }, - false + false, ); - el.addEventListener('error', error => { + el.addEventListener('error', (error) => { reject(error); }); @@ -117,7 +119,7 @@ async function appendExternalCss( export async function appendExternalStyle( cssText: string, - root: HTMLElement = document.head + root: HTMLElement = document.head, ): Promise { return new Promise((resolve, reject) => { let el: HTMLStyleElement = document.createElement('style'); @@ -128,9 +130,9 @@ export async function appendExternalStyle( () => { resolve(el); }, - false + false, ); - el.addEventListener('error', error => { + el.addEventListener('error', (error) => { reject(error); }); @@ -140,11 +142,10 @@ export async function appendExternalStyle( function getIfExistAssetByUrl( url: string, - tag: 'link' | 'script' + tag: 'link' | 'script', ): HTMLLinkElement | HTMLScriptElement | undefined { - return Array.from(document.getElementsByTagName(tag)).find(item => { - const elUrl = - (item as HTMLLinkElement).href || (item as HTMLScriptElement).src; + return Array.from(document.getElementsByTagName(tag)).find((item) => { + const elUrl = (item as HTMLLinkElement).href || (item as HTMLScriptElement).src; if (/^(https?:)?\/\/([\w.]+\/?)\S*/gi.test(url)) { // if url === http://xxx.xxx diff --git a/runtime/react-renderer/src/utils/reactive.tsx b/runtime/renderer-react/src/utils/reactive.tsx similarity index 100% rename from runtime/react-renderer/src/utils/reactive.tsx rename to runtime/renderer-react/src/utils/reactive.tsx diff --git a/runtime/renderer-react/tsconfig.json b/runtime/renderer-react/tsconfig.json new file mode 100644 index 000000000..8e11bb7b7 --- /dev/null +++ b/runtime/renderer-react/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "paths": { + "@alilc/*": ["runtime/*/src"] + } + } +} diff --git a/runtime/renderer-react/vitest.config.ts b/runtime/renderer-react/vitest.config.ts new file mode 100644 index 000000000..73d181d02 --- /dev/null +++ b/runtime/renderer-react/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['tests/*.spec.ts'], + environment: 'jsdom' + } +}) diff --git a/runtime/router/package.json b/runtime/router/package.json index cceb950e2..5701eb7c8 100644 --- a/runtime/router/package.json +++ b/runtime/router/package.json @@ -4,5 +4,13 @@ "description": "", "type": "module", "bugs": "https://github.com/alibaba/lowcode-engine/issues", - "homepage": "https://github.com/alibaba/lowcode-engine/#readme" -} \ No newline at end of file + "homepage": "https://github.com/alibaba/lowcode-engine/#readme", + "license": "MIT", + "scripts": { + "build": "", + "test": "vitest" + }, + "dependencies": { + "@alilc/renderer-core": "^2.0.0-beta.0" + } +} diff --git a/runtime/router/tsconfig.json b/runtime/router/tsconfig.json index 7460ef428..b4e69ae1f 100644 --- a/runtime/router/tsconfig.json +++ b/runtime/router/tsconfig.json @@ -1,3 +1,6 @@ { - "extends": "../../tsconfig.json" -} \ No newline at end of file + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + } +}