feat: add renderer-core codes

This commit is contained in:
1ncounter 2024-03-19 10:47:13 +08:00
parent 3ba0926438
commit fb5de6441d
66 changed files with 2001 additions and 918 deletions

View File

@ -16,16 +16,14 @@ sidebar_position: 0
- 定义搭建基础协议国际化多语言支持规范AA - 定义搭建基础协议国际化多语言支持规范AA
- 定义搭建基础协议无障碍访问规范AAA - 定义搭建基础协议无障碍访问规范AAA
### 1.2 协议草案起草人 ### 1.2 协议草案起草人
- 撰写:月飞、康为、林熠 - 撰写:月飞、康为、林熠
- 审阅:大果、潕量、九神、元彦、戊子、屹凡、金禅、前道、天晟、游鹿、光弘、力皓 - 审阅:大果、潕量、九神、元彦、戊子、屹凡、金禅、前道、天晟、游鹿、光弘、力皓
### 1.3 版本号 ### 1.3 版本号
1.1.0 1.2.0
### 1.4 协议版本号规范A ### 1.4 协议版本号规范A
@ -35,7 +33,6 @@ sidebar_position: 0
- minor 是小版本号用于发布向下兼容的协议功能新增 - minor 是小版本号用于发布向下兼容的协议功能新增
- patch 是补丁号:用于发布向下兼容的协议问题修正 - patch 是补丁号:用于发布向下兼容的协议问题修正
### 1.5 协议中子规范 Level 定义 ### 1.5 协议中子规范 Level 定义
| 规范等级 | 实现要求 | | 规范等级 | 实现要求 |
@ -44,7 +41,6 @@ sidebar_position: 0
| AA | 推荐规范,推荐实现;遵守此类规范有助于业务未来的扩展性和跨团队合作研发效率的提升。 | | AA | 推荐规范,推荐实现;遵守此类规范有助于业务未来的扩展性和跨团队合作研发效率的提升。 |
| AAA | 参考规范,根据业务场景实际诉求实现;是集团层面鼓励的技术实现引导。 | | AAA | 参考规范,根据业务场景实际诉求实现;是集团层面鼓励的技术实现引导。 |
### 1.6 名词术语 ### 1.6 名词术语
#### 1.6.1 物料系统名词 #### 1.6.1 物料系统名词
@ -58,7 +54,6 @@ sidebar_position: 0
- **页面Page**:由组件 + 区块组合而成。由页面容器组件包裹,可描述页面级的状态管理和公共函数。 - **页面Page**:由组件 + 区块组合而成。由页面容器组件包裹,可描述页面级的状态管理和公共函数。
- **模板Template**:特定垂直业务领域内的业务组件、区块可组合为单个页面,或者是再配合路由组合为多个页面集,统称为模板。 - **模板Template**:特定垂直业务领域内的业务组件、区块可组合为单个页面,或者是再配合路由组合为多个页面集,统称为模板。
#### 1.6.2 低代码搭建系统名词 #### 1.6.2 低代码搭建系统名词
- **搭建编辑器**:使用可视化的方式实现页面搭建,支持组件 UI 编排、属性编辑、事件绑定、数据绑定,最终产出符合搭建基础协议规范的数据。 - **搭建编辑器**:使用可视化的方式实现页面搭建,支持组件 UI 编排、属性编辑、事件绑定、数据绑定,最终产出符合搭建基础协议规范的数据。
@ -76,7 +71,7 @@ sidebar_position: 0
### 1.7 背景 ### 1.7 背景
- **协议目标**:通过约束低代码引擎的搭建协议规范,让上层低代码编辑器的产出物(低代码业务组件、区块、应用)保持一致性,可跨低代码研发平台进行流通而提效,亦不阻碍集团业务间融合的发展。  - **协议目标**:通过约束低代码引擎的搭建协议规范,让上层低代码编辑器的产出物(低代码业务组件、区块、应用)保持一致性,可跨低代码研发平台进行流通而提效,亦不阻碍集团业务间融合的发展。
- **协议通** - **协议通**
- 协议顶层结构统一 - 协议顶层结构统一
- 协议 schema 具备有完整的描述能力,包含版本、国际化、组件树、组件映射关系等; - 协议 schema 具备有完整的描述能力,包含版本、国际化、组件树、组件映射关系等;
@ -109,7 +104,6 @@ sidebar_position: 0
- **面向多端**:不能仅面向 React还有小程序等多端。 - **面向多端**:不能仅面向 React还有小程序等多端。
- **支持国际化&无障碍访问标准的实现** - **支持国际化&无障碍访问标准的实现**
## 2 协议结构 ## 2 协议结构
协议最顶层结构如下: 协议最顶层结构如下:
@ -276,12 +270,10 @@ sidebar_position: 0
定义当前协议 schema 的版本号,不同的版本号对应不同的渲染 SDK以保障不同版本搭建协议产物的正常渲染 定义当前协议 schema 的版本号,不同的版本号对应不同的渲染 SDK以保障不同版本搭建协议产物的正常渲染
| 根属性名称 | 类型 | 说明 | 变量支持 | 默认值 | | 根属性名称 | 类型 | 说明 | 变量支持 | 默认值 |
| ---------- | ------ | ---------- | -------- | ------ | | ---------- | ------ | ---------- | -------- | ------ |
| version | String | 协议版本号 | - | 1.0.0 | | version | String | 协议版本号 | - | 1.0.0 |
描述示例: 描述示例:
```javascript ```javascript
@ -294,15 +286,14 @@ sidebar_position: 0
协议中用于描述 componentName 到公域组件映射关系的规范。 协议中用于描述 componentName 到公域组件映射关系的规范。
| 参数 | 说明 | 类型 | 变量支持 | 默认值 | | 参数 | 说明 | 类型 | 变量支持 | 默认值 |
| --------------- | ---------------------- | ------------------------- | -------- | ------ | | --------------- | ---------------------- | ------------------ | -------- | ------ |
| componentsMap[] | 描述组件映射关系的集合 | **ComponentMap**[] | - | null | | componentsMap[] | 描述组件映射关系的集合 | **ComponentMap**[] | - | null |
**ComponentMap 结构描述**如下: **ComponentMap 结构描述**如下:
| 参数 | 说明 | 类型 | 变量支持 | 默认值 | | 参数 | 说明 | 类型 | 变量支持 | 默认值 |
| ------------- | ------------------------------------------------------------------------------------------------------ | ------- | -------- | ------ | | ------------- | ------------------------------------------------------------------------------------------ | ------- | -------- | ------ |
| componentName | 协议中的组件名,唯一性,对应包导出的组件名,是一个有效的 **JS 标识符**,而且是大写字母打头 | String | - | - | | componentName | 协议中的组件名,唯一性,对应包导出的组件名,是一个有效的 **JS 标识符**,而且是大写字母打头 | String | - | - |
| package | npm 公域的 package name | String | - | - | | package | npm 公域的 package name | String | - | - |
| version | package version | String | - | - | | version | package version | String | - | - |
@ -311,48 +302,54 @@ sidebar_position: 0
| subName | 下标子组件名称 | String | - | | | subName | 下标子组件名称 | String | - | |
| main | 包导出组件入口文件路径 | String | - | - | | main | 包导出组件入口文件路径 | String | - | - |
描述示例: 描述示例:
```json ```json
{ {
"componentsMap": [{ "componentsMap": [
{
"componentName": "Button", "componentName": "Button",
"package": "@alifd/next", "package": "@alifd/next",
"version": "1.0.0", "version": "1.0.0",
"destructuring": true "destructuring": true
}, { },
{
"componentName": "MySelect", "componentName": "MySelect",
"package": "@alifd/next", "package": "@alifd/next",
"version": "1.0.0", "version": "1.0.0",
"destructuring": true, "destructuring": true,
"exportName": "Select" "exportName": "Select"
}, { },
{
"componentName": "ButtonGroup", "componentName": "ButtonGroup",
"package": "@alifd/next", "package": "@alifd/next",
"version": "1.0.0", "version": "1.0.0",
"destructuring": true, "destructuring": true,
"exportName": "Button", "exportName": "Button",
"subName": "Group" "subName": "Group"
}, { },
{
"componentName": "RadioGroup", "componentName": "RadioGroup",
"package": "@alifd/next", "package": "@alifd/next",
"version": "1.0.0", "version": "1.0.0",
"destructuring": true, "destructuring": true,
"exportName": "Radio", "exportName": "Radio",
"subName": "Group" "subName": "Group"
}, { },
{
"componentName": "CustomCard", "componentName": "CustomCard",
"package": "@ali/custom-card", "package": "@ali/custom-card",
"version": "1.0.0" "version": "1.0.0"
}, { },
{
"componentName": "CustomInput", "componentName": "CustomInput",
"package": "@ali/custom", "package": "@ali/custom",
"version": "1.0.0", "version": "1.0.0",
"main": "/lib/input", "main": "/lib/input",
"destructuring": true, "destructuring": true,
"exportName": "Input" "exportName": "Input"
}] }
]
} }
``` ```
@ -377,13 +374,10 @@ import CustomCard from '@ali/custom-card';
// 使用特定路径进行导出 // 使用特定路径进行导出
import { Input as CustomInput } from '@ali/custom/lib/input'; import { Input as CustomInput } from '@ali/custom/lib/input';
``` ```
### 2.3 组件树描述A ### 2.3 组件树描述A
协议中用于描述搭建出来的组件树结构的规范,整个组件树的描述由**组件结构**&**容器结构**两种结构嵌套构成。 协议中用于描述搭建出来的组件树结构的规范,整个组件树的描述由**组件结构**&**容器结构**两种结构嵌套构成。
- 组件结构:描述单个组件的名称、属性、子集的结构; - 组件结构:描述单个组件的名称、属性、子集的结构;
@ -403,12 +397,11 @@ import { Input as CustomInput } from '@ali/custom/lib/input';
##### 2.3.1.1 Props 结构描述 ##### 2.3.1.1 Props 结构描述
| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | | 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 |
| ----------- | ------------ | ------ | -------- | ------ | ------------------------------------- | | --------- | ------------- | ------ | -------- | ------ | --------------------------------- |
| id | 组件 ID | String | ✅ | - | 系统属性 | | id | 组件 ID | String | ✅ | - | 系统属性 |
| className | 组件样式类名 | String | ✅ | - | 系统属性,支持变量表达式 | | className | 组件样式类名 | String | ✅ | - | 系统属性,支持变量表达式 |
| style | 组件内联样式 | Object | ✅ | - | 系统属性,单个内联样式属性值 | | style | 组件内联样式 | Object | ✅ | - | 系统属性,单个内联样式属性值 |
| ref | 组件 ref 名称 | String | ✅ | - | 可通过 `this.$(ref)` 获取组件实例 | | ref | 组件 ref 名称 | String | ✅ | - | 可通过 `this.$(ref)` 获取组件实例 |
| extendProps | 组件继承属性 | 变量 | ✅ | - | 仅支持变量绑定,常用于继承属性对象 |
| ... | 组件私有属性 | - | - | - | | | ... | 组件私有属性 | - | - | - | |
##### 2.3.1.2 css/less/scss 样式描述 ##### 2.3.1.2 css/less/scss 样式描述
@ -428,19 +421,19 @@ import { Input as CustomInput } from '@ali/custom/lib/input';
##### 2.3.1.3 ComponentDataSource 对象描述 ##### 2.3.1.3 ComponentDataSource 对象描述
| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | | 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 |
| ----------- | ---------------------- | -------------------------------------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------- | | ----------- | ---------------------- | ----------------------------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| list[] | 数据源列表 | **ComponentDataSourceItem**[] | - | - | 成为为单个请求配置, 内容定义详见 [ComponentDataSourceItem 对象描述](#2314-componentdatasourceitem-对象描述) | | list[] | 数据源列表 | **ComponentDataSourceItem**[] | - | - | 成为为单个请求配置, 内容定义详见 [ComponentDataSourceItem 对象描述](#2314-componentdatasourceitem-对象描述) |
| dataHandler | 所有请求数据的处理函数 | Function | - | - | 详见 [dataHandler Function 描述](#2317-datahandler-function-描述) | | dataHandler | 所有请求数据的处理函数 | Function | - | - | 详见 [dataHandler Function 描述](#2317-datahandler-function-描述) |
##### 2.3.1.4 ComponentDataSourceItem 对象描述 ##### 2.3.1.4 ComponentDataSourceItem 对象描述
| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | | 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 |
| -------------- | ---------------------------- | ---------------------------------------------------- | -------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -------------- | ---------------------------- | ---------------------------------------------------- | -------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| id | 数据请求 ID 标识 | String | - | - | | | id | 数据请求 ID 标识 | String | - | - | |
| isInit | 是否为初始数据 | Boolean | ✅ | true | 值为 true 时,将在组件初始化渲染时自动发送当前数据请求 | | isInit | 是否为初始数据 | Boolean | ✅ | true | 值为 true 时,将在组件初始化渲染时自动发送当前数据请求 |
| isSync | 是否需要串行执行 | Boolean | ✅ | false | 值为 true 时,当前请求将被串行执行 | | isSync | 是否需要串行执行 | Boolean | ✅ | false | 值为 true 时,当前请求将被串行执行 |
| type | 数据请求类型 | String | - | fetch | 支持四种类型fetch/mtop/jsonp/custom | | 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。也可以返回一个 Promiseresolve 的值作为请求的 optionsreject 时,使用原 options | | willFetch | 单个数据结果请求参数处理函数 | Function | - | options => options | 只接受一个参数options返回值作为请求的 options当处理异常时使用原 options。也可以返回一个 Promiseresolve 的值作为请求的 optionsreject 时,使用原 options |
| requestHandler | 自定义扩展的外部请求处理器 | Function | - | - | 仅 type='custom' 时生效 | | requestHandler | 自定义扩展的外部请求处理器 | Function | - | - | 仅 type='custom' 时生效 |
| dataHandler | request 成功后的回调函数 | Function | - | `response => response.data` | 参数:请求成功后 promise 的 value 值 | | | dataHandler | request 成功后的回调函数 | Function | - | `response => response.data` | 参数:请求成功后 promise 的 value 值 | |
@ -462,17 +455,18 @@ try {
dataSourceItem.status = 'error'; dataSourceItem.status = 'error';
} }
``` ```
**注意:** **注意:**
- dataHandler 和 errorHandler 只会走其中的一个回调 - dataHandler 和 errorHandler 只会走其中的一个回调
- 它们都有修改 promise 状态的机会,意味着可以修改当前数据源最终状态 - 它们都有修改 promise 状态的机会,意味着可以修改当前数据源最终状态
- 最后返回的结果会被认为是当前数据源的最终结果,如果被 catch 了,那么会认为数据源请求出错 - 最后返回的结果会被认为是当前数据源的最终结果,如果被 catch 了,那么会认为数据源请求出错
- dataHandler 会有默认值,考虑到返回结果入参都是 response 完整对象,默认值会返回 `response.data`errorHandler 没有默认值 - dataHandler 会有默认值,考虑到返回结果入参都是 response 完整对象,默认值会返回 `response.data`errorHandler 没有默认值
##### 2.3.1.5 ComponentDataSourceItemOptions 对象描述 ##### 2.3.1.5 ComponentDataSourceItemOptions 对象描述
| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | | 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 |
| ------- | ------------ | ------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------- | | ------- | ------------ | ------- | -------- | ------ | ------------------------------------------------------------------------------------------------------------ |
| uri | 请求地址 | String | ✅ | - | | | uri | 请求地址 | String | ✅ | - | |
| params | 请求参数 | Object | ✅ | {} | 当前数据源默认请求参数(在运行时会被实际的 load 方法的参数替换,如果 load 的 params 没有则会使用当前 params) | | params | 请求参数 | Object | ✅ | {} | 当前数据源默认请求参数(在运行时会被实际的 load 方法的参数替换,如果 load 的 params 没有则会使用当前 params) |
| method | 请求方法 | String | ✅ | GET | | | method | 请求方法 | String | ✅ | GET | |
@ -480,16 +474,14 @@ try {
| timeout | 超时时长 | Number | ✅ | 5000 | 单位 ms | | timeout | 超时时长 | Number | ✅ | 5000 | 单位 ms |
| headers | 请求头信息 | Object | ✅ | - | 自定义请求头 | | headers | 请求头信息 | Object | ✅ | - | 自定义请求头 |
##### 2.3.1.6 ComponentLifeCycles 对象描述 ##### 2.3.1.6 ComponentLifeCycles 对象描述
生命周期对象schema 面向多端,不同 DSL 有不同的生命周期方法: 生命周期对象schema 面向多端,不同 DSL 有不同的生命周期方法:
- React对于中后台 PC 物料,已明确使用 React 作为最终渲染框架,因此提案采用 [React16 标准生命周期方法](https://reactjs.org/docs/react-component.html)标准来定义生命周期方法,降低理解成本,支持生命周期如下: - React对于中后台 PC 物料,已明确使用 React 作为最终渲染框架,因此提案采用 [React16 标准生命周期方法](https://reactjs.org/docs/react-component.html)标准来定义生命周期方法,降低理解成本,支持生命周期如下:
- constructor(props, context)  - constructor(props, context)
- 说明:初始化渲染时执行,常用于设置 state 值。 - 说明:初始化渲染时执行,常用于设置 state 值。
- render()  - render()
- 说明:执行于容器组件 React Class 的 render 方法最前,常用于计算变量挂载到 this 对象上,供 props 上属性绑定。此 render() 方法不需要设置 return 返回值。 - 说明:执行于容器组件 React Class 的 render 方法最前,常用于计算变量挂载到 this 对象上,供 props 上属性绑定。此 render() 方法不需要设置 return 返回值。
- componentDidMount() - componentDidMount()
- 说明:组件已加载 - 说明:组件已加载
@ -520,7 +512,6 @@ try {
}, },
``` ```
##### 2.3.1.7 dataHandler Function 描述 ##### 2.3.1.7 dataHandler Function 描述
- 参数:为 dataMap 对象,包含字段如下: - 参数:为 dataMap 对象,包含字段如下:
@ -531,13 +522,14 @@ try {
##### 2.3.1.8 ComponentPropDefinition 对象描述 ##### 2.3.1.8 ComponentPropDefinition 对象描述
| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | | 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 |
| ------------ | ---------- | -------------- | -------- | --------- | ----------------------------------------------------------------------------------------------------------------- | | ------------ | ---------- | -------------- | -------- | --------- | ---------------------------------------------------------------------------------------------------------- |
| name | 属性名称 | String | - | - | | | name | 属性名称 | String | - | - | |
| propType | 属性类型 | String\|Object | - | - | 具体值内容结构参考《低代码引擎物料协议规范》内的“2.2.2.3 组件属性信息”中描述的**基本类型**和**复合类型** | | propType | 属性类型 | String\|Object | - | - | 具体值内容结构参考《低代码引擎物料协议规范》内的“2.2.2.3 组件属性信息”中描述的**基本类型**和**复合类型** |
| description | 属性描述 | String | - | '' | | | description | 属性描述 | String | - | '' | |
| defaultValue | 属性默认值 | Any | - | undefined | 当 defaultValue 和 defaultProps 中存在同一个 prop 的默认值时,优先使用 defaultValue。 | | defaultValue | 属性默认值 | Any | - | undefined | 当 defaultValue 和 defaultProps 中存在同一个 prop 的默认值时,优先使用 defaultValue。 |
范例: 范例:
```json ```json
{ {
"propDefinitions": [{ "propDefinitions": [{
@ -556,18 +548,16 @@ try {
对应生成源码开发体系中 render 函数返回的 jsx 代码,主要描述有以下属性: 对应生成源码开发体系中 render 函数返回的 jsx 代码,主要描述有以下属性:
| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | | 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 |
| ------------- | ---------------------- | ---------------- | -------- | ----------------- | ---------------------------------------------------------------------------------------------------------- | | ------------- | ---------------------- | ---------------- | -------- | ----------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- |
| id | 组件唯一标识 | String | - | | 可选,组件 id 由引擎随机生成UUID并保证唯一性消费方为上层应用平台在组件发生移动等场景需保持 id 不变 | | id | 组件唯一标识 | String | - | | 可选,组件 id 由引擎随机生成UUID并保证唯一性消费方为上层应用平台在组件发生移动等场景需保持 id 不变 |
| componentName | 组件名称 | String | - | Div | 必填,首字母大写,同 [componentsMap](#22-组件映射关系a) 中的要求 | | componentName | 组件名称 | String | - | Div | 必填,首字母大写,同 [componentsMap](#22-组件映射关系a) 中的要求 |
| props {} | 组件属性对象 | **Props**| - | {} | 必填,详见 | 必填,详见 [Props 结构描述](#2311-props-结构描述) | | props | 组件属性对象 | **Props** | - | {} | 必填,详见 | 必填,详见 [Props 结构描述](#2311-props-结构描述) |
| condition | 渲染条件 | Boolean | ✅ | true | 选填,根据表达式结果判断是否渲染物料;支持变量表达式 | | condition | 渲染条件 | Boolean | ✅ | true | 选填,根据表达式结果判断是否渲染物料;支持变量表达式 |
| loop | 循环数据 | Array | ✅ | - | 选填,默认不进行循环渲染;支持变量表达式 | | loop | 循环数据 | Array | ✅ | - | 选填,默认不进行循环渲染;支持变量表达式 |
| loopArgs | 循环迭代对象、索引名称 | [String, String] | | ["item", "index"] | 选填,仅支持字符串 | | loopArgs | 循环迭代对象、索引名称 | [String, String] | | ["item", "index"] | 选填,仅支持字符串 |
| children | 子组件 | Array | | | 选填,支持变量表达式 | | children | 子组件 | Array | | | 选填,支持变量表达式 |
描述举例: 描述举例:
```json ```json
@ -597,7 +587,6 @@ try {
} }
``` ```
#### 2.3.3 容器结构描述 (A)  #### 2.3.3 容器结构描述 (A) 
容器是一类特殊的组件,在组件能力基础上增加了对生命周期对象、自定义方法、样式文件、数据源等信息的描述。包含**低代码业务组件容器 Component**、**区块容器 Block**、**页面容器 Page** 3 种。主要描述有以下属性: 容器是一类特殊的组件,在组件能力基础上增加了对生命周期对象、自定义方法、样式文件、数据源等信息的描述。包含**低代码业务组件容器 Component**、**区块容器 Block**、**页面容器 Page** 3 种。主要描述有以下属性:
@ -612,15 +601,13 @@ try {
- 条件渲染condition - 条件渲染condition
- 样式文件css/less/scss - 样式文件css/less/scss
详细描述: 详细描述:
| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | | 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 |
| --------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------- | | --------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------- | --- | --- | ----------------------------------------------------------------------------------------- |
| componentName | 组件名称 | 枚举类型,包括`'Page'` (代表页面容器)、`'Block'` (代表区块容器)、`'Component'` (代表低代码业务组件容器) | - | 'Div' | 必填,首字母大写 | | componentName | 组件名称 | 枚举类型,包括`'Page'` (代表页面容器)、`'Block'` (代表区块容器)、`'Component'` (代表低代码业务组件容器) | - | 'Div' | 必填,首字母大写 |
| fileName | 文件名称 | String | - | - | 必填,英文 | | fileName | 文件名称 | String | - | - | 必填,英文 |
| props { } | 组件属性对象 | **Props** | - | {} | 必填,详见 [Props 结构描述](#2311-props-结构描述) | | props | 组件属性对象 | **Props** | - | {} | 必填,详见 [Props 结构描述](#2311-props-结构描述) |
| static | 低代码业务组件类的静态对象 | | | | |
| defaultProps | 低代码业务组件默认属性 | Object | - | - | 选填,仅用于定义低代码业务组件的默认属性 | | defaultProps | 低代码业务组件默认属性 | Object | - | - | 选填,仅用于定义低代码业务组件的默认属性 |
| propDefinitions | 低代码业务组件属性类型定义 | **ComponentPropDefinition**[] | - | - | 选填,仅用于定义低代码业务组件的属性数据类型。详见 [ComponentPropDefinition 对象描述](#2318-componentpropdefinition-对象描述) | | propDefinitions | 低代码业务组件属性类型定义 | **ComponentPropDefinition**[] | - | - | 选填,仅用于定义低代码业务组件的属性数据类型。详见 [ComponentPropDefinition 对象描述](#2318-componentpropdefinition-对象描述) |
| condition | 渲染条件 | Boolean | ✅ | true | 选填,根据表达式结果判断是否渲染物料;支持变量表达式 | | condition | 渲染条件 | Boolean | ✅ | true | 选填,根据表达式结果判断是否渲染物料;支持变量表达式 |
@ -629,9 +616,7 @@ try {
| css/less/scss | 样式属性 | String | ✅ | - | 选填,详见 [css/less/scss 样式描述](#2312-csslessscss 样式描述) | | css/less/scss | 样式属性 | String | ✅ | - | 选填,详见 [css/less/scss 样式描述](#2312-csslessscss 样式描述) |
| lifeCycles | 生命周期对象 | **ComponentLifeCycles** | - | - | 详见 [ComponentLifeCycles 对象描述](#2316-componentlifecycles-对象描述) | | lifeCycles | 生命周期对象 | **ComponentLifeCycles** | - | - | 详见 [ComponentLifeCycles 对象描述](#2316-componentlifecycles-对象描述) |
| methods | 自定义方法对象 | Object | - | - | 选填,对象成员为函数类型 | | methods | 自定义方法对象 | Object | - | - | 选填,对象成员为函数类型 |
| dataSource {} | 数据源对象 | **ComponentDataSource**| - | - | 选填,异步数据源,详见 | - | - | 选填,异步数据源,详见 [ComponentDataSource 对象描述](#2313-componentdatasource-对象描述) | | dataSource | 数据源对象 | **ComponentDataSource** | - | - | 选填,异步数据源,详见 | - | - | 选填,异步数据源,详见 [ComponentDataSource 对象描述](#2313-componentdatasource-对象描述) |
#### 完整描述示例 #### 完整描述示例
@ -647,7 +632,8 @@ try {
"background": "#dd2727" "background": "#dd2727"
} }
}, },
"children": [{ "children": [
{
"componentName": "Button", "componentName": "Button",
"props": { "props": {
"text": { "text": {
@ -655,7 +641,8 @@ try {
"value": "this.state.btnText" "value": "this.state.btnText"
} }
} }
}], }
],
"state": { "state": {
"btnText": "submit" "btnText": "submit"
}, },
@ -683,7 +670,8 @@ try {
} }
}, },
"dataSource": { "dataSource": {
"list": [{ "list": [
{
"id": "list", "id": "list",
"isInit": true, "isInit": true,
"type": "fetch/mtop/jsonp", "type": "fetch/mtop/jsonp",
@ -699,7 +687,8 @@ try {
"type": "JSFunction", "type": "JSFunction",
"value": "function(data, err) {}" "value": "function(data, err) {}"
} }
}], }
],
"dataHandler": { "dataHandler": {
"type": "JSFunction", "type": "JSFunction",
"value": "function(dataMap) { }" "value": "function(dataMap) { }"
@ -764,11 +753,10 @@ try {
**ReactNode** 描述: **ReactNode** 描述:
| 参数 | 说明 | 值类型 | 默认值 | 备注 | | 参数 | 说明 | 值类型 | 默认值 | 备注 |
| ----- | ---------- | --------------------- | -------- | -------------------------------------------------------------- | | ----- | ---------- | -------------------------- | -------- | ------------------------------------------------------------------ |
| type | 值类型描述 | String | 'JSSlot' | 固定值 | | type | 值类型描述 | String | 'JSSlot' | 固定值 |
| value | 具体的值 | NodeSchema \| NodeSchema[] | null | 内容为 NodeSchema 类型,详见[组件结构描述](#232-组件结构描述A) | | value | 具体的值 | NodeSchema \| NodeSchema[] | null | 内容为 NodeSchema 类型,详见[组件结构描述](#232-组件结构描述A) |
举例描述:如 **Card** 的 **title** 属性 举例描述:如 **Card** 的 **title** 属性
```json ```json
@ -791,16 +779,14 @@ try {
``` ```
**Function-Return-ReactNode** 描述: **Function-Return-ReactNode** 描述:
| 参数 | 说明 | 值类型 | 默认值 | 备注 | | 参数 | 说明 | 值类型 | 默认值 | 备注 |
| ------ | ---------- | --------------------- | -------- | -------------------------------------------------------------- | | ------ | ---------- | -------------------------- | -------- | ------------------------------------------------------------------ |
| type | 值类型描述 | String | 'JSSlot' | 固定值 | | type | 值类型描述 | String | 'JSSlot' | 固定值 |
| value | 具体的值 | NodeSchema \| NodeSchema[] | null | 内容为 NodeSchema 类型,详见[组件结构描述](#232-组件结构描述A) | | value | 具体的值 | NodeSchema \| NodeSchema[] | null | 内容为 NodeSchema 类型,详见[组件结构描述](#232-组件结构描述A) |
| params | 函数的参数 | String[] | null | 函数的入参,其子节点可以通过 `this[参数名]` 来获取对应的参数。 | | params | 函数的参数 | String[] | null | 函数的入参,其子节点可以通过 `this[参数名]` 来获取对应的参数。 |
举例描述:如 **Table.Column****cell** 属性 举例描述:如 **Table.Column****cell** 属性
```json ```json
@ -868,7 +854,8 @@ try {
}" }"
} }
}, },
"children": [{ "children": [
{
"componentName": "Button", "componentName": "Button",
"props": { "props": {
"text": "按钮", "text": "按钮",
@ -879,7 +866,8 @@ try {
}" }"
} }
} }
}] }
]
} }
``` ```
@ -889,7 +877,6 @@ try {
变量**类型**的属性值描述如下: 变量**类型**的属性值描述如下:
- return 数字类型 - return 数字类型
```json ```json
@ -898,6 +885,7 @@ try {
"value": "this.state.num" "value": "this.state.num"
} }
``` ```
- return 数字类型 - return 数字类型
```json ```json
@ -906,6 +894,7 @@ try {
"value": "this.state.num - this.state.num2" "value": "this.state.num - this.state.num2"
} }
``` ```
- return "8 万" 字符串类型 - return "8 万" 字符串类型
```json ```json
@ -914,6 +903,7 @@ try {
"value": "`${this.state.num}万`" "value": "`${this.state.num}万`"
} }
``` ```
- return "8 万" 字符串类型 - return "8 万" 字符串类型
```json ```json
@ -922,6 +912,7 @@ try {
"value": "this.state.num + '万'" "value": "this.state.num + '万'"
} }
``` ```
- return 13 数字类型 - return 13 数字类型
```json ```json
@ -930,6 +921,7 @@ try {
"value": "getNum(this.state.num, this.state.num2)" "value": "getNum(this.state.num, this.state.num2)"
} }
``` ```
- return true 布尔类型 - return true 布尔类型
```json ```json
@ -958,7 +950,8 @@ try {
}" }"
} }
}, },
"children": [{ "children": [
{
"componentName": "Button", "componentName": "Button",
"props": { "props": {
"text": { "text": {
@ -970,7 +963,8 @@ try {
"type": "JSExpression", "type": "JSExpression",
"value": "this.state.num > this.state.num2" "value": "this.state.num > this.state.num2"
} }
}] }
]
} }
``` ```
@ -984,13 +978,14 @@ try {
type Ti18n = { type Ti18n = {
type: 'i18n'; type: 'i18n';
key: string; // i18n 结构中字段的 key 标识符 key: string; // i18n 结构中字段的 key 标识符
params?: Record<string, JSDataType | JSExpression>; // 模版型 i18n 文案的入参JSDataType 指代传统 JS 值类型 params?: Record<string, string | number | JSExpression>;
} };
``` ```
其中 `key` 对应协议 `i18n` 内容的语料键值,`params` 为语料为字符串模板时的变量内容。 其中 `key` 对应协议 `i18n` 内容的语料键值,`params` 为语料为字符串模板时的变量内容。
假设协议已加入如下 i18n 内容: 假设协议已加入如下 i18n 内容:
```json ```json
{ {
"i18n": { "i18n": {
@ -1041,27 +1036,26 @@ type Ti18n = {
} }
``` ```
#### 2.3.5 上下文 API 描述A #### 2.3.5 上下文 API 描述A
在上述**事件类型描述**和**变量类型描述**中,在函数或 JS 表达式内,均可以通过 **this** 对象获取当前组件所在容器React Class的实例化对象在搭建场景下的渲染模块和出码模块实现上统一约定了该实例化 **this** 对象下所挂载的最小 API 集合,以保障搭建协议具备有一致的**数据流**和**事件上下文**。  在上述**事件类型描述**和**变量类型描述**中,在函数或 JS 表达式内,均可以通过 **this** 对象获取当前组件所在容器React Class的实例化对象在搭建场景下的渲染模块和出码模块实现上统一约定了该实例化 **this** 对象下所挂载的最小 API 集合,以保障搭建协议具备有一致的**数据流**和**事件上下文**。
##### 2.3.5.1 容器 API ##### 2.3.5.1 容器 API
| 参数 | 说明 | 类型 | 备注 | | 参数 | 说明 | 类型 | 备注 |
| ----------------------------------- | --------------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------- | | ----------------------------------- | ---------------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------- |
| **this {}** | 当前区块容器的实例对象 | Class Instance | - | | **this** | 当前区块容器的实例对象 | Class Instance | - |
| *this*.state | 三种容器实例的数据对象 state | Object | - | | _this_.state | 三种容器实例的数据对象 state | Object | - |
| *this*.setState(newState, callback) | 三种容器实例更新数据的方法 | Function | 这个 setState 通常会异步执行,详见下文 [setState](#setstate) | | _this_.setState(newState, callback) | 三种容器实例更新数据的方法 | Function | 这个 setState 通常会异步执行,详见下文 [setState](#setstate) |
| *this*.customMethod() | 三种容器实例的自定义方法 | Function | - | | _this_.customMethod() | 三种容器实例的自定义方法 | Function | - |
| *this*.dataSourceMap {} | 三种容器实例的数据源对象 Map | Object | 单个请求的 id 为 key, value 详见下文 [DataSourceMapItem 结构描述](#datasourcemapitem-结构描述) | | _this_.dataSourceMap | 三种容器实例的数据源对象 Map | Object | 单个请求的 id 为 key, value 详见下文 [DataSourceMapItem 结构描述](#datasourcemapitem-结构描述) |
| *this*.reloadDataSource() | 三种容器实例的初始化异步数据请求重载 | Function | 返回 \<Promise\> | | _this_.reloadDataSource | 三种容器实例的初始化异步数据请求重载 | Function | 返回 Promise |
| **this.page {}** | 当前页面容器的实例对象 | Class Instance | | | **this.page** | 当前页面容器的实例对象 | Class Instance | |
| *this.page*.props | 读取页面路由,参数等相关信息 | Object | query 查询参数 { key: value } 形式path 路径uri 页面唯一标识;其它扩展字段 | | _this.page_.props | 读取页面路由,参数等相关信息 | Object | query 查询参数 { key: value } 形式path 路径uri 页面唯一标识;其它扩展字段 |
| *this.page*.xxx | 继承 this 对象所有 API | | 此处 `xxx` 代指 `this.page` 中的其他 API | | _this.page_.xxx | 继承 this 对象所有 API | | 此处 `xxx` 代指 `this.page` 中的其他 API |
| **this.component {}** | 当前低代码业务组件容器的实例对象 | Class Instance | | | **this.component** | 当前低代码业务组件容器的实例对象 | Class Instance | |
| *this.component*.props | 读取低代码业务组件容器的外部传入的 props | Object | | | _this.component_.props | 读取低代码业务组件容器的外部传入的 props | Object | |
| *this.component*.xxx | 继承 this 对象所有 API | | 此处 `xxx` 代指 `this.component` 中的其他 API | | _this.component_.xxx | 继承 this 对象所有 API | | 此处 `xxx` 代指 `this.component` 中的其他 API |
| **this.$(ref)** | 获取组件的引用(单个) | Component Instance | `ref` 对应组件上配置的 `ref` 属性,用于唯一标识一个组件;若有同名的,则会返回第一个匹配的。 | | **this.$(ref)** | 获取组件的引用(单个) | Component Instance | `ref` 对应组件上配置的 `ref` 属性,用于唯一标识一个组件;若有同名的,则会返回第一个匹配的。 |
| **this.$$(ref)** | 获取组件的引用(所有同名的) | Array of Component Instances | `ref` 对应组件上配置的 `ref` 属性,用于唯一标识一个组件;总是返回一个数组,里面是所有匹配 `ref` 的组件的引用。 | | **this.$$(ref)** | 获取组件的引用(所有同名的) | Array of Component Instances | `ref` 对应组件上配置的 `ref` 属性,用于唯一标识一个组件;总是返回一个数组,里面是所有匹配 `ref` 的组件的引用。 |
@ -1092,11 +1086,10 @@ this.setState((prevState) => ({ count: prevState.count + 1 }));
为了方便更新部分状态,`setState` 会将 `newState` 浅合并到新的 `state` 上。 为了方便更新部分状态,`setState` 会将 `newState` 浅合并到新的 `state` 上。
##### DataSourceMapItem 结构描述 ##### DataSourceMapItem 结构描述
| 参数 | 说明 | 类型 | 备注 | | 参数 | 说明 | 类型 | 备注 |
| ------------ | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | | ------------ | -------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------- |
| load(params) | 调用单个数据源 | Function | 当前参数 params 会替换 [ComponentDataSourceItemOptions 对象描述](#2315-componentdatasourceitemoptions-对象描述)中的 params 内容 | | load(params) | 调用单个数据源 | Function | 当前参数 params 会替换 [ComponentDataSourceItemOptions 对象描述](#2315-componentdatasourceitemoptions-对象描述)中的 params 内容 |
| status | 获取单个数据源上次请求状态 | String | loading、loaded、error、init | | status | 获取单个数据源上次请求状态 | String | loading、loaded、error、init |
| data | 获取上次请求成功后的数据 | Any | | | data | 获取上次请求成功后的数据 | Any | |
@ -1104,12 +1097,10 @@ this.setState((prevState) => ({ count: prevState.count + 1 }));
备注:如果组件没有在区块容器内,而是直接在页面内,那么 `this === this.page` 备注:如果组件没有在区块容器内,而是直接在页面内,那么 `this === this.page`
##### 2.3.5.2 循环数据 API ##### 2.3.5.2 循环数据 API
获取在循环场景下的数据对象。举例:上层组件设置了 loop 循环数据,且设置了 `loopArgs["item", "index"]`,当前组件的属性表达式或绑定的事件函数中,可以通过 this 上下文获取所在循环的数据环境;默认值为 `['item','index']` ,如有多层循环,需要自定义不同 loopArgs同样通过 `this[自定义循环别名]` 获取对应的循环数据和序号; 获取在循环场景下的数据对象。举例:上层组件设置了 loop 循环数据,且设置了 `loopArgs["item", "index"]`,当前组件的属性表达式或绑定的事件函数中,可以通过 this 上下文获取所在循环的数据环境;默认值为 `['item','index']` ,如有多层循环,需要自定义不同 loopArgs同样通过 `this[自定义循环别名]` 获取对应的循环数据和序号;
| 参数 | 说明 | 类型 | 可选值 | | 参数 | 说明 | 类型 | 可选值 |
| ---------- | --------------------------------- | ------ | ------ | | ---------- | --------------------------------- | ------ | ------ |
| this.item | 获取当前 index 对应的循环体数据 | Any | - | | this.item | 获取当前 index 对应的循环体数据 | Any | - |
@ -1120,17 +1111,17 @@ this.setState((prevState) => ({ count: prevState.count + 1 }));
用于描述物料开发过程中自定义扩展或引入的第三方工具类例如lodash 及 moment增强搭建基础协议的扩展性提供通用的工具类方法的配置方案及调用 API。 用于描述物料开发过程中自定义扩展或引入的第三方工具类例如lodash 及 moment增强搭建基础协议的扩展性提供通用的工具类方法的配置方案及调用 API。
| 参数 | 说明 | 类型 | 支持变量 | 默认值 | | 参数 | 说明 | 类型 | 支持变量 | 默认值 |
| ------------------ | ------------------ | ---------------------------------------------------------------------------------------------------------------- | -------- | ------ | | ------- | ---------------- | ---------------------------------------------------------------------------- | -------- | ------ |
| utils[] | 工具类扩展映射关系 | **UtilItem**[] | - | | | name | 工具类扩展项名称 | String | - | |
| *UtilItem*.name | 工具类扩展项名称 | String | - | | | type | 工具类扩展项类型 | 枚举, `'npm'` (代表 npm 类型) / `'function'` (代表 Javascript 函数类型) | - | |
| *UtilItem*.type | 工具类扩展项类型 | 枚举, `'npm'` (代表公网 npm 类型) / `'tnpm'` (代表阿里巴巴内部 npm 类型) / `'function'` (代表 Javascript 函数类型) | - | | | content | 工具类扩展项内容 | [ComponentMap 类型](#22-组件映射关系a) 或 [JSFunction](#2342事件函数类型a) | - | |
| *UtilItem*.content | 工具类扩展项内容 | [ComponentMap 类型](#22-组件映射关系a) 或 [JSFunction](#2342事件函数类型a) | - | |
描述示例: 描述示例:
```javascript ```javascript
{ {
utils: [{ utils: [
{
name: 'clone', name: 'clone',
type: 'npm', type: 'npm',
content: { content: {
@ -1139,9 +1130,10 @@ this.setState((prevState) => ({ count: prevState.count + 1 }));
exportName: 'clone', exportName: 'clone',
subName: '', subName: '',
destructuring: false, destructuring: false,
main: '/lib/clone' main: '/lib/clone',
} },
}, { },
{
name: 'moment', name: 'moment',
type: 'npm', type: 'npm',
content: { content: {
@ -1150,16 +1142,19 @@ this.setState((prevState) => ({ count: prevState.count + 1 }));
exportName: 'Moment', exportName: 'Moment',
subName: '', subName: '',
destructuring: true, destructuring: true,
main: '' main: '',
} },
}, { },
{
name: 'recordEvent', name: 'recordEvent',
type: 'function', type: 'function',
content: { content: {
type: 'JSFunction', type: 'JSFunction',
value: "function(logkey, gmkey, gokey, reqMethod) {\n goldlog.record('/xxx.event.' + logkey, gmkey, gokey, reqMethod);\n}" 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 | | i18n | 国际化语料信息 | Object | - | null |
描述示例: 描述示例:
```json ```json
@ -1247,6 +1240,7 @@ export const recordEvent = function(logkey, gmkey, gokey, reqMethod) {
``` ```
使用举例(已废弃) 使用举例(已废弃)
```json ```json
{ {
"componentName": "Button", "componentName": "Button",
@ -1293,12 +1287,11 @@ export const recordEvent = function(logkey, gmkey, gokey, reqMethod) {
路由配置的结构说明: 路由配置的结构说明:
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 | | 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 |
| ----------- | ---------------------- | ------------------------------- | ------ | --------- | ------ | | ----------- | ---------------------------------- | ------------------------------- | ------ | --------- | ------ |
| baseName | 应用根路径 | String | - | '/' | 选填| | | baseName | 应用根路径 | String | - | '/' | 选填| |
| historyMode | history 模式 | 枚举类型,包括'browser'、'hash' | - | 'browser' | 选填| | | historyMode | history 模式 | 枚举类型,包括'browser'、'hash' | - | 'browser' | 选填| |
| routes | 路由对象组,路径与页面的关系对照组 | Route[] | - | - | 必填| | | routes | 路由对象组,路径与页面的关系对照组 | Route[] | - | - | 必填| |
##### 2.11.2 Route (路由记录)结构描述 ##### 2.11.2 Route (路由记录)结构描述
路由记录路径与页面的关系对照。Route 的结构说明: 路由记录路径与页面的关系对照。Route 的结构说明:
@ -1307,10 +1300,8 @@ export const recordEvent = function(logkey, gmkey, gokey, reqMethod) {
| -------- | ---------------------------- | ---------------------------- | ------ | ------ | ---------------------------------------------------------------------- | | -------- | ---------------------------- | ---------------------------- | ------ | ------ | ---------------------------------------------------------------------- |
| name | 该路径项的名称 | String | - | - | 选填 | | name | 该路径项的名称 | String | - | - | 选填 |
| path | 路径 | String | - | - | 必填,路径规则详见下面说明 | | path | 路径 | String | - | - | 必填,路径规则详见下面说明 |
| query | 路径的 query 参数 | Object | - | - | 选填 |
| page | 路径对应的页面 ID | String | - | - | 选填page 与 redirect 字段中必须要有有一个存在 | | page | 路径对应的页面 ID | String | - | - | 选填page 与 redirect 字段中必须要有有一个存在 |
| redirect | 此路径需要重定向到的路由信息 | String \| Object \| Function | - | - | 选填page 与 redirect 字段中必须要有有一个存在,详见下文 **redirect** | | redirect | 此路径需要重定向到的路由信息 | String \| Object \| Function | - | - | 选填page 与 redirect 字段中必须要有有一个存在,详见下文 **redirect** |
| meta | 路由元数据 | Object | - | - | 选填 |
| children | 子路由 | Route[] | - | - | 选填 | | children | 子路由 | Route[] | - | - | 选填 |
以上结构仅说明了路由记录需要的必需字段,如果需要更多的信息字段可以自行实现。 以上结构仅说明了路由记录需要的必需字段,如果需要更多的信息字段可以自行实现。
@ -1325,6 +1316,7 @@ path页面路径是浏览器URL的组成部分同时大部分网站的
路径规则是路由配置的重要组成部分,我们希望一个路径配置的基本能力需要支持具体的路径(/xxx与路径参数 (/:abc 路径规则是路由配置的重要组成部分,我们希望一个路径配置的基本能力需要支持具体的路径(/xxx与路径参数 (/:abc
以一个 `/one/:two?/three/:four?/:five?` 路径为例,它能够解析以下路径: 以一个 `/one/:two?/three/:four?/:five?` 路径为例,它能够解析以下路径:
- `/one/three` - `/one/three`
- `/one/:two/three` - `/one/:two/three`
- `/one/three/:four` - `/one/three/:four`
@ -1339,6 +1331,7 @@ path页面路径是浏览器URL的组成部分同时大部分网站的
关于 **redirect** 字段的详细说明: 关于 **redirect** 字段的详细说明:
**redirect** 字段有三种填入类型,分别是 `String``Object``Function` **redirect** 字段有三种填入类型,分别是 `String``Object``Function`
1. 字符串(`String`)格式下默认处理为重定向到路径,支持传入 '/xxx'、'/xxx?ab=c'。 1. 字符串(`String`)格式下默认处理为重定向到路径,支持传入 '/xxx'、'/xxx?ab=c'。
2. 对象(`String`)格式下可传入路由对象,如 { name: 'xxx' }、{ path: '/xxx' },可重定向到对应的路由对象。 2. 对象(`String`)格式下可传入路由对象,如 { name: 'xxx' }、{ path: '/xxx' },可重定向到对应的路由对象。
3. 函数`Function`格式为`(to) => Route`,它的入参为当前路由项信息,支持返回一个 Route 对象或者字符串,存在一些特殊情况,在重定向的时候需要对重定向之后的路径进行处理的情况下,需要使用函数声明。 3. 函数`Function`格式为`(to) => Route`,它的入参为当前路由项信息,支持返回一个 Route 对象或者字符串,存在一些特殊情况,在重定向的时候需要对重定向之后的路径进行处理的情况下,需要使用函数声明。
@ -1347,7 +1340,7 @@ path页面路径是浏览器URL的组成部分同时大部分网站的
{ {
"redirect": { "redirect": {
"type": "JSFunction", "type": "JSFunction",
"value": "(to) => { return { path: '/a', query: { fromPath: to.path } } }", "value": "(to) => { return { path: '/a', query: { fromPath: to.path } } }"
} }
} }
``` ```
@ -1389,20 +1382,19 @@ path页面路径是浏览器URL的组成部分同时大部分网站的
在一些比较复杂的场景下,允许声明一层页面映射关系,以支持页面声明更多信息与配置,同时能够支持不同类型的产物。 在一些比较复杂的场景下,允许声明一层页面映射关系,以支持页面声明更多信息与配置,同时能够支持不同类型的产物。
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 | | 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 |
| ------- | --------------------- | ------ | ------ | ------ | -------------------------------------------------------- | | --------- | ---------- | ------ | ------ | ------ | ----------------------------------------------------------------------------- |
| id | 页面 id | String | - | - | 必填 | | id | 页面 id | String | - | - | 必填 |
| type | 页面类型 | String | - | - | 选填,可用来区分页面的类型 | | type | 页面类型 | String | - | - | 选填,可用来区分页面的类型,如 componentsTreepackage默认为 componentsTree |
| treeId | 对应的低代码树中的 id | String | - | - | 选填,页面对应的 componentsTree 中的子项 id | | mappingId | 映射 id | String | - | - | 必填,根据页面的类型 type 判断及确定目标的 id |
| packageId | 对应的资产包对象 | String | - | - | 选填,页面对应的资产包对象,一般用于微应用场景下,当路由匹配到当前页面的时候,会加载 `packageId` 对应的微应用进行渲染。 |
| meta | 页面元信息 | Object | - | - | 选填,用于描述当前应用的配置信息 | | meta | 页面元信息 | Object | - | - | 选填,用于描述当前应用的配置信息 |
| config | 页面配置 | Object | - | - | 选填,用于描述当前应用的元数据信息 | | config | 页面配置 | Object | - | - | 选填,用于描述当前应用的元数据信息 |
#### 2.12.1 微应用(低代码+)相关说明 #### 2.12.1 微应用(低代码+)相关说明
在开发过程中,我们经常会遇到一些特殊的情况,比如一个低代码应用想要集成一些别的系统的页面或者系统中的一些页面只能是源码开发(与低代码相对的纯工程代码形式),为了满足更多的使用场景,应用级渲染引擎引入了微应用(微前端)的概念,使低代码页面与其他的页面结合成为可能。 在开发过程中,我们经常会遇到一些特殊的情况,比如一个低代码应用想要集成一些别的系统的页面或者系统中的一些页面只能是源码开发(与低代码相对的纯工程代码形式),为了满足更多的使用场景,应用级渲染引擎引入了微应用(微前端)的概念,使低代码页面与其他的页面结合成为可能。
微应用对象通过资产包加载,需要暴露两个生命周期方法: 微应用对象通过资产包加载,需要暴露两个生命周期方法:
- mount(container: HTMLElement, props: any) - mount(container: HTMLElement, props: any)
- 说明:微应用挂载到 containerdom 节点)的调用方法,会在渲染微应用时调用 - 说明:微应用挂载到 containerdom 节点)的调用方法,会在渲染微应用时调用
- unmout(container: HTMLElement, props: any) - unmout(container: HTMLElement, props: any)
@ -1469,7 +1461,6 @@ path页面路径是浏览器URL的组成部分同时大部分网站的
] ]
``` ```
## 3 应用描述 ## 3 应用描述
### 3.1 文件目录 ### 3.1 文件目录
@ -1477,52 +1468,28 @@ path页面路径是浏览器URL的组成部分同时大部分网站的
以下是推荐的应用目录结构,与标准源码 build-scripts 对齐,这里的目录结构是帮助理解应用级协议的设计,不做强约束 以下是推荐的应用目录结构,与标准源码 build-scripts 对齐,这里的目录结构是帮助理解应用级协议的设计,不做强约束
```html ```html
├── META/ # 低代码元数据信息,用于多分支冲突解决、数据回滚等功能 ├── META/ # 低代码元数据信息,用于多分支冲突解决、数据回滚等功能 ├── public/ # 静态文件,构建时会
├── public/ # 静态文件,构建时会 copy 到 build/ 目录 copy 到 build/ 目录 │ ├── index.html # 应用入口 HTML │ └── favicon.png # Favicon ├── src/ │ ├──
│ ├── index.html # 应用入口 HTML components/ # 应用内的低代码业务组件 │ │ └── guide-component/ │ │ ├── index.js # 组件入口 │ │ ├──
│ └── favicon.png # Favicon components.js # 组件依赖的其他组件 │ │ ├── schema.js # schema 描述 │ │ └── index.scss # css 样式 │
├── src/ ├── pages/ # 页面 │ │ └── home/ # Home 页面 │ │ ├── index.js # 页面入口 │ │ └── index.scss # css
│ ├── components/ # 应用内的低代码业务组件 样式 │ ├── layouts/ │ │ └── basic-layout/ # layout 组件名称 │ │ ├── index.js # layout 入口 │ │ ├──
│ │ └── guide-component/ components.js # layout 组件依赖的其他组件 │ │ ├── schema.js # layout schema 描述 │ │ └── index.scss
│ │ ├── index.js # 组件入口 # layout css 样式 │ ├── config/ # 配置信息 │ │ ├── components.js # 应用上下文所有组件 │ │ ├──
│ │ ├── components.js # 组件依赖的其他组件 routes.js # 页面路由列表 │ │ └── app.js # 应用配置文件 │ ├── utils/ # 工具库 │ │ └── index.js #
│ │ ├── schema.js # schema 描述 应用第三方扩展函数 │ ├── locales/ # [可选] 国际化资源 │ │ ├── en-US │ │ └── zh-CN │ ├── global.scss
│ │ └── index.scss # css 样式 # 全局样式 │ └── index.jsx # 应用入口脚本,依赖 config/routes.js 的路由配置动态生成路由; ├──
│ ├── pages/ # 页面 webpack.config.js # 项目工程配置,包含插件配置及自定义 webpack 配置等 ├── README.md ├── package.json
│ │ └── home/ # Home 页面 ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .stylelintignore └──
│ │ ├── index.js # 页面入口 .stylelintrc.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 ### 3.2 应用级别 APIs
> 下文中 `xxx` 代指任意 API > 下文中 `xxx` 代指任意 API
#### 3.2.1 路由 Router API #### 3.2.1 路由 Router API
- this.location.`xxx` 「不推荐,推荐统一通过 this.router api」 - this.location.`xxx` 「不推荐,推荐统一通过 this.router api」
- this.history.`xxx` 「不推荐,推荐统一通过 this.router api」 - this.history.`xxx` 「不推荐,推荐统一通过 this.router api」
- this.match.`xxx` 「不推荐,推荐统一通过 this.router api」 - this.match.`xxx` 「不推荐,推荐统一通过 this.router api」
@ -1531,7 +1498,7 @@ path页面路径是浏览器URL的组成部分同时大部分网站的
##### Router 结构说明 ##### Router 结构说明
| API | 函数签名 | 说明 | | API | 函数签名 | 说明 |
| -------------- | ---------------------------------------------------------- | -------------------------------------------------------------- | | ---------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| getCurrentRoute | () => RouteLocation | 获取当前解析后的路由信息RouteLocation 结构详见下面说明 | | getCurrentRoute | () => RouteLocation | 获取当前解析后的路由信息RouteLocation 结构详见下面说明 |
| push | (target: string \| Route) => void | 路由跳转方法,跳转到指定的路径或者 Route | | push | (target: string \| Route) => void | 路由跳转方法,跳转到指定的路径或者 Route |
| replace | (target: string \| Route) => void | 路由跳转方法,与 `push` 的区别在于不会增加一条历史记录而是替换当前的历史记录 | | replace | (target: string \| Route) => void | 路由跳转方法,与 `push` 的区别在于不会增加一条历史记录而是替换当前的历史记录 |
@ -1543,7 +1510,7 @@ path页面路径是浏览器URL的组成部分同时大部分网站的
**RouteLocation** 是路由控制器匹配到对应的路由记录后进行解析产生的对象,它的结构如下: **RouteLocation** 是路由控制器匹配到对应的路由记录后进行解析产生的对象,它的结构如下:
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 | | 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 |
| -------------- | ---------------------- | ------ | ------ | ------ | ------ | | -------------- | ---------------------------------- | ------ | ------ | ------ | ---------------------------------------------- |
| path | 当前解析后的路径 | String | - | - | 必填 | | path | 当前解析后的路径 | String | - | - | 必填 |
| hash | 当前路径的 hash 值,以 # 开头 | String | - | - | 必填 | | hash | 当前路径的 hash 值,以 # 开头 | String | - | - | 必填 |
| href | 当前的全部路径 | String | - | - | 必填 | | href | 当前的全部路径 | String | - | - | 必填 |
@ -1554,33 +1521,38 @@ path页面路径是浏览器URL的组成部分同时大部分网站的
| redirectedFrom | 原本指向向的路由记录 | Route | - | - | 选填,在重定向到当前地址之前,原先想访问的地址 | | redirectedFrom | 原本指向向的路由记录 | Route | - | - | 选填,在重定向到当前地址之前,原先想访问的地址 |
| fullPath | 包括 search 和 hash 在内的完整地址 | String | - | - | 选填 | | fullPath | 包括 search 和 hash 在内的完整地址 | String | - | - | 选填 |
##### beforeRouteLeave ##### beforeRouteLeave
通过 beforeRouteLeave 注册的路由守卫方法会在每次路由跳转前执行。该方法一般会在应用鉴权,路由重定向等场景下使用。 通过 beforeRouteLeave 注册的路由守卫方法会在每次路由跳转前执行。该方法一般会在应用鉴权,路由重定向等场景下使用。
> `beforeRouteLeave` 只在 `router.push/replace` 的方法调用时生效。 > `beforeRouteLeave` 只在 `router.push/replace` 的方法调用时生效。
传入守卫的入参为: 传入守卫的入参为:
* to: 即将要进入的目标路由(RouteLocation)
* from: 当前导航正要离开的路由(RouteLocation) - to: 即将要进入的目标路由(RouteLocation)
- from: 当前导航正要离开的路由(RouteLocation)
该守卫返回一个 `boolean` 或者路由对象来告知路由控制器接下来的行为。 该守卫返回一个 `boolean` 或者路由对象来告知路由控制器接下来的行为。
* 如果返回 `false` 则停止跳转
* 如果返回 `true`,则继续跳转 - 如果返回 `false` 则停止跳转
* 如果返回路由对象,则重定向至对应的路由 - 如果返回 `true`,则继续跳转
- 如果返回路由对象,则重定向至对应的路由
**使用范例:** **使用范例:**
```json ```json
{ {
"componentsTree": [{ "componentsTree": [
{
"componentName": "Page", "componentName": "Page",
"fileName": "Page1", "fileName": "Page1",
"props": {}, "props": {},
"children": [{ "children": [
{
"componentName": "Div", "componentName": "Div",
"props": {}, "props": {},
"children": [{ "children": [
{
"componentName": "Button", "componentName": "Button",
"props": { "props": {
"text": "跳转到首页", "text": "跳转到首页",
@ -1588,35 +1560,43 @@ path页面路径是浏览器URL的组成部分同时大部分网站的
"type": "JSFunction", "type": "JSFunction",
"value": "function () { this.router.push('/home'); }" "value": "function () { this.router.push('/home'); }"
} }
}, }
}] }
}], ]
}] }
]
}
]
} }
``` ```
#### 3.2.2 应用级别的公共函数或第三方扩展 #### 3.2.2 应用级别的公共函数或第三方扩展
- this.utils.`xxx` - this.utils.`xxx`
#### 3.2.3 国际化相关 API #### 3.2.3 国际化相关 API
| API | 函数签名 | 说明 | | API | 函数签名 | 说明 |
| -------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------ | | -------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| this.i18n | (i18nKey: string, params?: { [paramName: string]: string; }) => string | i18nKey 是语料的标识符params 可选,是用来做模版字符串替换的。返回语料字符串 | | this.i18n | (i18nKey: string, params?: { [paramName: string]: string; }) => string | i18nKey 是语料的标识符params 可选,是用来做模版字符串替换的。返回语料字符串 |
| this.getLocale | () => string | 返回当前环境语言 code | | this.getLocale | () => string | 返回当前环境语言 code |
| this.setLocale | (locale: string) => void | 设置当前环境语言 code | | this.setLocale | (locale: string) => void | 设置当前环境语言 code |
**使用范例:** **使用范例:**
```json ```json
{ {
"componentsTree": [{ "componentsTree": [
{
"componentName": "Page", "componentName": "Page",
"fileName": "Page1", "fileName": "Page1",
"props": {}, "props": {},
"children": [{ "children": [
{
"componentName": "Div", "componentName": "Div",
"props": {}, "props": {},
"children": [{ "children": [
{
"componentName": "Button", "componentName": "Button",
"props": { "props": {
"children": { "children": {
@ -1627,18 +1607,22 @@ path页面路径是浏览器URL的组成部分同时大部分网站的
"type": "JSFunction", "type": "JSFunction",
"value": "function () { this.setLocale('en-US'); }" "value": "function () { this.setLocale('en-US'); }"
} }
}
}, },
}, { {
"componentName": "Button", "componentName": "Button",
"props": { "props": {
"children": { "children": {
"type": "JSExpression", "type": "JSExpression",
"value": "this.i18n('i18n-chicken', { count: this.state.count })" "value": "this.i18n('i18n-chicken', { count: this.state.count })"
}, }
}, }
}] }
}], ]
}], }
]
}
],
"i18n": { "i18n": {
"zh-CN": { "zh-CN": {
"i18n-hello": "你好", "i18n-hello": "你好",

View File

@ -57,6 +57,7 @@
"lerna": "^4.0.0", "lerna": "^4.0.0",
"typescript": "^5.4.2", "typescript": "^5.4.2",
"yarn": "^1.22.17", "yarn": "^1.22.17",
"prettier": "^3.2.5",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup": "^4.13.0", "rollup": "^4.13.0",
"vite": "^5.1.6", "vite": "^5.1.6",

View File

@ -1,6 +1,6 @@
{ {
"name": "@alilc/lowcode-types", "name": "@alilc/lowcode-types",
"version": "1.3.2", "version": "1.3.3",
"description": "Types for Ali lowCode engine", "description": "Types for Ali lowCode engine",
"files": [ "files": [
"es", "es",

View File

@ -1,16 +1,19 @@
import { IPublicEnumContextMenuType } from '../enum'; import { IPublicEnumContextMenuType } from '../enum';
import { IPublicModelNode } from '../model'; import { IPublicModelNode } from '../model';
import { IPublicTypeI18nData } from './i8n-data'; import { IPublicTypeI18nData } from './i18n-data';
import { IPublicTypeHelpTipConfig } from './widget-base-config'; import { IPublicTypeHelpTipConfig } from './widget-base-config';
export interface IPublicTypeContextMenuItem extends Omit<IPublicTypeContextMenuAction, 'condition' | 'disabled' | 'items'> { export interface IPublicTypeContextMenuItem
extends Omit<
IPublicTypeContextMenuAction,
'condition' | 'disabled' | 'items'
> {
disabled?: boolean; disabled?: boolean;
items?: Omit<IPublicTypeContextMenuItem, 'items'>[]; items?: Omit<IPublicTypeContextMenuItem, 'items'>[];
} }
export interface IPublicTypeContextMenuAction { export interface IPublicTypeContextMenuAction {
/** /**
* *
* Unique identifier for the action * Unique identifier for the action
@ -41,7 +44,11 @@ export interface IPublicTypeContextMenuAction {
* *
* Sub-menu items or function to generate child node, optional * Sub-menu items or function to generate child node, optional
*/ */
items?: Omit<IPublicTypeContextMenuAction, 'items'>[] | ((nodes?: IPublicModelNode[]) => Omit<IPublicTypeContextMenuAction, 'items'>[]); items?:
| Omit<IPublicTypeContextMenuAction, 'items'>[]
| ((
nodes?: IPublicModelNode[],
) => Omit<IPublicTypeContextMenuAction, 'items'>[]);
/** /**
* *
@ -60,4 +67,3 @@ export interface IPublicTypeContextMenuAction {
*/ */
help?: IPublicTypeHelpTipConfig; help?: IPublicTypeHelpTipConfig;
} }

View File

@ -24,7 +24,7 @@ export * from './widget-base-config';
export * from './node-data'; export * from './node-data';
export * from './icon-type'; export * from './icon-type';
export * from './transformed-component-metadata'; export * from './transformed-component-metadata';
export * from './i8n-data'; export * from './i18n-data';
export * from './npm-info'; export * from './npm-info';
export * from './drag-node-data-object'; export * from './drag-node-data-object';
export * from './drag-node-object'; export * from './drag-node-object';

View File

@ -12,5 +12,7 @@ export interface IPublicTypeLowCodeComponent {
} }
export type IPublicTypeProCodeComponent = IPublicTypeNpmInfo; export type IPublicTypeProCodeComponent = IPublicTypeNpmInfo;
export type IPublicTypeComponentMap = IPublicTypeProCodeComponent | IPublicTypeLowCodeComponent; export type IPublicTypeComponentMap =
| IPublicTypeProCodeComponent
| IPublicTypeLowCodeComponent;
export type IPublicTypeComponentsMap = IPublicTypeComponentMap[]; export type IPublicTypeComponentsMap = IPublicTypeComponentMap[];

7
rollup.config.mjs Normal file
View File

@ -0,0 +1,7 @@
export default {
input: 'src/main.js',
output: {
file: 'bundle.js',
format: 'cjs'
}
};

View File

@ -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"
}
}

View File

@ -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<any>;
}
export interface ReactRender extends RenderBase {}
export type ReactApp = App<ReactRender>;
export const createApp = createAppFunction<AppOptions, ReactRender>(
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(<AppComponent context={appContext} />);
},
unmount() {
if (root) {
root.unmount();
root = undefined;
}
},
};
return {
renderBase: reactRender,
renderer,
};
}
);

View File

@ -1,3 +0,0 @@
{
"extends": "../../tsconfig.json"
}

View File

@ -5,6 +5,12 @@
"type": "module", "type": "module",
"bugs": "https://github.com/alibaba/lowcode-engine/issues", "bugs": "https://github.com/alibaba/lowcode-engine/issues",
"homepage": "https://github.com/alibaba/lowcode-engine/#readme", "homepage": "https://github.com/alibaba/lowcode-engine/#readme",
"license": "MIT",
"scripts": {
"build": "",
"test": "vitest --run",
"test:watch": "vitest"
},
"dependencies": { "dependencies": {
"@alilc/lowcode-types": "1.3.2", "@alilc/lowcode-types": "1.3.2",
"lodash-es": "^4.17.21" "lodash-es": "^4.17.21"

View File

@ -1,26 +1,18 @@
import { import type { Project, Package, PlainObject } from '../types';
type ProjectSchema, import { type PackageManager, createPackageManager } from '../package';
type Package, import { createPluginManager, type Plugin } from '../plugin';
type AnyObject, import { createScope, type CodeScope } from '../code-runtime';
} from '@alilc/runtime-shared'; import { appBoosts, type AppBoosts, type AppBoostsManager } from '../boosts';
import { type PackageManager, createPackageManager } from '../core/package'; import { type AppSchema, createAppSchema } from '../schema';
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';
export interface AppOptionsBase { export interface AppOptionsBase {
schema: ProjectSchema; schema: Project;
packages?: Package[]; packages?: Package[];
plugins?: Plugin[]; plugins?: Plugin[];
appScopeValue?: AnyObject; appScopeValue?: PlainObject;
} }
export interface RenderBase { export interface AppBase {
mount: (el: HTMLElement) => void | Promise<void>; mount: (el: HTMLElement) => void | Promise<void>;
unmount: () => void | Promise<void>; unmount: () => void | Promise<void>;
} }
@ -36,13 +28,13 @@ export interface AppContext {
boosts: AppBoostsManager; boosts: AppBoostsManager;
} }
type AppCreator<O, T> = ( type AppCreator<O, T extends AppBase> = (
appContext: Omit<AppContext, 'renderer'>, appContext: Omit<AppContext, 'renderer'>,
appOptions: O appOptions: O,
) => Promise<{ renderBase: T; renderer?: any }>; ) => Promise<{ appBase: T; renderer?: any }>;
export type App<T extends RenderBase = RenderBase> = { export type App<T extends AppBase = AppBase> = {
schema: ProjectSchema; schema: Project;
config: Map<string, any>; config: Map<string, any>;
readonly boosts: AppBoosts; readonly boosts: AppBoosts;
@ -55,11 +47,14 @@ export type App<T extends RenderBase = RenderBase> = {
* @param options * @param options
* @returns * @returns
*/ */
export function createAppFunction< export function createAppFunction<O extends AppOptionsBase, T extends AppBase = AppBase>(
O extends AppOptionsBase, appCreator: AppCreator<O, T>,
T extends RenderBase = RenderBase ): (options: O) => Promise<App<T>> {
>(appCreator: AppCreator<O, T>): (options: O) => Promise<App<T>> { if (typeof appCreator !== 'function') {
return async options => { throw Error('The first parameter must be a function.');
}
return async (options) => {
const { schema, appScopeValue = {} } = options; const { schema, appScopeValue = {} } = options;
const appSchema = createAppSchema(schema); const appSchema = createAppSchema(schema);
const appConfig = new Map<string, any>(); const appConfig = new Map<string, any>();
@ -77,14 +72,19 @@ export function createAppFunction<
boosts: appBoosts, 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({ const pluginManager = createPluginManager({
...appContext, ...appContext,
renderer, renderer,
}); });
if (options.plugins?.length) { 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) { if (options.packages?.length) {
@ -100,7 +100,7 @@ export function createAppFunction<
return appBoosts.value; return appBoosts.value;
}, },
}, },
renderBase appBase,
); );
}; };
} }

View File

@ -1,104 +1,27 @@
import { isJsFunction } from '@alilc/runtime-shared'; import { CreateContainerOptions, createContainer } from '../container';
import { import { createCodeRuntime, createScope } from '../code-runtime';
type CodeRuntime, import { throwRuntimeError } from '../utils/error';
createCodeRuntime, import { validateContainerSchema } from '../validator/schema';
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<C = any>
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<C = any> {
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;
}
export interface ComponentOptionsBase<C> { export interface ComponentOptionsBase<C> {
componentsTree: RootSchema; componentsTree: RootSchema;
componentsRecord: Record<string, C | Package>; componentsRecord: Record<string, C | Package>;
supCodeScope?: CodeScope;
initScopeValue?: AnyObject;
dataSourceCreator: DataSourceCreator; dataSourceCreator: DataSourceCreator;
} }
export function createComponentFunction< export function createComponentFunction<C, O extends ComponentOptionsBase<C>>(options: {
C,
O extends ComponentOptionsBase<C>
>(options: {
stateCreator: (initState: AnyObject) => StateContext; stateCreator: (initState: AnyObject) => StateContext;
componentCreator: (container: Container, componentOptions: O) => C; componentCreator: (container: Container, componentOptions: O) => C;
defaultOptions?: Partial<O>; defaultOptions?: Partial<O>;
}): (componentOptions: O) => C { }): (componentOptions: O) => C {
const { stateCreator, componentCreator, defaultOptions = {} } = options; const { stateCreator, componentCreator, defaultOptions = {} } = options;
return componentOptions => { return (componentOptions) => {
const finalOptions = Object.assign({}, defaultOptions, componentOptions); const finalOptions = Object.assign({}, defaultOptions, componentOptions);
const { const { supCodeScope, initScopeValue = {}, dataSourceCreator } = finalOptions;
supCodeScope,
initScopeValue = {},
dataSourceCreator,
} = finalOptions;
const codeRuntimeScope = const codeRuntimeScope =
supCodeScope?.createSubScope(initScopeValue) ?? supCodeScope?.createSubScope(initScopeValue) ?? createScope(initScopeValue);
createScope(initScopeValue);
const codeRuntime = createCodeRuntime(codeRuntimeScope); const codeRuntime = createCodeRuntime(codeRuntimeScope);
const container: Container = { const container: Container = {
@ -116,9 +39,7 @@ export function createComponentFunction<
const mapRefToComponentInstance: Map<string, C> = new Map(); const mapRefToComponentInstance: Map<string, C> = new Map();
const initialState = codeRuntime.parseExprOrFn( const initialState = codeRuntime.parseExprOrFn(componentsTree.state ?? {});
componentsTree.state ?? {}
);
const stateContext = stateCreator(initialState); const stateContext = stateCreator(initialState);
codeRuntimeScope.setValue( codeRuntimeScope.setValue(
@ -135,13 +56,10 @@ export function createComponentFunction<
}, },
stateContext, stateContext,
dataSourceCreator dataSourceCreator
? dataSourceCreator( ? dataSourceCreator(componentsTree.dataSource ?? ({ list: [] } as any), stateContext)
componentsTree.dataSource ?? ({ list: [] } as any), : {},
stateContext
)
: {}
) as ContainerInstanceScope<C>, ) as ContainerInstanceScope<C>,
true true,
); );
if (componentsTree.methods) { if (componentsTree.methods) {
@ -155,10 +73,7 @@ export function createComponentFunction<
triggerLifeCycle('constructor'); triggerLifeCycle('constructor');
function triggerLifeCycle( function triggerLifeCycle(lifeCycleName: LifeCycleNameT, ...args: any[]) {
lifeCycleName: LifeCycleName,
...args: any[]
) {
// keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象 // keys 用来判断 lifeCycleName 存在于 schema 对象上,不获取原型链上的对象
if ( if (
!componentsTree.lifeCycles || !componentsTree.lifeCycles ||
@ -169,9 +84,7 @@ export function createComponentFunction<
const lifeCycleSchema = componentsTree.lifeCycles[lifeCycleName]; const lifeCycleSchema = componentsTree.lifeCycles[lifeCycleName];
if (isJsFunction(lifeCycleSchema)) { if (isJsFunction(lifeCycleSchema)) {
const lifeCycleFn = codeRuntime.createFnBoundScope( const lifeCycleFn = codeRuntime.createFnBoundScope(lifeCycleSchema.value);
lifeCycleSchema.value
);
if (lifeCycleFn) { if (lifeCycleFn) {
lifeCycleFn.apply(codeRuntime.getScope().value, args); lifeCycleFn.apply(codeRuntime.getScope().value, args);
} }
@ -202,8 +115,8 @@ export function createComponentFunction<
? componentsTree.children ? componentsTree.children
: [componentsTree.children] : [componentsTree.children]
: []; : [];
const treeNodes = childNodes.map(item => { const treeNodes = childNodes.map((item) => {
return createNode(item, undefined); return createComponentTreeNode(item, undefined);
}); });
return treeNodes; return treeNodes;

View File

@ -1,8 +1,11 @@
import { type AnyFunction } from '@alilc/runtime-shared'; import { type AnyFunction } from './types';
import { createHooks, type Hooks } from '../helper/hook'; import { createHookStore, type HookStore } from './utils/hook';
import { type RuntimeError } from './error'; 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 { export interface RuntimeHooks {
'app:error': (error: RuntimeError) => void; 'app:error': (error: RuntimeError) => void;
@ -11,22 +14,29 @@ export interface RuntimeHooks {
} }
export interface AppBoostsManager { export interface AppBoostsManager {
hooks: Hooks<RuntimeHooks>; hookStore: HookStore<RuntimeHooks>;
readonly value: AppBoosts; readonly value: AppBoosts;
add(name: PropertyKey, value: any, force?: boolean): void; add(name: PropertyKey, value: any, force?: boolean): void;
remove(name: PropertyKey): void;
} }
const boostsValue: AppBoosts = {}; const boostsValue: AppBoosts = {};
const proxyBoostsValue = nonSetterProxy(boostsValue);
export const appBoosts: AppBoostsManager = { export const appBoosts: AppBoostsManager = {
hooks: createHooks(), hookStore: createHookStore(),
get value() { get value() {
return boostsValue; return proxyBoostsValue;
}, },
add(name: PropertyKey, value: any, force = false) { add(name: PropertyKey, value: any, force = false) {
if ((boostsValue as any)[name] && !force) return; if ((boostsValue as any)[name] && !force) return;
(boostsValue as any)[name] = value; (boostsValue as any)[name] = value;
}, },
remove(name) {
if ((boostsValue as any)[name]) {
delete (boostsValue as any)[name];
}
},
}; };

View File

@ -1,24 +1,22 @@
import { import type { AnyFunction, PlainObject, JSExpression, JSFunction } from './types';
type AnyFunction, import { isJSExpression, isJSFunction } from './utils/type-guard';
type AnyObject, import { processValue } from './utils/value';
JSExpression,
JSFunction,
isJsExpression,
isJsFunction,
} from '@alilc/runtime-shared';
import { processValue } from '../utils/value';
export interface CodeRuntime { export interface CodeRuntime {
run<T = unknown>(code: string): T | undefined; run<T = unknown>(code: string): T | undefined;
createFnBoundScope(code: string): AnyFunction | undefined; createFnBoundScope(code: string): AnyFunction | undefined;
parseExprOrFn(value: AnyObject): any; parseExprOrFn(value: PlainObject): any;
bindingScope(scope: CodeScope): void; bindingScope(scope: CodeScope): void;
getScope(): CodeScope; getScope(): CodeScope;
} }
export function createCodeRuntime(scope?: CodeScope): CodeRuntime { const SYMBOL_SIGN = '__code__scope';
let runtimeScope = scope ?? createScope({});
export function createCodeRuntime(scopeOrValue: PlainObject = {}): CodeRuntime {
let runtimeScope = scopeOrValue[Symbol.for(SYMBOL_SIGN)]
? (scopeOrValue as CodeScope)
: createScope(scopeOrValue);
function run<T = unknown>(code: string): T | undefined { function run<T = unknown>(code: string): T | undefined {
if (!code) return undefined; if (!code) return undefined;
@ -26,16 +24,11 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime {
try { try {
return new Function( return new Function(
'scope', 'scope',
`"use strict";return (function(){return (${code})}).bind(scope)();` `"use strict";return (function(){return (${code})}).bind(scope)();`,
)(runtimeScope.value) as T; )(runtimeScope.value) as T;
} catch (err) { } catch (err) {
console.log( // todo
'%c eval error', console.error('%c eval error', code, runtimeScope.value, err);
'font-size:13px; background:pink; color:#bf2c9f;',
code,
scope.value,
err
);
return undefined; return undefined;
} }
} }
@ -46,11 +39,11 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime {
return fn.bind(runtimeScope.value); return fn.bind(runtimeScope.value);
} }
function parseExprOrFn(value: AnyObject) { function parseExprOrFn(value: PlainObject) {
return processValue( return processValue(
value, value,
data => { (data) => {
return isJsExpression(data) || isJsFunction(data); return isJSExpression(data) || isJSFunction(data);
}, },
(node: JSExpression | JSFunction) => { (node: JSExpression | JSFunction) => {
let v; let v;
@ -65,7 +58,7 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime {
return (node as any).mock; return (node as any).mock;
} }
return v; return v;
} },
); );
} }
@ -84,14 +77,14 @@ export function createCodeRuntime(scope?: CodeScope): CodeRuntime {
} }
export interface CodeScope { export interface CodeScope {
readonly value: AnyObject; readonly value: PlainObject;
inject(name: string, value: any, force?: boolean): void; inject(name: string, value: any, force?: boolean): void;
setValue(value: AnyObject, replace?: boolean): void; setValue(value: PlainObject, replace?: boolean): void;
createSubScope(initValue: AnyObject): CodeScope; createSubScope(initValue?: PlainObject): CodeScope;
} }
export function createScope(initValue: AnyObject): CodeScope { export function createScope(initValue: PlainObject = {}): CodeScope {
const innerScope = { value: initValue }; const innerScope = { value: initValue };
const proxyValue = new Proxy(Object.create(null), { const proxyValue = new Proxy(Object.create(null), {
set(target, p, newValue, receiver) { set(target, p, newValue, receiver) {
@ -120,7 +113,7 @@ export function createScope(initValue: AnyObject): CodeScope {
innerScope.value[name] = value; innerScope.value[name] = value;
} }
function createSubScope(initValue: AnyObject) { function createSubScope(initValue: PlainObject = {}) {
const childScope = createScope(initValue); const childScope = createScope(initValue);
(childScope as any).__raw.__parent = innerScope; (childScope as any).__raw.__parent = innerScope;
@ -144,6 +137,8 @@ export function createScope(initValue: AnyObject): CodeScope {
createSubScope, createSubScope,
}; };
Object.defineProperty(scope, Symbol.for(SYMBOL_SIGN), { get: () => true });
// development env
Object.defineProperty(scope, '__raw', { get: () => innerScope }); Object.defineProperty(scope, '__raw', { get: () => innerScope });
return scope; return scope;

View File

@ -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<InstanceT = unknown, LifeCycleNameT extends string = string> {
readonly codeRuntime: CodeRuntime;
readonly instanceApiObject: InstanceApi<InstanceT>;
/**
* 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<Element>(): (TextWidget<Element> | ComponentWidget<Element>)[];
}
export interface CreateContainerOptions<LifeCycleNameT extends string> {
supCodeScope?: CodeScope;
initScopeValue?: PlainObject;
componentsTree: ComponentTree<LifeCycleNameT>;
stateCreator: (initalState: PlainObject) => InstanceStateApi;
// type todo
dataSourceCreator: (...args: any[]) => InstanceDataSourceApi;
}
export function createContainer<InstanceT, LifeCycleNameT extends string>(
options: CreateContainerOptions<LifeCycleNameT>,
): Container<InstanceT, LifeCycleNameT> {
const { componentsTree, supCodeScope, initScopeValue, stateCreator, dataSourceCreator } = options;
validContainerSchema(componentsTree);
const instancesMap = new Map<string, InstanceT[]>();
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<InstanceT> = 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<InstanceT>;
},
getCssText() {
return componentsTree.css;
},
triggerLifeCycle,
setInstance: setInstanceByRef,
removeInstance: removeInstanceByRef,
createWidgets<Element>() {
if (!componentsTree.children) return [];
return componentsTree.children.map((item) => createWidget<Element>(item));
},
};
}
const CONTAINTER_NAME = ['Page', 'Block', 'Component'];
function validContainerSchema(schema: ComponentTree) {
if (!CONTAINTER_NAME.includes(schema.componentName)) {
throw Error('container schema not valid');
}
}

View File

@ -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';

View File

@ -1,13 +1,6 @@
import { import { type Package, type ComponentMap, type LowCodeComponent } from './types';
type Package,
type ProCodeComponent,
type ComponentMap,
type LowCodeComponent,
isLowCodeComponentPackage,
} from '@alilc/runtime-shared';
const packageStore: Map<string, any> = ((window as any).__PACKAGE_STORE__ ??= const packageStore: Map<string, any> = ((window as any).__PACKAGE_STORE__ ??= new Map());
new Map());
export interface PackageLoader { export interface PackageLoader {
name?: string; name?: string;
@ -31,9 +24,7 @@ export interface PackageManager {
/** 解析组件映射 */ /** 解析组件映射 */
resolveComponentMaps(componentMaps: ComponentMap[]): void; resolveComponentMaps(componentMaps: ComponentMap[]): void;
/** 获取组件映射对象key = componentName value = component */ /** 获取组件映射对象key = componentName value = component */
getComponentsNameRecord<C = unknown>( getComponentsNameRecord<C = unknown>(componentMaps?: ComponentMap[]): Record<string, C>;
componentMaps?: ComponentMap[]
): Record<string, C>;
/** 通过组件名获取对应的组件 */ /** 通过组件名获取对应的组件 */
getComponent<C = unknown>(componentName: string): C | undefined; getComponent<C = unknown>(componentName: string): C | undefined;
/** 注册组件 */ /** 注册组件 */
@ -48,8 +39,10 @@ export function createPackageManager(): PackageManager {
async function addPackages(packages: Package[]) { async function addPackages(packages: Package[]) {
for (const item of packages) { for (const item of packages) {
const newId = item.package ?? item.id; if (!item.package && !item.id) continue;
const isExist = packagesRef.some(_ => {
const newId = item.package ?? item.id!;
const isExist = packagesRef.some((_) => {
const itemId = _.package ?? _.id; const itemId = _.package ?? _.id;
return itemId === newId; return itemId === newId;
}); });
@ -58,7 +51,7 @@ export function createPackageManager(): PackageManager {
packagesRef.push(item); packagesRef.push(item);
if (!packageStore.has(newId)) { if (!packageStore.has(newId)) {
const loader = packageLoaders.find(loader => loader.active(item)); const loader = packageLoaders.find((loader) => loader.active(item));
if (!loader) continue; if (!loader) continue;
try { try {
@ -73,14 +66,14 @@ export function createPackageManager(): PackageManager {
} }
function getPackageInfo(packageName: string) { function getPackageInfo(packageName: string) {
return packagesRef.find(p => p.package === packageName); return packagesRef.find((p) => p.package === packageName);
} }
function getLibraryByPackageName(packageName: string) { function getLibraryByPackageName(packageName: string) {
const packageInfo = getPackageInfo(packageName); const packageInfo = getPackageInfo(packageName);
if (packageInfo) { 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[]) { function resolveComponentMaps(componentMaps: ComponentMap[]) {
for (const map of componentMaps) { for (const map of componentMaps) {
if ((map as LowCodeComponent).devMode === 'lowCode') { if (map.devMode === 'lowCode') {
const packageInfo = packagesRef.find(_ => { const packageInfo = packagesRef.find((_) => {
return _.id === (map as LowCodeComponent).id; return _.id === (map as LowCodeComponent).id;
}); });
if (isLowCodeComponentPackage(packageInfo)) { if (packageInfo) {
componentsRecord[map.componentName] = packageInfo; componentsRecord[map.componentName] = packageInfo;
} }
} else { } else {
const npmInfo = map as ProCodeComponent; if (packageStore.has(map.package!)) {
const library = packageStore.get(map.package!);
if (packageStore.has(npmInfo.package)) {
const library = packageStore.get(npmInfo.package);
// export { exportName } from xxx exportName === global.libraryName.exportName // export { exportName } from xxx exportName === global.libraryName.exportName
// export exportName from xxx exportName === global.libraryName.default || global.libraryName // export exportName from xxx exportName === global.libraryName.default || global.libraryName
// export { exportName as componentName } from package // export { exportName as componentName } from package
// if exportName == null exportName === componentName; // if exportName == null exportName === componentName;
// const componentName = exportName.subName, if exportName empty subName donot use // const componentName = exportName.subName, if exportName empty subName donot use
const paths = const paths = map.exportName && map.subName ? map.subName.split('.') : [];
npmInfo.exportName && npmInfo.subName const exportName = map.exportName ?? map.componentName;
? npmInfo.subName.split('.')
: [];
const exportName = npmInfo.exportName ?? npmInfo.componentName;
if (npmInfo.destructuring) { if (map.destructuring) {
paths.unshift(exportName); paths.unshift(exportName);
} }
@ -123,7 +111,7 @@ export function createPackageManager(): PackageManager {
result = result[path] || result; result = result[path] || result;
} }
const recordName = npmInfo.componentName ?? npmInfo.exportName; const recordName = map.componentName ?? map.exportName;
componentsRecord[recordName] = result; componentsRecord[recordName] = result;
} }
} }
@ -152,7 +140,7 @@ export function createPackageManager(): PackageManager {
getLibraryByPackageName, getLibraryByPackageName,
setLibraryByPackageName, setLibraryByPackageName,
addPackageLoader(loader) { addPackageLoader(loader) {
if (!loader.name || !packageLoaders.some(_ => _.name === loader.name)) { if (!loader.name || !packageLoaders.some((_) => _.name === loader.name)) {
packageLoaders.push(loader); packageLoaders.push(loader);
} }
}, },

View File

@ -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<C extends PluginSetupContext = PluginSetupContext> { export interface Plugin<C extends PluginSetupContext = PluginSetupContext> {
name: string; // 插件的 name 作为唯一标识,并不可重复。 name: string; // 插件的 name 作为唯一标识,并不可重复。
@ -14,24 +15,12 @@ export function createPluginManager(context: PluginSetupContext) {
const installedPlugins: Plugin[] = []; const installedPlugins: Plugin[] = [];
let readyToInstallPlugins: Plugin[] = []; let readyToInstallPlugins: Plugin[] = [];
const setupContext = new Proxy(context, { const setupContext = nonSetterProxy(context);
get(target, p, receiver) {
return Reflect.get(target, p, receiver);
},
set() {
return false;
},
has(target, p) {
return Reflect.has(target, p);
},
});
async function install(plugin: Plugin) { async function install(plugin: Plugin) {
if (installedPlugins.some(p => p.name === plugin.name)) return; if (installedPlugins.some((p) => p.name === plugin.name)) return;
if ( if (plugin.dependsOn?.some((dep) => !installedPlugins.some((p) => p.name === dep))) {
plugin.dependsOn?.some(dep => !installedPlugins.some(p => p.name === dep))
) {
readyToInstallPlugins.push(plugin); readyToInstallPlugins.push(plugin);
return; return;
} }
@ -41,24 +30,22 @@ export function createPluginManager(context: PluginSetupContext) {
// 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装 // 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装
for (const item of readyToInstallPlugins) { for (const item of readyToInstallPlugins) {
if ( if (item.dependsOn?.every((dep) => installedPlugins.some((p) => p.name === dep))) {
item.dependsOn?.every(dep => installedPlugins.some(p => p.name === dep))
) {
await item.setup(setupContext); await item.setup(setupContext);
installedPlugins.push(item); installedPlugins.push(item);
} }
} }
if (readyToInstallPlugins.length) { if (readyToInstallPlugins.length) {
readyToInstallPlugins = readyToInstallPlugins.filter(item => readyToInstallPlugins = readyToInstallPlugins.filter((item) =>
installedPlugins.some(p => p.name === item.name) installedPlugins.some((p) => p.name === item.name),
); );
} }
} }
return { return {
async add(plugin: Plugin) { async add(plugin: Plugin) {
if (installedPlugins.find(item => item.name === plugin.name)) { if (installedPlugins.find((item) => item.name === plugin.name)) {
console.warn('该插件已安装'); console.warn('该插件已安装');
return; return;
} }
@ -68,8 +55,6 @@ export function createPluginManager(context: PluginSetupContext) {
}; };
} }
export function definePlugin<C extends PluginSetupContext, P = Plugin<C>>( export function definePlugin<C extends PluginSetupContext, P = Plugin<C>>(plugin: P) {
plugin: P
) {
return plugin; return plugin;
} }

View File

@ -1,43 +1,33 @@
import type { import type { Project, ComponentTree, ComponentMap, PageConfig } from './types';
ProjectSchema, import { throwRuntimeError } from './utils/error';
RootSchema,
ComponentMap,
PageSchema,
} from '@alilc/runtime-shared';
import { throwRuntimeError } from './error';
import { set, get } from 'lodash-es'; import { set, get } from 'lodash-es';
type AppSchemaType = ProjectSchema<RootSchema>;
export interface AppSchema { export interface AppSchema {
getComponentsTrees(): RootSchema[]; getComponentsTrees(): ComponentTree[];
addComponentsTree(tree: RootSchema): void; addComponentsTree(tree: ComponentTree): void;
removeComponentsTree(id: string): void; removeComponentsTree(id: string): void;
getComponentsMaps(): ComponentMap[]; getComponentsMaps(): ComponentMap[];
addComponentsMap(componentName: ComponentMap): void; addComponentsMap(componentName: ComponentMap): void;
removeComponentsMap(componentName: string): void; removeComponentsMap(componentName: string): void;
getPages(): PageSchema[]; getPages(): PageConfig[];
addPage(page: PageSchema): void; addPage(page: PageConfig): void;
removePage(id: string): void; removePage(id: string): void;
getByKey<K extends keyof AppSchemaType>(key: K): AppSchemaType[K] | undefined; getByKey<K extends keyof Project>(key: K): Project[K] | undefined;
updateByKey<K extends keyof AppSchemaType>( updateByKey<K extends keyof Project>(
key: K, key: K,
updater: AppSchemaType[K] | ((value: AppSchemaType[K]) => AppSchemaType[K]) updater: Project[K] | ((value: Project[K]) => Project[K]),
): void; ): void;
getByPath(path: string | string[]): any; getByPath(path: string | string[]): any;
updateByPath( updateByPath(path: string | string[], updater: any | ((value: any) => any)): void;
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.')) { if (!schema.version.startsWith('1.')) {
throwRuntimeError('core', 'schema version must be 1.x.x'); throwRuntimeError('core', 'schema version must be 1.x.x');
} }
@ -93,21 +83,13 @@ export function createAppSchema(schema: ProjectSchema): AppSchema {
return get(schemaRef, path); return get(schemaRef, path);
}, },
updateByPath(path, updater) { updateByPath(path, updater) {
set( set(schemaRef, path, typeof updater === 'function' ? updater(this.getByPath(path)) : updater);
schemaRef,
path,
typeof updater === 'function' ? updater(this.getByPath(path)) : updater
);
}, },
}; };
} }
function addArrayItem<T extends Record<string, any>>( function addArrayItem<T extends Record<string, any>>(target: T[], item: T, comparison: string) {
target: T[], const idx = target.findIndex((_) => _[comparison] === item[comparison]);
item: T,
comparison: string
) {
const idx = target.findIndex(_ => _[comparison] === item[comparison]);
if (idx > -1) { if (idx > -1) {
target.splice(idx, 1, item); target.splice(idx, 1, item);
} else { } else {
@ -118,8 +100,8 @@ function addArrayItem<T extends Record<string, any>>(
function removeArrayItem<T extends Record<string, any>>( function removeArrayItem<T extends Record<string, any>>(
target: T[], target: T[],
comparison: string, 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); if (idx > -1) target.splice(idx, 1);
} }

View File

@ -0,0 +1,3 @@
export type AnyFunction = (...args: any[]) => any;
export type PlainObject = Record<PropertyKey, any>;

View File

@ -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';

View File

@ -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;
}

View File

@ -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';

View File

@ -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<string, JSONValue>;
/**
*
*/
meta?: Record<string, JSONValue>;
/**
*
* @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<LifeCycleNameT extends string = string> =
ComponentTreeContainer<LifeCycleNameT>;
/**
* (A)
*
*/
export interface ComponentTreeContainer<LifeCycleNameT extends string>
extends Omit<ComponentTreeNode, 'loop' | 'loopArgs' | 'condition'> {
componentName: 'Page' | 'Block' | 'Component';
/**
*
*/
fileName: string;
/**
*
*/
state?: Record<string, JSONValue | JSExpression>;
/**
*
*/
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<string, string>;
}
/**
* 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;
/**
* IDpage redirect
*/
page?: string;
/**
* page redirect
*/
redirect?: string | object | JSFunction;
/**
*
*/
children?: RouteRecord[];
}
/**
* AA
*
*
*/
export interface PageConfig {
/**
* id
*/
id: string;
/**
* componentsTreepackage 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<string, string | number | JSExpression>;
}
export type NodeType = string | JSExpression | I18nNode | ComponentTreeNode;

View File

@ -0,0 +1,75 @@
import { AnyFunction, PlainObject } from '../common';
/**
* JS this
* this API
*
*/
export interface InstanceApi<InstanceT = unknown> 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<S = PlainObject> {
/**
* state
*/
state: Readonly<S>;
/**
*
* like React.Component.setState
*/
setState<K extends keyof S>(
newState: ((prevState: Readonly<S>) => Pick<S, K> | S | null) | (Pick<S, K> | S | null),
callback?: () => void,
): void;
}
export interface InstanceDataSourceApi {
/**
* Map
*/
dataSourceMap: any;
/**
*
*/
reloadDataSource: () => void;
}
export interface UtilsApi {
utils: Record<string, AnyFunction>;
}
export interface IntlApi {
/**
*
* @param i18nKey
* @param params
*/
i18n(i18nKey: string, params?: Record<string, string>): string;
/**
*
*/
getLocale(): string;
/**
*
* @param locale
*/
setLocale(locale: string): void;
}
export interface RouterApi {}

View File

@ -1,11 +1,14 @@
import { appBoosts } from './boosts'; import { appBoosts } from '../boosts';
export type ErrorType = string; export type ErrorType = string;
export class RuntimeError extends Error { export class RuntimeError extends Error {
constructor(public type: ErrorType, message: string) { constructor(
public type: ErrorType,
message: string,
) {
super(message); super(message);
appBoosts.hooks.call(`app:error`, this); appBoosts.hookStore.call(`app:error`, this);
} }
} }

View File

@ -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<T = AnyFunction>() {
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<F = AnyFunction> = ReturnType<typeof useEvent<F>>;
export type HookCallback = (...args: any) => Promise<any> | any;
export type HookCallback = (...args: any) => Promise<void> | void;
type HookKeys<T> = keyof T & PropertyKey; type HookKeys<T> = keyof T & PropertyKey;
type InferCallback<HT, HN extends keyof HT> = HT[HN] extends HookCallback type InferCallback<HT, HN extends keyof HT> = HT[HN] extends HookCallback ? HT[HN] : never;
? HT[HN]
: never;
declare global { declare global {
interface Console { interface Console {
@ -18,19 +50,16 @@ declare global {
// https://developer.chrome.com/blog/devtools-modern-web-debugging/#linked-stack-traces // https://developer.chrome.com/blog/devtools-modern-web-debugging/#linked-stack-traces
type CreateTask = typeof console.createTask; type CreateTask = typeof console.createTask;
const defaultTask: ReturnType<CreateTask> = { run: fn => fn() }; const defaultTask: ReturnType<CreateTask> = { run: (fn) => fn() };
const _createTask: CreateTask = () => defaultTask; const _createTask: CreateTask = () => defaultTask;
const createTask = const createTask = typeof console.createTask !== 'undefined' ? console.createTask : _createTask;
typeof console.createTask !== 'undefined' ? console.createTask : _createTask;
export interface Hooks< export interface HookStore<
HooksT extends Record<PropertyKey, any> = Record<PropertyKey, HookCallback>, HooksT extends Record<PropertyKey, any> = Record<PropertyKey, HookCallback>,
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT> HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
> { > {
hook<NameT extends HookNameT>( hook<NameT extends HookNameT>(name: NameT, fn: InferCallback<HooksT, NameT>): () => void;
name: NameT,
fn: InferCallback<HooksT, NameT>
): () => void;
call<NameT extends HookNameT>( call<NameT extends HookNameT>(
name: NameT, name: NameT,
...args: Parameters<InferCallback<HooksT, NameT>> ...args: Parameters<InferCallback<HooksT, NameT>>
@ -43,29 +72,28 @@ export interface Hooks<
name: NameT, name: NameT,
...args: Parameters<InferCallback<HooksT, NameT>> ...args: Parameters<InferCallback<HooksT, NameT>>
): Promise<void[]>; ): Promise<void[]>;
remove<NameT extends HookNameT>(
name: NameT, remove<NameT extends HookNameT>(name: NameT, fn?: InferCallback<HooksT, NameT>): void;
fn?: InferCallback<HooksT, NameT>
): void; clear<NameT extends HookNameT>(name?: NameT): void;
getHooks<NameT extends HookNameT>(name: NameT): InferCallback<HooksT, NameT>[] | undefined;
} }
export function createHooks< export function createHookStore<
HooksT extends Record<PropertyKey, any> = Record<PropertyKey, HookCallback>, HooksT extends Record<PropertyKey, any> = Record<PropertyKey, HookCallback>,
HookNameT extends HookKeys<HooksT> = HookKeys<HooksT> HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>,
>(): Hooks<HooksT, HookNameT> { >(): HookStore<HooksT, HookNameT> {
const hooksMap = new Map<HookNameT, Callback<HookCallback>>(); const hooksMap = new Map<HookNameT, Event<HookCallback>>();
function hook<NameT extends HookNameT>( function hook<NameT extends HookNameT>(name: NameT, fn: InferCallback<HooksT, NameT>) {
name: NameT,
fn: InferCallback<HooksT, NameT>
) {
if (!name || typeof fn !== 'function') { if (!name || typeof fn !== 'function') {
return () => {}; return () => {};
} }
let hooks = hooksMap.get(name); let hooks = hooksMap.get(name);
if (!hooks) { if (!hooks) {
hooks = useCallbacks(); hooks = useEvent();
hooksMap.set(name, hooks); hooksMap.set(name, hooks);
} }
@ -92,9 +120,8 @@ export function createHooks<
const task = createTask(name.toString()); const task = createTask(name.toString());
return hooks.reduce( return hooks.reduce(
(promise, hookFunction) => (promise, hookFunction) => promise.then(() => task.run(() => hookFunction(...args))),
promise.then(() => task.run(() => hookFunction(...args))), Promise.resolve(),
Promise.resolve()
); );
} }
@ -104,19 +131,16 @@ export function createHooks<
) { ) {
const hooks = hooksMap.get(name)?.list() ?? []; const hooks = hooksMap.get(name)?.list() ?? [];
const task = createTask(name.toString()); 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<NameT extends HookNameT>( function remove<NameT extends HookNameT>(name: NameT, fn?: InferCallback<HooksT, NameT>) {
name: NameT,
fn?: InferCallback<HooksT, NameT>
) {
const hooks = hooksMap.get(name); const hooks = hooksMap.get(name);
if (!hooks) return; if (!hooks) return;
if (fn) { if (fn) {
hooks.remove(fn); hooks.remove(fn);
if (hooks.list.length === 0) { if (hooks.list().length === 0) {
hooksMap.delete(name); hooksMap.delete(name);
} }
} else { } else {
@ -124,11 +148,30 @@ export function createHooks<
} }
} }
function clear<NameT extends HookNameT>(name?: NameT) {
if (name) {
remove(name);
} else {
hooksMap.clear();
}
}
function getHooks<NameT extends HookNameT>(
name: NameT,
): InferCallback<HooksT, NameT>[] | undefined {
return hooksMap.get(name)?.list() as InferCallback<HooksT, NameT>[] | undefined;
}
return { return {
hook, hook,
call, call,
callAsync, callAsync,
callParallel, callParallel,
remove, remove,
clear,
getHooks,
}; };
} }

View File

@ -0,0 +1,13 @@
export function nonSetterProxy<T extends object>(target: T) {
return new Proxy<T>(target, {
get(target, p, receiver) {
return Reflect.get(target, p, receiver);
},
set() {
return false;
},
has(target, p) {
return Reflect.has(target, p);
},
});
}

View File

@ -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';
}

View File

@ -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;
}

View File

@ -0,0 +1,92 @@
import type { NodeType, ComponentTreeNode, ComponentTreeNodeProps } from './types';
import { isJSExpression, isI18nNode } from './utils/type-guard';
export class Widget<Data, Element> {
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<NodeType, ComponentTreeNode>;
export type TextWidgetType = 'string' | 'expression' | 'i18n';
export class TextWidget<E = unknown> extends Widget<TextWidgetData, E> {
type: TextWidgetType = 'string';
protected init() {
if (isJSExpression(this.raw)) {
this.type = 'expression';
} else if (isI18nNode(this.raw)) {
this.type = 'i18n';
}
}
}
export class ComponentWidget<E = unknown> extends Widget<ComponentTreeNode, E> {
private _children: (TextWidget<E> | ComponentWidget<E>)[] = [];
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<E>(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<E = unknown>(data: NodeType) {
if (typeof data === 'string' || isJSExpression(data) || isI18nNode(data)) {
return new TextWidget<E>(data);
} else if (data.componentName) {
return new ComponentWidget<E>(data);
}
throw Error(`unknown node data: ${JSON.stringify(data)}`);
}

View File

@ -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();
});
});

View File

@ -0,0 +1,5 @@
import { describe, it, expect } from 'vitest';
describe('createComponentFunction', () => {
it('', () => {});
});

View File

@ -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();
});
});

View File

@ -0,0 +1 @@
import {} from 'vitest';

View File

@ -0,0 +1,2 @@
import { expect } from 'vitest';
import { createPackageManager } from '../src/package';

View File

@ -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', () => {});
});

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['tests/**/*.spec.ts']
}
})

View File

@ -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"
}
}

View File

@ -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<any>;
}
export interface ReactRender extends RenderBase {}
export type ReactApp = App<ReactRender>;
export const createApp = createAppFunction<AppOptions, ReactRender>(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(<AppComponent context={appContext} />);
},
unmount() {
if (root) {
root.unmount();
root = undefined;
}
},
};
return {
renderBase: reactRender,
renderer,
};
});

View File

@ -11,12 +11,12 @@ import {
type CodeRuntime, type CodeRuntime,
createCodeRuntime, createCodeRuntime,
} from '@alilc/runtime-core'; } from '@alilc/runtime-core';
import { isPlainObject } from 'lodash-es';
import { import {
type AnyObject, type AnyObject,
type Package, type Package,
type JSSlot, type JSSlot,
type JSFunction, type JSFunction,
isPlainObject,
isJsExpression, isJsExpression,
isJsSlot, isJsSlot,
isLowCodeComponentPackage, isLowCodeComponentPackage,
@ -79,8 +79,7 @@ export interface ConvertedTreeNode {
setReactNode(element: ReactNode): void; setReactNode(element: ReactNode): void;
} }
export interface CreateComponentOptions<C = ComponentType<any>> export interface CreateComponentOptions<C = ComponentType<any>> extends ComponentOptionsBase<C> {
extends ComponentOptionsBase<C> {
displayName?: string; displayName?: string;
beforeNodeCreateComponent?(convertedNode: ConvertedTreeNode): void; beforeNodeCreateComponent?(convertedNode: ConvertedTreeNode): void;
@ -124,7 +123,7 @@ export const createComponent = createComponentFunction<
function getComponentByName( function getComponentByName(
componentName: string, componentName: string,
componentsRecord: Record<string, ComponentType<any> | Package> componentsRecord: Record<string, ComponentType<any> | Package>,
) { ) {
const Component = componentsRecord[componentName]; const Component = componentsRecord[componentName];
if (!Component) { if (!Component) {
@ -154,7 +153,7 @@ export const createComponent = createComponentFunction<
function createConvertedTreeNode( function createConvertedTreeNode(
rawNode: ComponentTreeNode, rawNode: ComponentTreeNode,
codeRuntime: CodeRuntime codeRuntime: CodeRuntime,
): ConvertedTreeNode { ): ConvertedTreeNode {
let elementValue: ReactNode = null; let elementValue: ReactNode = null;
@ -172,10 +171,7 @@ export const createComponent = createComponentFunction<
}; };
if (rawNode.type === 'component') { if (rawNode.type === 'component') {
node.rawComponent = getComponentByName( node.rawComponent = getComponentByName(rawNode.data.componentName, componentsRecord);
rawNode.data.componentName,
componentsRecord
);
} }
return node; return node;
@ -185,7 +181,7 @@ export const createComponent = createComponentFunction<
node: ComponentTreeNode, node: ComponentTreeNode,
codeRuntime: CodeRuntime, codeRuntime: CodeRuntime,
instance: ContainerInstance, instance: ContainerInstance,
componentsRecord: Record<string, ComponentType<any> | Package> componentsRecord: Record<string, ComponentType<any> | Package>,
) { ) {
const convertedNode = createConvertedTreeNode(node, codeRuntime); const convertedNode = createConvertedTreeNode(node, codeRuntime);
@ -206,7 +202,7 @@ export const createComponent = createComponentFunction<
target: { target: {
text: rawValue, text: rawValue,
}, },
valueGetter: node => codeRuntime.parseExprOrFn(node), valueGetter: (node) => codeRuntime.parseExprOrFn(node),
}); });
convertedNode.setReactNode(<ReactivedText key={rawValue.value} />); convertedNode.setReactNode(<ReactivedText key={rawValue.value} />);
@ -235,7 +231,7 @@ export const createComponent = createComponentFunction<
props: AnyObject, props: AnyObject,
codeRuntime: CodeRuntime, codeRuntime: CodeRuntime,
key: string, key: string,
children: ReactNode[] = [] children: ReactNode[] = [],
) { ) {
const { ref, ...componentProps } = props; const { ref, ...componentProps } = props;
@ -249,45 +245,29 @@ export const createComponent = createComponentFunction<
// 先将 jsslot, jsFunction 对象转换 // 先将 jsslot, jsFunction 对象转换
const finalProps = processValue( const finalProps = processValue(
componentProps, componentProps,
node => isJsSlot(node) || isJsFunction(node), (node) => isJsSlot(node) || isJsFunction(node),
(node: JSSlot | JSFunction) => { (node: JSSlot | JSFunction) => {
if (isJsSlot(node)) { if (isJsSlot(node)) {
if (node.value) { if (node.value) {
const nodes = ( const nodes = (Array.isArray(node.value) ? node.value : [node.value]).map(
Array.isArray(node.value) ? node.value : [node.value] (n) => createNode(n, undefined),
).map(n => createNode(n, undefined)); );
if (node.params?.length) { if (node.params?.length) {
return (...args: any[]) => { return (...args: any[]) => {
const params = node.params!.reduce( const params = node.params!.reduce((prev, cur, idx) => {
(prev, cur, idx) => {
return (prev[cur] = args[idx]); return (prev[cur] = args[idx]);
}, }, {} as AnyObject);
{} as AnyObject const subCodeScope = codeRuntime.getScope().createSubScope(params);
); const subCodeRuntime = createCodeRuntime(subCodeScope);
const subCodeScope = codeRuntime
.getScope()
.createSubScope(params);
const subCodeRuntime =
createCodeRuntime(subCodeScope);
return nodes.map(n => return nodes.map((n) =>
createReactElement( createReactElement(n, subCodeRuntime, instance, componentsRecord),
n,
subCodeRuntime,
instance,
componentsRecord
)
); );
}; };
} else { } else {
return nodes.map(n => return nodes.map((n) =>
createReactElement( createReactElement(n, codeRuntime, instance, componentsRecord),
n,
codeRuntime,
instance,
componentsRecord
)
); );
} }
} }
@ -296,7 +276,7 @@ export const createComponent = createComponentFunction<
} }
return null; return null;
} },
); );
if (someValue(finalProps, isJsExpression)) { if (someValue(finalProps, isJsExpression)) {
@ -308,14 +288,14 @@ export const createComponent = createComponentFunction<
key, key,
ref: refFunction, ref: refFunction,
}, },
children children,
); );
} }
Props.displayName = 'Props'; Props.displayName = 'Props';
const Reactived = reactive(Props, { const Reactived = reactive(Props, {
target: finalProps, target: finalProps,
valueGetter: node => codeRuntime.parseExprOrFn(node), valueGetter: (node) => codeRuntime.parseExprOrFn(node),
}); });
return <Reactived key={key} />; return <Reactived key={key} />;
@ -327,7 +307,7 @@ export const createComponent = createComponentFunction<
key, key,
ref: refFunction, ref: refFunction,
}, },
children children,
); );
} }
} }
@ -339,9 +319,9 @@ export const createComponent = createComponentFunction<
nodeProps, nodeProps,
codeRuntime, codeRuntime,
currentComponentKey, currentComponentKey,
rawNode.children?.map(n => rawNode.children?.map((n) =>
createReactElement(n, codeRuntime, instance, componentsRecord) createReactElement(n, codeRuntime, instance, componentsRecord),
) ),
); );
if (loop) { if (loop) {
@ -360,14 +340,9 @@ export const createComponent = createComponentFunction<
nodeProps, nodeProps,
subCodeRuntime, subCodeRuntime,
`loop-${currentComponentKey}-${idx}`, `loop-${currentComponentKey}-${idx}`,
rawNode.children?.map(n => rawNode.children?.map((n) =>
createReactElement( createReactElement(n, subCodeRuntime, instance, componentsRecord),
n, ),
subCodeRuntime,
instance,
componentsRecord
)
)
); );
}); });
}; };
@ -386,7 +361,7 @@ export const createComponent = createComponentFunction<
target: { target: {
loop, loop,
}, },
valueGetter: expr => codeRuntime.parseExprOrFn(expr), valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
}); });
element = createElement(ReactivedLoop, { element = createElement(ReactivedLoop, {
@ -410,7 +385,7 @@ export const createComponent = createComponentFunction<
target: { target: {
condition, condition,
}, },
valueGetter: expr => codeRuntime.parseExprOrFn(expr), valueGetter: (expr) => codeRuntime.parseExprOrFn(expr),
}); });
return createElement(ReactivedCondition, { return createElement(ReactivedCondition, {
@ -434,7 +409,7 @@ export const createComponent = createComponentFunction<
const LowCodeComponent = forwardRef(function ( const LowCodeComponent = forwardRef(function (
props: LowCodeComponentProps, props: LowCodeComponentProps,
ref: ForwardedRef<any> ref: ForwardedRef<any>,
) { ) {
const { id, className, style, ...extraProps } = props; const { id, className, style, ...extraProps } = props;
const isMounted = useRef(false); const isMounted = useRef(false);
@ -451,7 +426,7 @@ export const createComponent = createComponentFunction<
scopeValue.reloadDataSource(); scopeValue.reloadDataSource();
if (instance.cssText) { if (instance.cssText) {
appendExternalStyle(instance.cssText).then(el => { appendExternalStyle(instance.cssText).then((el) => {
styleEl = el; styleEl = el;
}); });
} }
@ -484,9 +459,7 @@ export const createComponent = createComponentFunction<
<div id={id} className={className} style={style} ref={ref}> <div id={id} className={className} style={style} ref={ref}>
{instance {instance
.getComponentTreeNodes() .getComponentTreeNodes()
.map(n => .map((n) => createReactElement(n, codeRuntime, instance, componentsRecord))}
createReactElement(n, codeRuntime, instance, componentsRecord)
)}
</div> </div>
); );
}); });

View File

@ -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<AppContextObject>({} as any);
AppContext.displayName = 'RootContext';
export const useAppContext = () => useContext(AppContext);

View File

@ -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<Router>({} as any);
RouterContext.displayName = 'RouterContext';
export const useRouter = () => useContext(RouterContext);
export const RouteLocationContext = createContext<RouteLocation>({
name: undefined,
path: '/',
query: {},
params: {},
hash: '',
fullPath: '/',
redirectedFrom: undefined,
matched: [],
meta: {},
});
RouteLocationContext.displayName = 'RouteLocationContext';
export const useRouteLocation = () => useContext(RouteLocationContext);
export const PageSchemaContext = createContext<PageSchema | undefined>(
undefined
);
PageSchemaContext.displayName = 'PageContext';
export const usePageSchema = () => useContext(PageSchemaContext);

View File

@ -3,12 +3,12 @@ import { someValue } from '@alilc/runtime-core';
import { isJsExpression } from '@alilc/runtime-shared'; import { isJsExpression } from '@alilc/runtime-shared';
import { definePlugin } from '../../renderer'; import { definePlugin } from '../../renderer';
import { PAGE_EVENTS } from '../../events'; import { PAGE_EVENTS } from '../../events';
import { reactive } from '../../helper/reactive'; import { reactive } from '../../utils/reactive';
import { createIntl } from './intl'; import { createIntl } from './intl';
export { createIntl }; export { createIntl };
declare module '@alilc/runtime-core' { declare module '@alilc/renderer-core' {
interface AppBoosts { interface AppBoosts {
intl: ReturnType<typeof createIntl>; intl: ReturnType<typeof createIntl>;
} }
@ -24,7 +24,7 @@ export const intlPlugin = definePlugin({
appScope.setValue(intl); appScope.setValue(intl);
boosts.add('intl', 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') { if (node.type === 'i18n') {
const { key, params } = node.raw.data; const { key, params } = node.raw.data;

View File

@ -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_LIST_VALUE: RegExp = /^(?:\d)+/;
const RE_TOKEN_NAMED_VALUE: RegExp = /^(?:\w)+/; const RE_TOKEN_NAMED_VALUE: RegExp = /^(?:\w)+/;
@ -50,18 +50,11 @@ export function parse(format: string): Array<Token> {
return tokens; return tokens;
} }
export function compile( export function compile(tokens: Token[], values: Record<string, any> | any[] = {}): string[] {
tokens: Token[],
values: Record<string, any> | any[] = {}
): string[] {
const compiled: string[] = []; const compiled: string[] = [];
let index: number = 0; let index: number = 0;
const mode: string = Array.isArray(values) const mode: string = Array.isArray(values) ? 'list' : isObject(values) ? 'named' : 'unknown';
? 'list'
: isObject(values)
? 'named'
: 'unknown';
if (mode === 'unknown') { if (mode === 'unknown') {
return compiled; return compiled;
} }
@ -81,7 +74,7 @@ export function compile(
} else { } else {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
console.warn( 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!`,
); );
} }
} }

View File

@ -1,13 +1,9 @@
import { import { type Router, type RouterOptions, createRouter } from '@alilc/runtime-router';
type Router,
type RouterOptions,
createRouter,
} from '@alilc/runtime-router';
import { createRouterProvider } from './components/router-view'; import { createRouterProvider } from './components/router-view';
import RouteOutlet from './components/outlet'; import RouteOutlet from './components/outlet';
import { type ReactRendererSetupContext } from './renderer'; import { type ReactRendererSetupContext } from './renderer';
declare module '@alilc/runtime-core' { declare module '@alilc/renderer-core' {
interface AppBoosts { interface AppBoosts {
router: Router; router: Router;
} }
@ -21,9 +17,7 @@ const defaultRouterOptions: RouterOptions = {
export function initRouter(context: ReactRendererSetupContext) { export function initRouter(context: ReactRendererSetupContext) {
const { schema, boosts, appScope, renderer } = context; const { schema, boosts, appScope, renderer } = context;
const router = createRouter( const router = createRouter(schema.getByKey('router') ?? defaultRouterOptions);
schema.getByKey('router') ?? defaultRouterOptions
);
appScope.inject('router', router); appScope.inject('router', router);
boosts.add('router', router); boosts.add('router', router);

View File

@ -10,13 +10,7 @@ import {
isReactive, isReactive,
isShallow, isShallow,
} from '@vue/reactivity'; } from '@vue/reactivity';
import { import { noop, isObject, isPlainObject, isSet, isMap } from 'lodash-es';
noop,
isObject,
isPlainObject,
isSet,
isMap,
} from '@alilc/runtime-shared';
export { ref as createSignal, computed, effect }; export { ref as createSignal, computed, effect };
export type { Ref as Signal, ComputedRef as ComputedSignal }; export type { Ref as Signal, ComputedRef as ComputedSignal };
@ -32,7 +26,7 @@ export function watch<T = any>(
}: { }: {
deep?: boolean; deep?: boolean;
immediate?: boolean; immediate?: boolean;
} = {} } = {},
) { ) {
let getter: () => any; let getter: () => any;
let forceTrigger = false; let forceTrigger = false;
@ -42,9 +36,7 @@ export function watch<T = any>(
forceTrigger = isShallow(source); forceTrigger = isShallow(source);
} else if (isReactive(source)) { } else if (isReactive(source)) {
getter = () => { getter = () => {
return deep === true return deep === true ? source : traverse(source, deep === false ? 1 : undefined);
? source
: traverse(source, deep === false ? 1 : undefined);
}; };
forceTrigger = true; forceTrigger = true;
} else { } else {
@ -93,12 +85,7 @@ export function watch<T = any>(
return unwatch; return unwatch;
} }
function traverse( function traverse(value: unknown, depth?: number, currentDepth = 0, seen?: Set<unknown>) {
value: unknown,
depth?: number,
currentDepth = 0,
seen?: Set<unknown>
) {
if (!isObject(value)) { if (!isObject(value)) {
return value; return value;
} }

View File

@ -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') { export function getElementById(id: string, tag: string = 'div') {
let el = document.getElementById(id); 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(styles.map((item) => appendExternalCss(item)));
await Promise.all(scripts.map(item => appendExternalScript(item))); await Promise.all(scripts.map((item) => appendExternalScript(item)));
} }
async function appendExternalScript( async function appendExternalScript(
url: string, url: string,
root: HTMLElement = document.body root: HTMLElement = document.body,
): Promise<HTMLElement> { ): Promise<HTMLElement> {
if (url) { if (url) {
const el = getIfExistAssetByUrl(url, 'script'); const el = getIfExistAssetByUrl(url, 'script');
@ -73,9 +75,9 @@ async function appendExternalScript(
() => { () => {
resolve(scriptElement); resolve(scriptElement);
}, },
false false,
); );
scriptElement.addEventListener('error', error => { scriptElement.addEventListener('error', (error) => {
if (root.contains(scriptElement)) { if (root.contains(scriptElement)) {
root.removeChild(scriptElement); root.removeChild(scriptElement);
} }
@ -88,7 +90,7 @@ async function appendExternalScript(
async function appendExternalCss( async function appendExternalCss(
url: string, url: string,
root: HTMLElement = document.head root: HTMLElement = document.head,
): Promise<HTMLElement> { ): Promise<HTMLElement> {
if (url) { if (url) {
const el = getIfExistAssetByUrl(url, 'link'); const el = getIfExistAssetByUrl(url, 'link');
@ -105,9 +107,9 @@ async function appendExternalCss(
() => { () => {
resolve(el); resolve(el);
}, },
false false,
); );
el.addEventListener('error', error => { el.addEventListener('error', (error) => {
reject(error); reject(error);
}); });
@ -117,7 +119,7 @@ async function appendExternalCss(
export async function appendExternalStyle( export async function appendExternalStyle(
cssText: string, cssText: string,
root: HTMLElement = document.head root: HTMLElement = document.head,
): Promise<HTMLElement> { ): Promise<HTMLElement> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let el: HTMLStyleElement = document.createElement('style'); let el: HTMLStyleElement = document.createElement('style');
@ -128,9 +130,9 @@ export async function appendExternalStyle(
() => { () => {
resolve(el); resolve(el);
}, },
false false,
); );
el.addEventListener('error', error => { el.addEventListener('error', (error) => {
reject(error); reject(error);
}); });
@ -140,11 +142,10 @@ export async function appendExternalStyle(
function getIfExistAssetByUrl( function getIfExistAssetByUrl(
url: string, url: string,
tag: 'link' | 'script' tag: 'link' | 'script',
): HTMLLinkElement | HTMLScriptElement | undefined { ): HTMLLinkElement | HTMLScriptElement | undefined {
return Array.from(document.getElementsByTagName(tag)).find(item => { return Array.from(document.getElementsByTagName(tag)).find((item) => {
const elUrl = const elUrl = (item as HTMLLinkElement).href || (item as HTMLScriptElement).src;
(item as HTMLLinkElement).href || (item as HTMLScriptElement).src;
if (/^(https?:)?\/\/([\w.]+\/?)\S*/gi.test(url)) { if (/^(https?:)?\/\/([\w.]+\/?)\S*/gi.test(url)) {
// if url === http://xxx.xxx // if url === http://xxx.xxx

View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"paths": {
"@alilc/*": ["runtime/*/src"]
}
}
}

View File

@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['tests/*.spec.ts'],
environment: 'jsdom'
}
})

View File

@ -4,5 +4,13 @@
"description": "", "description": "",
"type": "module", "type": "module",
"bugs": "https://github.com/alibaba/lowcode-engine/issues", "bugs": "https://github.com/alibaba/lowcode-engine/issues",
"homepage": "https://github.com/alibaba/lowcode-engine/#readme" "homepage": "https://github.com/alibaba/lowcode-engine/#readme",
"license": "MIT",
"scripts": {
"build": "",
"test": "vitest"
},
"dependencies": {
"@alilc/renderer-core": "^2.0.0-beta.0"
}
} }

View File

@ -1,3 +1,6 @@
{ {
"extends": "../../tsconfig.json" "extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
}
} }