From a69f4eb47811aa46b59ec92fdbca25c0b06e6611 Mon Sep 17 00:00:00 2001 From: gchust Date: Wed, 2 Apr 2025 11:01:55 +0800 Subject: [PATCH] chore: remove useless files --- .cursor/rules/000-global.mdc | 27 - .cursor/rules/001-global-guidelines.mdc | 31 - .cursor/rules/002-component-guidelines.mdc | 23 - .../rules/003-documentation-guidelines.mdc | 23 - .../plugin-event-filter-system/design-cn.md | 1499 ----------------- .../plugin-event-filter-system/design.md | 1016 ----------- repos | 26 - 7 files changed, 2645 deletions(-) delete mode 100644 .cursor/rules/000-global.mdc delete mode 100644 .cursor/rules/001-global-guidelines.mdc delete mode 100644 .cursor/rules/002-component-guidelines.mdc delete mode 100644 .cursor/rules/003-documentation-guidelines.mdc delete mode 100644 packages/plugins/@nocobase/plugin-event-filter-system/design-cn.md delete mode 100644 packages/plugins/@nocobase/plugin-event-filter-system/design.md delete mode 100644 repos diff --git a/.cursor/rules/000-global.mdc b/.cursor/rules/000-global.mdc deleted file mode 100644 index 259d9c5f66..0000000000 --- a/.cursor/rules/000-global.mdc +++ /dev/null @@ -1,27 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -# Instructions - -During your interaction with the user, if you find anything reusable in this project (e.g. version of a library, model name), especially about a fix to a mistake you made or a correction you received, you should take note in the `Lessons` section in the `.cursorrules` file so you will not make the same mistake again. - -You should also use the `.cursorrules` file as a Scratchpad to organize your thoughts. Especially when you receive a new task, you should first review the content of the Scratchpad, clear old different task if necessary, first explain the task, and plan the steps you need to take to complete the task. You can use todo markers to indicate the progress, e.g. -[X] Task 1 -[ ] Task 2 - -Also update the progress of the task in the Scratchpad when you finish a subtask. -Especially when you finished a milestone, it will help to improve your depth of task accomplishment to use the Scratchpad to reflect and plan. -The goal is to help you maintain a big picture as well as the progress of the task. Always refer to the Scratchpad when you plan the next step. - -# Lessons - -## User Specified Lessons - -- Due to Cursor's limit, when you use `git` and `gh` and need to submit a multiline commit message, first write the message in a file, and then use `git commit -F ` or similar command to commit. And then remove the file. Include "[Cursor] " in the commit message and PR title. - -## Cursor learned -- When searching for recent news, use the current year (2025) instead of previous years, or simply use the "recent" keyword to get the latest information - -# Scratchpad \ No newline at end of file diff --git a/.cursor/rules/001-global-guidelines.mdc b/.cursor/rules/001-global-guidelines.mdc deleted file mode 100644 index ad689200e0..0000000000 --- a/.cursor/rules/001-global-guidelines.mdc +++ /dev/null @@ -1,31 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- ---- -description: Global coding standards and best practices for the project. -globs: "**/*" -alwaysApply: true ---- - -# Global Project Guidelines - -- **TypeScript Practices**: - - Enforce strict typing with explicit type definitions. - - Prefer interfaces over type aliases for object shapes. - - Avoid the use of `any`; strive for precise types. - -- **React Guidelines**: - - Employ functional components exclusively. - - Utilize React hooks for state and side effects management. - - Ensure components are reusable and adhere to the Single Responsibility Principle. - -- **Ant Design Integration**: - - Import Ant Design components as needed to optimize bundle size. - - Customize Ant Design themes to align with project branding. - -- **CSS-in-JS Styling**: - - Use a CSS-in-JS solution (e.g., styled-components or @emotion/styled) for component styling. - - Define styles within the same file as the component for better cohesion. - - Avoid inline styles; encapsulate styles within styled components or equivalent. diff --git a/.cursor/rules/002-component-guidelines.mdc b/.cursor/rules/002-component-guidelines.mdc deleted file mode 100644 index bb24f5100a..0000000000 --- a/.cursor/rules/002-component-guidelines.mdc +++ /dev/null @@ -1,23 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- ---- -description: Standards and practices for React components. -globs: "src/components/**/*.tsx" ---- - -# Component Guidelines - -- **File Structure**: - - Each component should reside in its own folder within `src/components/`. - - Include related files such as styles and tests within the same folder. - -- **Naming Conventions**: - - Use PascalCase for component names. - - Suffix component files with `.tsx`. - -- **Testing**: - - Write unit tests for each component using Jest and React Testing Library. - - Ensure tests cover various states and interactions of the component. diff --git a/.cursor/rules/003-documentation-guidelines.mdc b/.cursor/rules/003-documentation-guidelines.mdc deleted file mode 100644 index 6479016aa0..0000000000 --- a/.cursor/rules/003-documentation-guidelines.mdc +++ /dev/null @@ -1,23 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- ---- -description: Guidelines for writing and maintaining project documentation using Dumi. -globs: "docs/**/*.md" ---- - -# Documentation Guidelines - -- **Structure**: - - Organize documentation files within the `docs` directory. - - Use a clear and consistent hierarchy to facilitate navigation. - -- **Content**: - - Provide comprehensive explanations and examples for components and features. - - Keep language clear and concise, suitable for both new and experienced developers. - -- **Dumi Integration**: - - Utilize Dumi's features to create interactive and visually appealing documentation. - - Ensure that documentation is updated alongside code changes to maintain accuracy. diff --git a/packages/plugins/@nocobase/plugin-event-filter-system/design-cn.md b/packages/plugins/@nocobase/plugin-event-filter-system/design-cn.md deleted file mode 100644 index cb91de0cab..0000000000 --- a/packages/plugins/@nocobase/plugin-event-filter-system/design-cn.md +++ /dev/null @@ -1,1499 +0,0 @@ -# 事件过滤器系统设计 - -## 简介 - -事件过滤器系统插件集成了 NocoBase 中的两个核心系统: - -1. **事件系统**:管理整个应用程序中的事件发布和订阅。 -2. **过滤器系统**:提供跨多种数据类型的数据转换能力。 - -本文档概述了这些系统的设计原则、架构和实现细节。 - -## 系统流程图 - -### 事件系统流程 - -```mermaid -flowchart TD - A[组件事件触发] -->|分发| B[EventManager] - B -->|路由至| C[事件监听器] - C -->|通过过滤器| D{过滤函数} - D -->|不匹配| E[跳过监听器] - D -->|匹配| F[执行监听器] - F -->|可能触发| G[后续事件] - F -->|返回| H[结果收集] - H -->|解析至| I[原始 Promise] - - subgraph 选项 - J[priority] - K[once] - L[blocking] - M[uischema] - N[过滤函数] - end - - C -.-> 选项 -``` - -### 过滤器系统流程 - -```mermaid -flowchart TD - A[组件] -->|调用| B[applyFilter] - B -->|查找匹配的| C[已注册过滤器] - C -->|按排序| D[优先级] - D -->|处理| E[输入数据] - E -->|通过转换| F[过滤器链] - F -->|返回| G[修改后的数据] - G -->|回到| A - - subgraph 注册 - H[addFilter] -->|注册| I[新过滤器] - I -->|带有| J[名称] - I -->|带有| K[过滤函数] - I -->|带有| L[选项] - end -``` - -### 组件-事件-过滤器集成 - -```mermaid -flowchart LR - A[UI 组件] -->|用户操作| B[事件触发] - B -->|分发| C[EventManager] - C -->|路由至| D[事件处理器] - D -->|可能调用| E[FilterManager] - E -->|转换| F[数据] - F -->|返回至| D - D -->|更新| A - - G[其他组件] -->|注册| H[事件监听器] - H -.->|连接至| D - - I[插件] -->|注册| J[过滤器] - J -.->|连接至| E -``` - -## 核心组件 - -### 过滤器 API - -过滤器 API 提供了数据转换机制,并通过 `addFilter` 增强了自定义能力。 - -#### FilterManager - -```typescript -// 定义过滤器函数的预期签名 -// 它可以是同步的或返回一个 Promise -type FilterFunction = (currentValue: any, ...contextArgs: any[]) => any | Promise; - -// 定义添加过滤器的选项 -interface FilterOptions { - priority?: number; // 数字越大,运行越晚 -} - -class FilterManager { // 单例,通常通过 app.filterManager 访问 - // 为给定名称添加过滤器函数。 - addFilter(name: string, filter: FilterFunction, options?: FilterOptions): () => void; // 返回一个取消注册的函数 - - // 移除指定的过滤器函数或移除某个名称下的所有过滤器。 - removeFilter(name: string, filter?: FilterFunction): void; - - // 将某个名称下所有已注册的过滤器应用于初始值。 - // 始终返回一个 Promise。 - applyFilter(name: string, initialValue: any, ...contextArgs: any[]): Promise; -} -``` - -**关于异步性的说明:** 此 API 被设计为完全异步,以支持需要执行异步操作(例如 API 调用)的过滤器。`applyFilter` *始终* 返回一个 `Promise`,即使所有注册的过滤器都是同步的。调用者 *必须* 使用 `await` 或 `.then()` 来获取最终结果。 - -#### 过滤器命名约定 - -过滤器名称使用结构化的、类似命名空间的层级格式,以确保清晰并防止冲突: - -`origin:[domain]:[sub-module/component]:attribute` - -- **`origin`**:(必需)标识来源。 - - `core`:用于源自 NocoBase 核心的过滤器。 - - `plugin:[plugin-name]`:用于来自特定插件的过滤器(例如 `plugin:workflow`)。 -- **`domain`**:(必需)高层级功能区域(例如 `block`、`collection`、`field`、`ui`、`data`、`auth`)。**注意:** 对于常见的、明确与 UI 相关的组件(例如 `modal`、`button`、`table`、`form`),如果组件名称明确暗示了其 UI 属性,为了简洁起见,*可以* 省略 `ui` 域。对于其他域或不太常见的组件,应包含域。 -- **`sub-module/component`**:(可选但推荐)更具体的组件或上下文(例如 `table`、`form`、`users`、`props`)。 -- **`attribute`**:(必需)正在被过滤的特定数据方面(例如 `props`、`data`、`schema`、`options`、`height`、`validationRules`)。 - -**理由:** 虽然这种格式可能导致名称更长,但明确性对于以下几点至关重要: - -1. **清晰性**:确保过滤器的来源和目的清晰。 -2. **避免冲突**:防止不同过滤器之间的命名冲突。 -3. **调试**:便于调试和维护。 -4. **可扩展性**:允许未来扩展而不破坏现有代码。 -5. **使用常量**:使用常量作为过滤器名称有助于在整个应用程序中保持一致性。 - -**示例:** - -- `core:block:table:props`:过滤核心表格区块的 props。 -- `core:collection:users:schema`:过滤核心 `users` 集合的 schema。 -- `plugin:workflow:node:approval:conditions`:过滤 `workflow` 插件中节点的审批条件。 -- `plugin:map:field:coordinates:format`:过滤 `map` 插件中坐标字段的显示格式。 -- `core:field:text:validationRules`:过滤核心文本字段的验证规则。 - -注册时需要使用这种完整的、结构化的名称。注册期间不支持通配符。 - -#### 方法 - -##### addFilter - -```typescript -app.filterManager.addFilter( - name: string, - filter: FilterFunction, // 可以是同步或异步 - options?: FilterOptions -): () => void // 返回一个取消注册的函数 -``` - -**参数**: -- `name`: 过滤器名称,遵循 `origin:[domain]:[sub-module]:attribute` 约定。不支持通配符。 -- `filter`: 过滤器函数。它接收当前值(前一个过滤器的输出,或第一个过滤器的 `initialValue`)作为第一个参数,并将传递给 `applyFilter` 的任何 `contextArgs` 作为后续参数。它可以直接返回转换后的值,或返回一个解析为转换后值的 `Promise`。 -- `options`: 配置选项,目前只有 `priority`(默认为 0)。优先级较低的过滤器先运行。 - -**返回**: -- 一个函数,调用该函数将取消注册此特定过滤器。 - -##### removeFilter - -```typescript -app.filterManager.removeFilter(name: string, filter?: FilterFunction): void -``` - -**参数**: -- `name`: 过滤器名称,遵循约定。 -- `filter`: (可选)要移除的特定过滤器函数。如果省略,则移除为 `name` 注册的 *所有* 过滤器。 - -##### applyFilter - -```typescript -async applyFilter( - name: string, - initialValue: any, - ...contextArgs: any[] -): Promise -``` - -**参数**: -- `name`: 要应用的过滤器链的名称。 -- `initialValue`: 将作为第一个参数传递给链中第一个过滤器函数的起始值。 -- `...contextArgs`: (可选)此处提供的任何其他参数将作为第二个、第三个等参数传递给链中的 *每个* 过滤器函数,在 `currentValue` 之后。 - -**执行流程**: -1. 检索为 `name` 注册的所有过滤器函数。 -2. 根据 `priority` 对它们进行排序(优先级较低的优先)。 -3. 按顺序执行每个过滤器函数,传递前一个过滤器的结果(或第一个过滤器的 `initialValue`)和 `contextArgs`。 -4. **关键点:** 如果过滤器函数返回 `Promise`,管理器会 `await` 其解析,然后再继续下一个过滤器。 - -**返回**: -- 一个 `Promise`,在被链中所有匹配的过滤器处理后,解析为最终值。 -- 如果任何过滤器抛出错误或拒绝,该 Promise 将以带有上下文的错误 **被拒绝**(请参阅错误处理部分)。 -- **破坏性变更:** 调用者现在必须使用 `await` 或 `.then()` 并包含 `try...catch` 块来处理潜在的拒绝。 - -#### 示例 - -**推荐模式:过滤组件 Props** - -与其在过滤器内部链式调用 `applyFilter`,不如为 *相同* 的顶级名称(例如 `core:block:table:props`)注册多个过滤器,并使用 `priority` 和 `contextArgs` 来管理转换,这样通常更清晰。 - -**为什么不鼓励嵌套 `applyFilter`:** - -虽然技术上可行,但强烈不鼓励在传递给 `addFilter` 的过滤器函数 *内部* 调用 `app.filterManager.applyFilter`,原因如下: - -1. **绕过中央控制:** `FilterManager` 的主要作用是根据注册的 `priority` 来协调过滤器的执行。当一个过滤器函数内部为另一个名称(甚至相同名称)调用 `applyFilter` 时,该嵌套执行发生在由初始 `applyFilter` 调用管理的主要优先级排序序列 *之外*。这绕过了预期的中央控制机制。 -2. **模糊执行流程:** 仅仅通过查看已注册的过滤器及其优先级,很难理解应用于给定过滤器名称(例如 `core:block:table:props`)的完整转换序列。有效的流程取决于潜在的许多不同过滤器函数的内部逻辑。 -3. **增加调试复杂性:** 追踪一个值是如何被转换的变得更加困难。你不能仅仅依赖 `FilterManager` 的排序列表;你需要进入每个过滤器函数的代码,查看它是否触发了其他嵌套的过滤器链。 - -**推荐方法(多个过滤器,相同名称):** - -- 为同一个宽泛的过滤器名称注册多个独立的过滤器函数。 -- 使用 `priority` 精确控制执行顺序。 -- 通过主 `applyFilter` 调用的 `contextArgs` 传递必要的上下文数据。 - -这种方法使单个过滤器函数保持专注,使整体转换序列明确并由 `FilterManager` 根据优先级集中管理,并简化了调试。 - -```typescript -// 过滤器 1:根据上下文调整表格高度(同步) -app.filterManager.addFilter('core:block:table:props', (props, context) => { - if (context?.compact) { - // 确保 props 对象是可变的或创建一个新的 - const newProps = { ...props }; - newProps.height = Math.min(props.height || 600, 300); - return newProps; - } - return props; // 返回原始或可能修改过的 props -}, { priority: 10 }); // 相对较早运行 - -// 过滤器 2:基于异步用户角色检查添加 CSS 类(异步) -app.filterManager.addFilter('core:block:table:props', async (props, context) => { - // 假设 context.currentUser 存在,但我们需要获取角色详细信息 - const userRole = await fetchUserRole(context.currentUser.id); // 示例异步调用 - - if (userRole === 'admin') { - const newProps = { ...props }; - newProps.className = `${props.className || ''} admin-table`; - return newProps; - } - return props; -}, { priority: 20 }); // 在高度调整之后运行 - -// --- 组件用法 --- - -const MyTableComponent = (/* ... */) => { - const initialProps = useMemo(() => ({ /* 基础 props */ }), [/* 依赖项 */]); - const context = useMemo(() => ({ /* 上下文 */ }), [/* 依赖项 */]); - const [filteredProps, setFilteredProps] = useState(initialProps); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - let isMounted = true; - const loadFilteredProps = async () => { - setIsLoading(true); - try { - // 必须 await 结果 - const result = await app.filterManager.applyFilter( - 'core:block:table:props', - initialProps, - context - ); - if (isMounted) { - setFilteredProps(result); - } - } catch (error) { - console.error("应用过滤器时出错:", error); - // 适当地处理错误 - } finally { - if (isMounted) { - setIsLoading(false); - } - } - }; - - loadFilteredProps(); - - return () => { isMounted = false; }; // 清理 - }, [initialProps, context]); - - if (isLoading) { - return ; // 过滤器运行时显示加载指示器 - } - - return ; -} -``` - -**示例说明:** -- 第一个过滤器保持同步。 -- 第二个过滤器现在是 `async` 并使用 `await` 来获取用户角色。 -- **关键点**,组件用法 (`MyTableComponent`) 现在使用 `useEffect` 和 `async`/`await` 来调用 `applyFilter` 并处理返回的 `Promise`。它还包括加载和错误处理状态。 -- **注意:** 过滤器函数应注意对象的可变性。返回一个新对象 (`{...props}`) 通常比直接修改输入更安全,除非该过滤器链明确打算并记录了可变性操作。 - -### Hooks - -#### useAddFilter - -```typescript -const useAddFilter = (name:string, filterFunction, options) => { - useEffect(() => { - // 注册过滤器并在组件卸载时返回取消注册函数 - return app.filterManager.addFilter(name, filterFunction, options); - }, [name, filterFunction, options, app.filterManager]); // 确保依赖项完整 -} -``` - -#### useApplyFilter - -```typescript -// 这个 hook 可能需要根据使用模式进行调整或提供一个新的异步版本。 -// 一个简单的版本可能只是返回函数本身,而不立即执行。 -const useApplyFilter = (name: string) => { - const filterManager = app.filterManager; // 假设 app.filterManager 是稳定的 - - // 注意:这里返回的是异步函数本身。 - // 使用此 hook 的组件需要使用 await 来调用它。 - return useCallback(async (initialValue: any, ...contextArgs: any[]) => { - // 确保 filterManager 存在 - if (!filterManager) { - console.warn(`FilterManager not available for filter: ${name}`); - return initialValue; // 或者抛出错误 - } - return filterManager.applyFilter(name, initialValue, ...contextArgs); - }, [filterManager, name]); // 依赖项包括 filterManager 和 name -} -``` - -### 事件 API - -事件 API 管理整个应用程序中的事件订阅和发布。 - -#### EventManager - -```typescript -class EventManager { // 单例, 可以挂载到 app 上 - // 注册事件监听器 - on(eventName: string | string[], listener: EventListener, options: EventListenerOptions = {}): Unsubscriber; - - // 注册一次性事件监听器 - once(eventName: string | string[], listener: EventListener, options: Omit = {}): Unsubscriber; - - // 取消事件监听器 - off(eventName: string | string[], listener?: EventListener): void; - - // 触发事件并收集结果 - // 始终返回一个解析为 ctx.results 对象的 Promise。 - async dispatchEvent(eventName: string | string[], ctx: EventContext): Promise>; // 返回 ctx.results - - // 获取特定事件的所有监听器, 主要用于测试 - getListeners(eventName: string | string[]): EventListener[]; -} - -type EventListener = (ctx: EventContext) => void | Promise; -type Unsubscriber = () => void; -``` -*(注:补全了类型定义和方法签名以提高清晰度)* - -#### 事件命名约定 - -事件名称遵循结构化的、类似过滤器的层级格式,以确保清晰并防止冲突: - -`origin:[domain]:[sub-module/component]:action` - -- **`origin`**:(必需)`core` 或 `plugin:[plugin-name]`。 -- **`domain`**:(必需)高层级区域(例如 `block`、`collection`、`ui`、`system`、`auth`)。**注意:** 对于常见的、明确与 UI 相关的组件(例如 `modal`、`button`、`table`、`form`),如果组件名称明确暗示了其 UI 属性,为了简洁起见,*可以* 省略 `ui` 域。对于其他域或不太常见的组件,应包含域。 -- **`sub-module/component`**:(可选但推荐)特定上下文(例如 `table`、`form`、`modal`、`row`、`users`)。 -- **`action`**:(必需)描述事件,通常将动作动词与生命周期前缀/后缀结合。示例:`beforeCreate`、`afterUpdate`、`loadSuccess`、`validateError`、`submit`、`afterSubmit`、`click`、`open`。 - -**理由与建议:** 与过滤器命名约定相同(清晰性、避免冲突、调试、可扩展性、使用常量)。 - -**示例:** - -- `core:collection:posts:beforeCreate` // 在核心 `posts` 集合中创建记录之前。 -- `core:block:table:row:select` // 在核心表格区块中选择了一行。 -- `core:modal:open` // 打开一个通用的核心模态框(省略了 `ui` 域)。 -- `core:form:afterSubmit` // 通用表单提交之后(使用组合动作)。 -- `core:auth:login:success` // 用户成功登录事件。 -- `plugin:workflow:execution:start` // 工作流执行开始(来自 `workflow` 插件)。 -- `plugin:audit-log:entry:created` // 创建了一条审计日志条目(来自 `audit-log` 插件)。 - -监听器可以使用通配符进行监听(例如 `core:block:table:**`),但分发必须使用特定的事件名称。(注意:通配符支持需要根据 `EventManager` 的实现细节确认)。 - -**监听器通配符支持:** - -通过 `on()` 和 `once()` 注册的事件监听器 **支持在 `eventName` 字符串中使用通配符** (`*`) 来匹配多个事件。通配符 `*` 精确匹配事件名称层级中的一个段。 - -- 示例:`core:collection:*:afterCreate` 会匹配 `core:collection:posts:afterCreate`、`core:collection:users:afterCreate` 等。 -- 示例:`plugin:workflow:*:start` 会匹配 `plugin:workflow:execution:start`、`plugin:workflow:node:start` 等。 -- 示例:`core:*:*:select` 会匹配 `core:block:table:select`、`core:field:user:select` 等。 - -**注意:** -- `dispatchEvent` **必须** 始终使用特定的、非通配符的事件名称。 -- 使用许多广泛的通配符(例如 `core:**:**`)可能会有性能影响,因为 `EventManager` 需要为每个分发的事件执行模式匹配。 - -#### EventContext - -```typescript -interface EventContext { - // 事件源 (dispatchEvent调用者) 信息 - // 监听器应该将其视为只读。 - source?: { - id?: string; // 触发源的标识符,例如按钮的 key 或区块的 uid - type?: string; // 触发类型,例如 'user-interaction', 'system', 'workflow' - component?: any; // 触发事件的组件实例(可选) - [key: string]: any // 其他额外信息 - }; - - // 用于指定接收者信息, 主要用于精准触发事件 - // 监听器应该将其视为只读。 - target?: { - id?: string; // 目标标识符,例如特定区块的 uid - uischema?: ISchema; // 目标的 UI Schema (用于 UI 组件定位) - }; - - // 事件相关数据 - // 监听器按约定应将其视为只读。 - // 使用 ctx.results 进行输出。 - payload?: T; - - // 元数据 - // 监听器应该将其视为只读。 - meta?: { - timestamp?: number; // 事件触发的时间戳 - userId?: string; // 当前用户 ID (可选) - event?: string | string[]; // 当前处理的事件名称(对于通配符监听器有用) - // 原始的 eventName (dispatch 时传入的) - dispatchedEventName?: string | string[]; - [key: string]: any; // 其他元数据 - }; - - // 用于收集事件监听器的输出结果 - // 由 dispatchEvent 初始化为 {}。监听器在此处添加属性。 - // 也可以包含保留的控制标志,如 `_stop` 或 `_errors`。 - results: Record; - - // 控制事件传播(可选的高级功能) - // propagation?: { - // stopped: boolean; - // stop: () => void; - // }; -} - -// 假设 ISchema 的基本定义 -interface ISchema { - 'x-uid'?: string; - [key: string]: any; -} -``` -*(注:增加了 `component` 和 `dispatchedEventName` 字段,细化了注释)* - -**监听器指南:** - -* **上下文不可变性:** 监听器应将传入的上下文属性(`ctx.source`, `ctx.target`, `ctx.payload`, `ctx.meta`)视为 **只读**。直接修改这些共享对象可能导致其他监听器或事件分发器产生不可预测的副作用。 -* **提供结果:** 为了将结果或数据传回给调用者或其他潜在的监听器(取决于执行顺序),监听器应 **向 `ctx.results` 对象添加属性**。`dispatchEvent` 返回此对象的最终状态。 -* **停止传播:** 监听器可以通过设置 `ctx.results._stop = true` 来阻止后续监听器(针对当前分发)运行。`dispatchEvent` 的 promise 仍将正常解析,并包含截至该点的已收集结果。 -* **覆盖结果:** 请注意,如果多个监听器写入 `ctx.results` 中的相同属性键,则最后完成的监听器设置的值将在最终返回的对象中生效。 - -#### 监听器选项 - -```typescript -interface EventListenerOptions { - priority?: number; // 监听器优先级,数值越小优先级越高,默认为 0 (与 Filter 相反,保持常见事件库风格) - once?: boolean; // 是否只执行一次,默认为 false - blocking?: boolean; // 是否为阻塞监听器,默认 false。阻塞监听器失败会 reject dispatchEvent 的 Promise。 - uischema?: ISchema; // UI Schema,主要用于在 defaultListenerFilter 中进行精准目标匹配 - filter?: (ctx: EventContext, options: EventListenerOptions) => boolean | Promise; // 过滤函数,允许根据上下文异步确定监听器是否执行。默认为 defaultListenerFilter。 - id?: string; // 监听器的唯一标识符(可选,用于调试或特定移除场景) -} -``` -*(注:调整了 `priority` 描述以符合常见实践,明确了 `blocking` 的影响,并允许 `filter` 返回 Promise)* - -默认过滤函数: -```typescript -/** - * 事件监听器的默认过滤函数。 - * 根据事件上下文 (ctx) 和监听器的选项确定监听器是否应执行。 - * - * 逻辑: - * 1. 如果事件分发未包含目标 (ctx.target 为假值),则监听器始终运行(多播)。 - * 2. 如果事件分发包含目标 (ctx.target 存在): - * a. 如果监听器选项包含特定的目标条件 (例如 options.uischema 且具有 'x-uid'), - * 则仅当目标条件与上下文的目标匹配时,监听器才运行。 - * b. 如果监听器选项不包含特定的目标条件 (例如没有 options.uischema 或没有 'x-uid'), - * 则监听器运行(它接受任何目标性事件)。 - */ -function defaultListenerFilter(ctx: EventContext, options: EventListenerOptions): boolean { - // 1. 多播:分发中未指定目标?监听器运行。 - if (!ctx.target) { - return true; - } - - // 2. 单播:分发中指定了目标。检查监听器选项。 - const listenerSchema = options.uischema; - const targetSchema = ctx.target.uischema; - - // 检查监听器是否有基于 uischema 的目标定位 - if (listenerSchema && listenerSchema['x-uid']) { - // 监听器具有 uischema 目标。仅当目标 uischema 存在且 'x-uid' 匹配时运行。 - return !!(targetSchema && targetSchema['x-uid'] && listenerSchema['x-uid'] === targetSchema['x-uid']); - } else { - // 监听器没有定义特定的目标选项(如此处的 uischema 'x-uid')。 - // 因此,即使是目标性分发,它也应该运行。 - return true; - } -} -``` -*(注:代码和注释与英文版保持一致,细化了逻辑说明)* - -#### 方法 - -##### on - -```typescript -on(event: string | string[], listener: EventListener, options?: EventListenerOptions): Unsubscriber -``` - -用于注册事件监听器。当触发与 `event` 名称(或模式,如果使用通配符)匹配的事件时,将执行监听器。 - -**参数:** -- `event`: 要监听的特定事件名称、通配符模式(使用 `*`)或名称/模式数组。 -- `listener`: 事件发生时要执行的函数。可以是同步或异步 (`async`) 函数。 -- `options`: 监听器的配置(优先级、阻塞等)。 - -**返回:** 一个 `Unsubscriber` 函数,用于移除此特定监听器。 - -##### once - -```typescript -once(event: string | string[], listener: EventListener, options?: Omit): Unsubscriber -``` - -与 `on()` 类似,但在 *任何* 匹配 `event` 名称或模式的事件第一次执行后,监听器会自动移除。 - -**参数:** 与 `on()` 相同(`once` 选项由 `once` 方法隐式设置)。 - -##### off - -```typescript -// 移除特定事件名称(或模式)下的所有监听器 -off(eventName: string | string[]): void; -// 移除特定事件名称(或模式)下的指定监听器 -off(eventName: string | string[], listener: EventListener): void; -``` - -移除事件监听器。 - -##### dispatchEvent - -```typescript -// 注意:异步结果可能需要重新考虑 timeout 参数 -async dispatchEvent( - eventName: string | string[], - ctx: EventContext - // timeout?: number // 为简化异步结果,暂时移除 timeout -): Promise>; // 解析为 ctx.results -``` - -**参数**: -- `eventName`: 事件名称或名称数组。**必须是具体的名称,不支持通配符**。 -- `ctx`: 事件上下文对象。`EventManager` 将确保 `ctx.results` 存在,然后再将其传递给监听器。通常 `ctx` 由调用者提供,至少包含 `payload` 或 `source`。 - -**执行流程**: -1. 确保 `ctx.results` 存在且是一个空对象 `{}`。 -2. 填充 `ctx.meta.dispatchedEventName`。 -3. 查找与 `eventName`(或数组中的每个名称)匹配的所有监听器。 -4. 根据 `priority`(值越小越优先)、`blocking` 选项和 `filter` 函数(包括异步过滤器)执行监听器。 - - 对于每个匹配的监听器,首先(异步)执行其 `filter` 函数(如果存在)。 - - 如果过滤器返回 `true`,则(异步)执行监听器本身。 -5. 适当地处理 `async` 监听器,如果需要,等待其完成(特别是阻塞监听器)。 -6. 监听器可以在其执行期间向共享的 `ctx.results` 对象添加属性。 -7. **每个监听器完成(或失败)后:** 检查 `ctx.results._stop === true`。如果是,则停止处理此分发的后续监听器。 -8. 捕获监听器的错误,并根据错误处理策略处理它们(对于非阻塞错误,填充 `ctx.results._errors`;对于阻塞失败,可能拒绝主 promise)。 - -**返回**: -- 一个 `Promise`,在所有相关监听器完成执行 *或* 通过 `ctx.results._stop` 停止传播后,解析为 `ctx.results` 对象的 **最终状态**。此对象包含累积的输出以及可能的控制标志(`_stop`、`_errors`)。 -- 如果一个 **阻塞** 监听器失败(抛出错误或返回拒绝的 Promise),则 `dispatchEvent` 返回的 Promise 将以该监听器的带上下文的错误 **被拒绝**。 -- 如果 **没有阻塞** 监听器失败(即使非阻塞监听器失败了),`dispatchEvent` 返回的 Promise 将 **成功解析** 为最终的 `ctx.results` 对象。调用者 **必须** 检查 `ctx.results._errors` 来检测非阻塞监听器的部分失败。 - -## 错误处理策略 - -本节概述了在过滤器和事件系统内如何处理错误。 - -**1. 过滤器系统 (`applyFilter`)** - -* **策略:** 快速失败并携带上下文。 -* **行为:** 如果链中的任何过滤器函数抛出错误或返回一个被拒绝的 Promise,`FilterManager` 会立即停止处理该特定 `applyFilter` 调用的剩余过滤器。 -* **错误传播:** `applyFilter` 返回的 `Promise` 将 **被拒绝**。 -* **错误上下文:** `FilterManager` 捕获内部错误,并在拒绝前对其进行增强(或包装),提供关键的调试信息。被拒绝的错误对象通常会包含: - * `filterName`:失败过滤器的完整名称。 - * `filterPriority`:失败过滤器的优先级。 - * `originalError`:过滤器函数抛出的原始错误。 -* **日志记录:** 带有上下文的错误由 `FilterManager` 在内部记录(例如,通过 `console.error`)。 -* **理由:** 过滤器链通常对数据完整性至关重要。立即失败提供了明确的信号,而添加的上下文有助于调试。 - -**2. 事件系统 (`dispatchEvent`)** - -* **策略:** 收集非阻塞错误,对阻塞错误快速失败。 -* **行为(非阻塞监听器):** 如果一个非阻塞监听器(`blocking: false` 或默认)抛出错误或返回一个被拒绝的 Promise,`EventManager` **不会** 停止处理该 `dispatchEvent` 调用的其他监听器。 -* **行为(阻塞监听器):** 如果一个阻塞监听器(`blocking: true`)抛出错误或返回一个被拒绝的 Promise,`EventManager` 会 **停止** 处理该特定 `dispatchEvent` 调用的后续监听器。 -* **错误收集(非阻塞失败):** - * `EventManager` 包装监听器执行以捕获错误。 - * 来自非阻塞监听器的错误被捕获,并使用诸如 `listener` 标识符(如果可能)、`eventName` 和 `originalError` 等详细信息进行上下文关联。 - * 这些带有上下文的错误被添加到一个位于结果对象内的专用数组中:`ctx.results._errors = []`。(`_errors` 属性为此目的保留)。 -* **错误传播和返回值:** - * 如果一个 **阻塞** 监听器失败,`dispatchEvent` 返回的 `Promise` 将以该监听器的带有上下文的错误 **被拒绝**。 - * 如果 **没有阻塞** 监听器失败(即使非阻塞监听器失败了),`dispatchEvent` 返回的 `Promise` 将 **成功解析** 为最终的 `ctx.results` 对象。调用者 **必须** 检查 `ctx.results._errors` 来检测来自非阻塞监听器的部分失败。 -* **日志记录:** 所有捕获到的错误(来自阻塞和非阻塞监听器)都由 `EventManager` 在内部连同其上下文一起记录。 -* **理由:** 这为非关键的副作用(非阻塞监听器)提供了韧性,同时确保关键工作流(阻塞监听器)清晰地失败。错误收集允许意识到部分失败。 - -## 表单提交流程示例 - -```mermaid -sequenceDiagram - participant 用户 - participant 表单 - participant EventManager - participant 提交前监听器 - participant 提交监听器 - participant 提交后监听器 - - 用户->>表单: 点击提交 - 表单->>EventManager: dispatchEvent(':beforeSubmit', ctx) - EventManager->>提交前监听器: 路由到处理器 - Note over 提交前监听器: 处理器可能向 ctx.results 添加内容 - 提交前监听器-->>EventManager: 完成 - EventManager-->>表单: 返回 Promise - 表单->>表单: 处理结果 (例如, 检查 ctx.results.validation) - alt 如果验证失败或通过 ctx.results 请求停止 - 表单-->>用户: 显示反馈 (停止) - else 继续提交 - 表单->>EventManager: dispatchEvent('form:submit', ctx) - EventManager->>提交监听器: 路由到处理器 - Note over 提交监听器: 处理器可能向 ctx.results 添加内容 - 提交监听器-->>EventManager: 完成 - EventManager-->>表单: 返回 Promise - 表单->>表单: 处理结果 - 表单->>EventManager: dispatchEvent('form:afterSubmit', ctx) - EventManager->>提交后监听器: 路由到处理器 - Note over 提交后监听器: 处理器可能向 ctx.results 添加内容 - 提交后监听器-->>EventManager: 完成 - EventManager-->>表单: 返回 Promise - 表单-->>用户: 显示成功反馈 - end -``` -*(注:将英文参与者和消息翻译为中文)* - -## 设计考量 - -### 当前实现挑战 - -1. **对象不可变性**:目前 `source`、`target`、`meta` 属性只能通过 `Object.freeze` 冻结,但对 payload 的修改没有限制。是否应该保护更多属性? - -2. **异步事件收集**:确定 `dispatchEvent` 是否应该收集并返回处理程序的返回值。如果需要返回值,应该是来自所有处理程序还是特定的处理程序? - -3. **性能优化**: - - 防止事件风暴 - - 实现防抖(debounce)机制 - -4. **错误处理**:考虑如何可靠地处理事件监听器中的错误 - -5. **事件链**:支持复杂的事件流并防止意外的循环依赖 - -### 集成点 - -1. **组件操作**: - ```jsx - - - // 内核已经监听 form:record:create 事件 - app.on('form:record:create', (ctx) => { - openCreationModal(ctx); // 假设这是一个打开创建模态框的函数 - }); - ``` - -2. **表单提交流**: - ```typescript - // block-form.ts - async function submit() { - const actionId = 'mySubmitAction'; // 示例动作 ID - const beforeSubmitCtx = { - results: {}, - source: { - id: actionId, - type: 'user-interaction' - }, - // ... 其他上下文,例如 payload: formData - }; - try { - // 分发并获取结果 - const beforeSubmitResults = await app.eventManager.dispatchEvent('core:form:beforeSubmit', beforeSubmitCtx); // 使用标准命名 - - // 检查结果中的停止条件或验证失败 - if (beforeSubmitResults._stop || beforeSubmitResults.validationFailed) { - console.log('提交被前置监听器阻止或验证失败'); - // 可能需要显示错误消息 - return; - } - - // 继续提交 - const submitCtx = { ...beforeSubmitCtx }; // 可以复用或创建新的上下文 - const submitResults = await app.eventManager.dispatchEvent('core:form:submit', submitCtx); - // 处理提交结果... - - const afterSubmitCtx = { ...submitCtx, results: {} }; // 为 afterSubmit 创建新 results - await app.eventManager.dispatchEvent('core:form:afterSubmit', afterSubmitCtx); - // 显示成功消息等... - - } catch (error) { - console.error('表单提交过程中发生错误:', error); - // 处理阻塞监听器可能抛出的拒绝错误 - } - } - - // event-flow.ts (某个插件或应用逻辑中) - const actionId = 'mySubmitAction'; - app.eventManager.on('core:form:beforeSubmit', async (ctx) => { - // 假设 openDialog 是一个异步操作,例如打开确认对话框 - const confirmed = await openDialog('确认提交吗?', ctx); - if (!confirmed) { - ctx.results._stop = true; // 如果用户取消,则停止后续处理 - } - // 可以在这里添加验证逻辑到 ctx.results - // if (!isValid(ctx.payload)) { - // ctx.results.validationFailed = true; - // ctx.results._stop = true; // 通常验证失败也应停止 - // } - }, { - // 使用 filter 确保只处理来自特定按钮的事件 - filter: (eventCtx, listenerOptions) => { - return eventCtx.source?.id === listenerOptions.id; - }, - id: actionId, // 将 actionId 作为监听器选项传递 - blocking: true // 确认对话框通常是阻塞性的 - }); - ``` - *(注:更新了示例代码以使用标准事件名称,添加了错误处理和更实际的上下文/结果用法)* - -3. **表格组件集成**: - ```jsx - // 假设 Table1 和 Table2 是两个独立的表格组件实例 - const table1Uid = 'table1-uid'; - const table2Uid = 'table2-uid'; - - // --- Table1 组件内部或其父组件 --- - - useEffect(() => { - const refresh = () => { - console.log('刷新 Table1 数据'); - // 调用 Table1 实例的刷新方法... - }; - const unsub = app.eventManager.on('core:block:table:data:refresh', refresh, { - // 使用 filter 函数或 uischema 进行目标匹配 - filter: (ctx) => !ctx.target || ctx.target.id === table1Uid // 处理多播和针对 Table1 的单播 - // 或者如果 uischema 可用且包含 uid: - // uischema: { 'x-uid': table1Uid } - }); - return unsub; // 组件卸载时取消订阅 - }, [table1Uid]); - - - // --- Table2 组件内部或其父组件 --- - - useEffect(() => { - const refresh = () => { - console.log('刷新 Table2 数据'); - // 调用 Table2 实例的刷新方法... - }; - const unsub = app.eventManager.on('core:block:table:data:refresh', refresh, { - filter: (ctx) => !ctx.target || ctx.target.id === table2Uid - // 或者: uischema: { 'x-uid': table2Uid } - }); - return unsub; - }, [table2Uid]); - - - // --- 触发刷新的按钮 --- - - // 按钮 1: 刷新所有表格 (多播) - - - // 按钮 2: 只刷新 Table2 (单播) - - ``` - *(注:使用了更标准的事件名,演示了通过 `useEffect` 在组件生命周期内管理订阅,并使用 `filter` 或 `target.id` 进行目标定位)* - -## 架构目标 - -1. **解耦 UI 和业务逻辑**:清晰分离事件触发和处理逻辑 - ```jsx - // 耦合的方式 - function ButtonWithLogic(props) { - const handleClick = () => { - // UI 组件内混合了业务逻辑 - openDialog(props.dialogContext); // 直接调用业务函数 - refreshData(props.refreshContext); - } - return ; - } - - // 解耦的方式 (推荐) - function ActionButton(props) { - const handleClick = () => { - // UI 组件只负责触发语义化的事件 - app.eventManager.dispatchEvent('ui:button:action:click', { - source: { id: props.actionKey, type: 'user-interaction' }, - payload: props.eventPayload - }); - } - return ; - } - - // 业务逻辑在监听器中处理 (可位于不同文件/模块) - app.eventManager.on('ui:button:action:click', (ctx) => { - if (ctx.source.id === 'myActionKey') { - openDialog(ctx.payload.dialogContext); - refreshData(ctx.payload.refreshContext); - } - }); - - // 另一个适合编排的方式:通过事件触发其他事件 - function orchestrateAction(ctx) { - // 这个 action 实现方式适合编排,只需保存需要触发的事件及顺序即可 - app.dispatchEvent('dialog:open', { payload: ctx.payload.dialogData }); // 触发打开对话框事件 - app.dispatchEvent('data:refresh', { payload: ctx.payload.refreshScope }); // 触发数据刷新事件 - } - // 监听某个初始事件来启动编排 - app.eventManager.on('app:start:someProcess', orchestrateAction); - - ``` - *(注:提供了更对比鲜明的例子,并展示了事件驱动的编排)* - -2. **提高可测试性**:事件处理器中的业务逻辑更容易独立测试 - ```javascript - // 假设 openCreationModal 是在监听器内部调用的函数 - import { openCreationModal } from './modalLogic'; - import { eventManager } from './eventManager'; // 假设是单例实例 - - // Mock 依赖项 - jest.mock('./modalLogic'); - - // 测试事件处理器逻辑 - test('form:record:create 事件处理器应打开创建模态框', () => { - const mockContext = { - source: { id: 'someButton' }, - payload: { entity: 'users' } - }; - // 假设 'form:record:create' 的监听器直接调用 openCreationModal - // 获取监听器实例(或者直接测试注册的函数) - const listeners = eventManager.getListeners('form:record:create'); - // 假设只有一个监听器,或者找到需要测试的那个 - const handler = listeners[0]; // 注意:实际获取方式可能不同 - - // 执行处理器 - handler(mockContext); - - // 断言业务逻辑函数被正确调用 - expect(openCreationModal).toHaveBeenCalledTimes(1); - expect(openCreationModal).toHaveBeenCalledWith(mockContext); // 确认传递了上下文 - }); - ``` - *(注:使用了 Jest 的 mock 功能来演示单元测试)* - -3. **增强可扩展性**:通过过滤器 API,允许在不修改核心代码的情况下进行扩展 - ```typescript - // 核心代码可能只提供基础功能 - app.on('core:table:record:create', async (ctx) => { - // 核心逻辑:打开通用的创建模态框 - const recordData = await openGenericCreationModal(ctx.payload.collectionName); - ctx.results.createdRecord = recordData; // 将结果放入 results - }); - - // 插件或应用特定代码可以添加额外的行为 - // 示例:添加创建前的验证 - app.on('core:table:record:create', async (ctx) => { - const validationResult = await app.dispatchEvent('plugin:data-validation:validate', { - payload: { action: 'create', collection: ctx.payload.collectionName } - }); - if (validationResult.errors) { - ctx.results._stop = true; // 如果验证失败,停止后续核心逻辑 - ctx.results.validationErrors = validationResult.errors; - showValidationErrors(validationResult.errors); // 显示错误 - } - }, { priority: -10 }); // 优先级设为负数,确保在核心逻辑之前运行 - - // 示例:记录创建日志 - app.on('core:table:record:create', (ctx) => { - // 检查核心逻辑是否已成功执行 (通过检查 results 和 _stop) - if (!ctx.results._stop && ctx.results.createdRecord) { - app.dispatchEvent('plugin:audit-log:log', { - payload: { action: 'create', record: ctx.results.createdRecord } - }); - } - }, { priority: 10 }); // 优先级设为正数,在核心逻辑之后运行 - ``` - *(注:展示了如何使用不同优先级的监听器来扩展核心事件,实现验证和日志记录等功能)* - -4. **简化工作流**:支持事件链和工作流 - ```typescript - // 简化复杂流程:创建记录后自动刷新表格并通知用户 - app.on('core:table:record:create:success', (ctx) => { // 假设有一个成功事件 - // 触发后续事件 - // 1. 刷新相关表格 - app.dispatchEvent('core:block:table:data:refresh', { - target: { id: ctx.source.tableUid } // 假设源信息中有表格 ID - }); - - // 2. 显示成功通知 - app.dispatchEvent('core:system:notification:show', { - payload: { type: 'success', message: '记录创建成功!' } - }); - - // 3. 可能触发其他业务流程 - if (ctx.payload.needsFurtherAction) { - app.dispatchEvent('plugin:workflow:start', { payload: { trigger: 'recordCreated', recordId: ctx.results.createdRecord.id } }); - } - }); - - // 另一个例子:打开表单,提交后刷新数据 - app.on('ui:button:openCreateForm:click', async (ctx) => { - const formData = await openForm(ctx.payload.formConfig); // 打开表单并等待结果 - if (formData) { // 检查用户是否提交了数据 - // 触发一个事件来处理表单数据的保存 - const saveResult = await app.dispatchEvent('core:data:record:save', { payload: formData }); - if (!saveResult._errors && !saveResult._stop) { - // 保存成功后触发数据刷新 - app.dispatchEvent('core:block:table:data:refresh', { target: { id: ctx.payload.targetTableUid } }); - } - } - }); - ``` - *(注:提供了两个更具体的事件链示例)* - -5. **方法封装**:通过功能模块更好地组织代码 - ```typescript - // --- dialogLogic.ts --- - export function openDialog(ctx) { - console.log('打开对话框,上下文:', ctx); - // 实现打开对话框的逻辑... - return true; // 或返回 Promise - } - app.on('core:dialog:open', openDialog); // 将函数注册为监听器 - - // --- dataLogic.ts --- - export function refreshData(ctx) { - console.log('刷新数据,上下文:', ctx); - // 实现刷新数据的逻辑... - return true; // 或返回 Promise - } - app.on('core:data:refresh', refreshData); - - // --- component.ts --- - import { eventManager } from './eventManager'; - - function MyComponent() { - const handleOpen = () => { - eventManager.dispatchEvent('core:dialog:open', { payload: 'dialog data' }); - } - const handleRefresh = () => { - eventManager.dispatchEvent('core:data:refresh', { payload: 'refresh scope' }); - } - // ... - } - ``` - *(注:展示了将逻辑封装在不同模块的函数中,并通过事件系统连接)* - -6. **一致性**:在整个实现中保持可预测的行为 - ```typescript - // 方便编排:支持通过配置动态改变运行时逻辑,为事件流(工作流引擎)做准备 - - // 不利于编排的动作实现 (直接调用) - function directAction(ctx) { - console.log("执行直接操作..."); - openDialog(ctx); // 硬编码调用 - refreshData(ctx); // 硬编码调用 - } - - // 利于编排的动作实现 (通过分发事件) - function eventDrivenAction(ctx) { - console.log("执行事件驱动的操作..."); - app.dispatchEvent('core:dialog:open', ctx); // 分发事件 - app.dispatchEvent('core:data:refresh', ctx); // 分发事件 - } - - // 编排配置 (例如,从 JSON 或数据库加载) - const workflowSteps = [ - { event: 'core:dialog:open', params: { message: '步骤 1' } }, - { event: 'core:data:refresh', params: { scope: 'users' } }, - { event: 'plugin:notification:show', params: { type: 'success', message: '完成' } } - ]; - - // 工作流引擎执行 - async function runWorkflow(steps, initialContext) { - let currentCtx = initialContext; - for (const step of steps) { - console.log(`执行工作流步骤: ${step.event}`); - // 合并参数到上下文中 (简化示例) - const stepCtx = { ...currentCtx, payload: step.params, results: {} }; - currentCtx = await app.dispatchEvent(step.event, stepCtx); - // 可以在此检查 currentCtx.results._stop 或 _errors 来处理中断 - if (currentCtx.results._stop) { - console.log('工作流被步骤中断:', step.event); - break; - } - } - console.log('工作流执行完毕'); - } - - // 触发工作流 - runWorkflow(workflowSteps, { source: { type: 'workflow' } }); - ``` - *(注:强化了编排的概念,展示了如何通过事件配置和执行引擎来实现动态工作流)* - -## 菜单配置流程 - -```mermaid -flowchart TD - A[设置菜单项] -- 点击 --> B(触发 onClick 处理函数) - B -- 分发 --> C{block:setting:update 事件?} - C -- 否 --> D[直接执行操作或分发其他事件, e.g., dialog:open] - C -- 是 --> E[分发 block:setting:update 事件] - - subgraph 过滤器流程 (处理菜单项展示逻辑) - F[applyFilter('settings:menu:item:props', initialProps, ctx)] --> G{获取最终 props} - G -- 应用 --> A - end - - subgraph 事件流程 (处理点击行为) - B -- 可能分发 --> H[例如: eventFlow:add (添加到事件流)] - H -- 可能分发 --> I[例如: dialog:open (打开设置对话框)] - I -- 可能分发 --> E[block:setting:update (如果对话框保存)] - end - - E --> J[已注册的监听器] - J --> K[例如: 更新区块设置] - - style F fill:#f9f,stroke:#333,stroke-width:2px - style H fill:#ccf,stroke:#333,stroke-width:2px -``` -*(注:重新梳理了流程图,使其更清晰地反映配置和事件处理的分离)* - -## 实现示例 - -### 组件用法 - -```jsx -import React, { useState, useEffect, useMemo, useCallback } from 'react'; -import { useActionSettings, useFilterSettings } from './customHooks'; // 假设的 Hooks -import { app } from './app'; // 假设的 app 实例 -import { Spin, Menu } from 'antd'; // 示例 UI 库 - -// 假设的内部组件和设置菜单 -const SomeInternalComponent = ({ onClick, ...props }) => ; -const SettingMenus = ({ menus }) =>
设置菜单: {JSON.stringify(menus)}
; - - -function MyComponent(props) { - // 假设这些 hooks 返回配置对象 - const [actionSettings] = useActionSettings('myComponentAction'); - const [filterSettings] = useFilterSettings('myComponentFilter'); - - // 使用 useApplyFilter Hook 获取异步应用过滤器的函数 - const applyPropsFilter = useApplyFilter('myComponent:props'); - const [filteredProps, setFilteredProps] = useState(props); - const [isLoading, setIsLoading] = useState(true); // 初始为 true,等待过滤器应用 - - // 应用过滤器来获取最终的 props - useEffect(() => { - let isMounted = true; - const applyFilters = async () => { - setIsLoading(true); - try { - // 使用 Hook 返回的函数,传入初始 props 和上下文 - const result = await applyPropsFilter(props, { componentContext: filterSettings }); - if (isMounted) { - setFilteredProps(result); - } - } catch (error) { - console.error("应用组件 props 过滤器时出错:", error); - // 可以设置错误状态或使用原始 props - if (isMounted) setFilteredProps(props); - } finally { - if (isMounted) setIsLoading(false); - } - }; - applyFilters(); - return () => { isMounted = false }; - }, [props, applyPropsFilter, filterSettings]); // 依赖项 - - const handleClick = useCallback(async () => { - // 在 dispatchEvent 之前可以做一些事情 - const eventCtx = { - source: { id: 'myComponentButton', type: 'user-interaction' }, - payload: { someData: 'example' }, - settings: actionSettings // 将动作配置附加到上下文中 - }; - - try { - // 示例:二次确认 - if (actionSettings?.secondConfirm) { - const confirmCtx = { ...eventCtx, payload: { message: '确定执行操作吗?' } }; - const confirmResult = await app.eventManager.dispatchEvent('core:system:dialog:confirm', confirmCtx); - if (confirmResult.confirmed !== true) { - console.log('操作取消'); - return; // 用户取消,则不继续 - } - } - - // 分发主要的动作事件 - const actionResult = await app.eventManager.dispatchEvent('core:action:myComponent:click', eventCtx); - - // 检查动作结果以及是否需要刷新 - if (!actionResult._stop && actionSettings?.refreshAfterDone) { - // 假设需要刷新另一个区块 'block-xyz' - await app.eventManager.dispatchEvent('core:block:xyz:refresh', { - source: { id: 'myComponentButton', type: 'system' }, // 源可以是触发它的组件/按钮 - payload: { triggerAction: 'myComponent:click' } - }); - } - console.log('操作完成', actionResult); - - } catch (error) { - console.error('处理点击事件时出错:', error); - // 处理阻塞监听器可能抛出的拒绝 - app.eventManager.dispatchEvent('core:system:notification:show', { - payload: { type: 'error', message: `操作失败: ${error.message}` } - }); - } - - }, [actionSettings, app.eventManager, applyPropsFilter]); // 确保 useCallback 依赖项完整 - - // 如果正在加载过滤器或 props 指示隐藏,则不渲染 - if (isLoading) { - return ; - } - if (filteredProps.hidden) { - return null; - } - - // 渲染最终组件 - return ( - <> - - {/* 假设 filteredProps 中包含 menus 数据 */} - - - ); -} - -// --- MenuItem 组件示例 --- -function MyMenuItem(props) { - const { itemKey, initialProps } = props; - const [actionSettings] = useActionSettings(`menuItem:${itemKey}`); - const [filterSettings] = useFilterSettings(`menuItem:${itemKey}`); - - const applyMenuItemPropsFilter = useApplyFilter('core:menu:item:props'); - const [filteredProps, setFilteredProps] = useState(initialProps); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - let isMounted = true; - const applyFilters = async () => { - setIsLoading(true); - try { - const result = await applyMenuItemPropsFilter(initialProps, { context: filterSettings, actionSettings }); - if (isMounted) setFilteredProps(result); - } catch (error) { - console.error(`应用菜单项 ${itemKey} props 过滤器时出错:`, error); - if (isMounted) setFilteredProps(initialProps); - } finally { - if (isMounted) setIsLoading(false); - } - }; - applyFilters(); - return () => { isMounted = false }; - }, [initialProps, applyMenuItemPropsFilter, filterSettings, actionSettings, itemKey]); - - - const handleClick = useCallback(() => { - // 点击菜单项时,通常是触发一个动作或打开设置 - app.eventManager.dispatchEvent('core:menu:item:click', { - source: { id: itemKey, type: 'user-interaction' }, - payload: { - // 传递相关信息 - actionSettings, - originalProps: props.initialProps - } - }); - - // 示例:如果菜单项用于打开设置 - if (actionSettings?.opensSettings) { - app.eventManager.dispatchEvent('core:settings:open', { - source: { id: itemKey }, - payload: { settingsType: 'menuItem', targetKey: itemKey, currentSettings: actionSettings } - }); - } - - }, [itemKey, actionSettings, props.initialProps, app.eventManager]); - - if (isLoading) return 加载中...; - if (filteredProps.hidden) return null; - - // 使用 Ant Design 的 Menu.Item 作为示例 - return ( - - {filteredProps.title || initialProps.title} - - ); -} - -// --- 全局过滤器文件示例 (global-filters.ts) --- -// 组合过滤器:为一个高层级过滤器组合多个细粒度过滤器的结果 -app.filterManager.addFilter('myComponent:props', async (initialProps, context) => { - let props = { ...initialProps }; // 从初始 props 开始 - - try { - // 1. 应用文本过滤器 - props.text = await app.filterManager.applyFilter('myComponent:props:text', props.text, context); - - // 2. 应用可见性过滤器 - props.hidden = await app.filterManager.applyFilter('myComponent:props:hidden', props.hidden, context); - - // 3. 应用菜单过滤器 - props.menus = await app.filterManager.applyFilter('myComponent:props:menus', props.menus || [], context); - - // ... 可以组合更多过滤器 - - } catch (error) { - console.error("组合过滤器 'myComponent:props' 执行出错:", error); - // 决定是返回部分修改的 props 还是原始 props,或抛出错误 - return initialProps; // 这里选择返回原始 props 作为安全回退 - } - - return props; // 返回组合结果 -}, { priority: 0 }); // 组合过滤器本身的优先级 - -// 单个细粒度过滤器示例 -app.filterManager.addFilter('myComponent:props:text', (currentText, context) => { - // 根据上下文修改文本 - if (context?.componentContext?.highlight) { - return `*${currentText}*`; - } - return currentText; -}, { priority: 10 }); - -app.filterManager.addFilter('myComponent:props:hidden', async (isHidden, context) => { - // 异步检查权限 - const hasPermission = await checkPermission('viewMyComponent', context.user); - return !hasPermission || isHidden; // 如果没有权限或原本就隐藏,则隐藏 -}, { priority: 5 }); // 优先级可以不同 -``` -*(注:提供了更完整的 React 组件示例,使用了假设的 hooks 和 Ant Design 组件,展示了异步过滤器应用、加载状态、错误处理以及组合过滤器模式)* - - -### 表格区块示例 - -```typescript -// table-filters.ts -const { addFilter, applyFilter } = app.filterManager; - -// 主过滤器:聚合表格区块所需的所有配置和数据 -addFilter('core:block:table', async (initialCtx) => { - const { settings, payload } = initialCtx; // 假设上下文包含设置和可能的载荷 - const ctx = { ...initialCtx, results: {} }; // 创建用于传递给子过滤器的上下文 - - try { - // 1. 获取表格自身的 Props (如 title, height, antd table props) - // 初始值可以是 settings 中的 tableProps 或一个空对象 - const initialTableProps = settings?.tableProps || {}; - ctx.results.props = await applyFilter('core:block:table:props', initialTableProps, ctx); - - // 2. 获取联动规则配置 - const initialLinkages = settings?.linkageRules || []; - ctx.results.linkageRules = await applyFilter('core:block:table:linkageRules', initialLinkages, ctx); - - // 3. 获取字段配置 (用于渲染列) - // 初始字段可能来自 settings 或 collection 定义 - const initialFields = settings?.fields || await getDefaultFields(settings?.collectionName); - ctx.results.fields = await applyFilter('core:block:table:fields', initialFields, ctx); - - // 4. 获取操作配置 (如行操作、工具栏按钮) - const initialActions = settings?.actions || getDefaultActions(settings?.collectionName); - ctx.results.actions = await applyFilter('core:block:table:actions', initialActions, ctx); - - // 5. 获取数据 (核心数据获取逻辑,可能涉及 API 调用) - // 数据过滤器可能需要字段和操作信息来进行查询优化 - const initialDataOptions = { fields: ctx.results.fields, actions: ctx.results.actions, settings }; - ctx.results.data = await applyFilter('core:block:table:data', null, { ...ctx, payload: initialDataOptions }); // 数据过滤器通常不接收初始数据 - - // 6. 获取设置菜单项 (用于配置面板) - const initialSettingOptions = settings?.settingOptions || getDefaultSettingOptions('table'); - ctx.results.settingOptions = await applyFilter('core:block:table:settingOptions', initialSettingOptions, ctx); - - } catch(error) { - console.error("处理 'core:block:table' 过滤器时出错:", error); - // 返回错误状态或部分结果,取决于需求 - return { error: error, ...ctx.results }; - } - - // 返回聚合后的所有配置和数据 - return ctx.results; -}); - - -// --- 细粒度过滤器示例 --- - -// 过滤表格 Props -addFilter('core:block:table:props', async (currentProps, ctx) => { - let props = { ...currentProps }; - - // a. 应用通用的区块 props 过滤器 (如标题、描述) - props = await applyFilter('core:block:common:props', props, ctx); - - // b. 处理表格特有的 props (如 antd Table 的 props) - // 示例:根据设置调整分页 - if (ctx.settings?.pagination === 'simple') { - props.pagination = { simple: true, ...props.pagination }; - } - // 示例:添加基于上下文的 CSS 类 - if (ctx.someContextFlag) { - props.className = `${props.className || ''} context-active`; - } - - // c. (可以废弃) 迁移旧的 props 结构? - // props = await applyFilter('deprecated:block:table:props:migration', props, ctx); - - return props; -}, { priority: 10 }); - - -// 过滤字段配置 -addFilter('core:block:table:fields', async (currentFields, ctx) => { - let fields = [...currentFields]; // 创建副本以避免修改原始数组 - - // a. 应用通用的字段处理过滤器 (可能处理数据类型、权限等) - fields = await applyFilter('core:fields', fields, ctx); - - // b. 应用表格特有的字段转换或添加 (例如,添加操作列) - if (ctx.results?.actions?.length > 0 && !fields.some(f => f.key === 'actions')) { - fields.push({ key: 'actions', title: '操作', type: 'actionColumn', /*...*/ }); - } - - // c. 根据联动规则隐藏/显示字段 - fields = applyLinkageRulesToFields(fields, ctx.results?.linkageRules); - - return fields; -}, { priority: 10 }); - - -// 过滤操作配置 -addFilter('core:block:table:actions', async (currentActions, ctx) => { - let actions = [...currentActions]; - - // a. 应用通用的操作处理过滤器 (权限、条件显示等) - actions = await applyFilter('core:actions', actions, ctx); - - // b. 表格特定操作调整 - // 示例:如果数据为空,禁用某些行操作 - if (ctx.results?.data?.length === 0) { - actions = actions.map(action => action.scope === 'row' ? { ...action, disabled: true } : action); - } - - return actions; -}, { priority: 10 }); - - -// 过滤数据获取逻辑 (重要!) -addFilter('core:block:table:data', async (initialValueIgnored, ctx) => { - const { settings, payload } = ctx; // payload 可能包含字段、过滤、排序等信息 - const { fields, linkageRules } = ctx.results; // 可以使用之前过滤器的结果 - - // 1. 构建查询参数 (基于 settings, payload, fields) - const queryParams = buildQuery(settings, payload, fields); - - // 2. 调用 API 获取数据 - let data = await fetchDataFromApi(settings.collectionName, queryParams); - - // 3. (可选) 应用数据转换过滤器 (例如,格式化日期、计算派生值) - data = await applyFilter('core:block:table:data:transform', data, ctx); - - // 4. (可选) 根据联动规则处理数据? (通常联动影响 UI 展示,而不是直接修改数据源) - - return data; -}, { priority: 5 }); // 数据获取通常优先级较高 - -// 过滤数据转换 (示例) -addFilter('core:block:table:data:transform', (currentData, ctx) => { - return currentData.map(row => ({ - ...row, - // 示例:将状态码转换为文本 - statusText: convertStatusCodeToText(row.status), - // 示例:格式化日期 - createdAtFormatted: formatDate(row.createdAt) - })); -}, { priority: 10 }); - - -// 过滤设置选项 -addFilter('core:block:table:settingOptions', (currentOptions, ctx) => { - let options = [...currentOptions]; - // 添加或修改表格特定的设置选项 - options.push({ key: 'enableRowSelection', type: 'switch', title: '启用行选择' }); - return options; -}, { priority: 10 }); -``` -*(注:这是一个更结构化的表格区块过滤器示例,展示了如何通过主过滤器聚合多个细粒度过滤器的结果,以及每个过滤器如何处理特定方面,如 props、字段、操作、数据获取和设置。强调了上下文传递和依赖关系。)* - -## 操作组件流程 - -```mermaid -flowchart TD - subgraph 操作组件 (例如按钮) - A[用户点击操作按钮] --> B{handleClick 函数} - B -- 准备上下文 ctx (含 payload, settings) --> C - C -- 分发动作事件 --> D(app.dispatchEvent('core:action:xxx:click', ctx)) - D -- Promise 返回 --> E{处理事件结果 (results, _stop, _errors)} - E -- 可能触发后续事件 (如刷新) --> F(app.dispatchEvent('core:block:yyy:refresh', ...)) - E -- 更新 UI (如按钮状态) --> G - end - - subgraph 事件监听器 (处理 action:xxx:click) - H[EventManager] --> I{查找匹配的监听器} - I -- 按优先级执行 --> J[监听器 1 (e.g., 验证)] - J -- (可能修改 ctx.results, 设置 _stop) --> K - K --> L[监听器 2 (e.g., 调用 API)] - L -- (可能修改 ctx.results) --> M - M --> N[...] - N -- 最终结果 --> H - H -- 返回最终 ctx.results --> D - end - - subgraph 数据流 (通过过滤器修改组件 Props - 在渲染时) - P[组件初始 Props] --> Q(applyFilter('core:action:xxx:props', Props, Ctx)) - Q -- 执行相关过滤器 --> R[过滤器 1 (e.g., 处理文本)] - R --> S[过滤器 2 (e.g., 处理显隐/禁用状态)] - S --> T[最终 Props] - T -- 应用到 --> U[操作组件 UI] - end - - style J fill:#ccf,stroke:#333,stroke-width:1px - style L fill:#ccf,stroke:#333,stroke-width:1px - style R fill:#f9f,stroke:#333,stroke-width:1px - style S fill:#f9f,stroke:#333,stroke-width:1px -``` -*(注:改进了流程图,更清晰地区分了事件处理流程(点击时)和过滤器流程(渲染时),并展示了它们如何交互影响组件。)* - -## 改进点 - -### 当前问题 - -1. **全局状态复杂性**:事件和过滤器都是全局的,增加了复杂性,使代码追踪困难。不知道单个操作中处理了多少过滤器可能导致意外行为。 - - **解决方案**:添加一致的约定和约束(例如,强制使用命名空间,限制过滤器副作用)。 - -2. **资源管理风险**:直接使用全局状态可能导致内存泄漏(未移除的监听器/过滤器)。 - - **解决方案**:提供 Hooks(如 `useAddFilter`, `useEventListener`)来自动进行资源清理。 - -3. **简单与复杂场景**:对于简单的数据转换,使用过滤器 API 可能过度设计。 - - **解决方案**:在文档中明确指出何时直接编写代码优于使用过滤器 API。 - -4. **过滤器设置与动作设置**:存储策略不清晰 - 应该存储在 UI schema 中还是分开存储? - - **解决方案**:考虑使用专用的过滤器(例如 `applyFilter('core:component:settings', ...)`)来聚合和处理设置,或者定义明确的存储约定。可能需要一个 `settings` 域。 - -### 未来增强 - -1. **类型安全**:改进 TypeScript 集成,为事件名称和载荷提供更好的自动完成和类型检查(例如,使用映射类型或代码生成)。 - -2. **文档**:为这两个系统创建带有真实世界示例的全面文档。 - -3. **性能监控**:添加工具来分析事件传播和过滤器执行性能(例如,测量耗时,检测长链)。 - -4. **测试实用程序**:开发专门的测试实用程序,用于模拟事件和验证过滤器链。 - -5. **事件可视化**:创建开发者工具来可视化事件流和过滤器链,以便调试。 - -6. **中间件支持**:为日志记录、验证和错误处理等横切关注点添加中间件能力。 - -7. **标准化事件 Schema**:进一步细化事件 schema,确保整个应用程序的一致性。 - -8. **过滤器组合**:改进组合和重用过滤器以进行复杂转换的方法(例如,提供组合函数)。 - -9. **条件事件处理**:增强对基于应用程序状态的条件事件处理的支持。 - -10. **错误恢复**:为失败的事件处理程序和过滤器实现健壮的错误恢复机制。 \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-event-filter-system/design.md b/packages/plugins/@nocobase/plugin-event-filter-system/design.md deleted file mode 100644 index c1d1968ff4..0000000000 --- a/packages/plugins/@nocobase/plugin-event-filter-system/design.md +++ /dev/null @@ -1,1016 +0,0 @@ -# Event Filter System Design - -## Introduction - -The Event Filter System plugin integrates two core systems in NocoBase: - -1. **Event System**: Manages event publishing and subscriptions throughout the application. -2. **Filter System**: Provides data transformation capabilities across various data types. - -This document outlines the design principles, architecture, and implementation details of these systems. - -## System Flow Diagrams - -### Event System Flow - -```mermaid -flowchart TD - A[Component Event Trigger] -->|dispatches| B[EventManager] - B -->|routes to| C[Event Listeners] - C -->|filter by| D{Filter Function} - D -->|not matching| E[Skip Listener] - D -->|matching| F[Execute Listener] - F -->|may trigger| G[Subsequent Events] - F -->|returns| H[Result Collection] - H -->|resolves| I[Original Promise] - - subgraph Options - J[priority] - K[once] - L[blocking] - M[uischema] - N[filter function] - end - - C -.-> Options -``` - -### Filter System Flow - -```mermaid -flowchart TD - A[Component] -->|calls| B[applyFilter] - B -->|finds matching| C[Registered Filters] - C -->|sorted by| D[Priority] - D -->|processes| E[Input Data] - E -->|transforms through| F[Filter Chain] - F -->|returns| G[Modified Data] - G -->|back to| A - - subgraph Registration - H[addFilter] -->|registers| I[New Filter] - I -->|with| J[Name] - I -->|with| K[Filter Function] - I -->|with| L[Options] - end -``` - -### Component-Event-Filter Integration - -```mermaid -flowchart LR - A[UI Component] -->|user action| B[Event Trigger] - B -->|dispatches| C[EventManager] - C -->|routes to| D[Event Handlers] - D -->|may call| E[FilterManager] - E -->|transforms| F[Data] - F -->|returns to| D - D -->|updates| A - - G[Other Components] -->|register| H[Event Listeners] - H -.->|connect to| D - - I[Plugin] -->|registers| J[Filters] - J -.->|connect to| E -``` - -## Core Components - -### Filter API - -The Filter API provides mechanisms for data transformation with enhanced customization capabilities through `addFilter`. - -#### FilterManager - -```typescript -// Define the expected signature for a filter function -// It can be synchronous or return a Promise -type FilterFunction = (currentValue: any, ...contextArgs: any[]) => any | Promise; - -// Define options for adding a filter -interface FilterOptions { - priority?: number; // Higher numbers run later -} - -class FilterManager { // Singleton, typically accessed via app.filterManager - // Adds a filter function for a given name. - addFilter(name: string, filter: FilterFunction, options?: FilterOptions): () => void; // Returns an unregister function - - // Removes a specific filter function or all filters for a name. - removeFilter(name: string, filter?: FilterFunction): void; - - // Applies all registered filters for a name to an initial value. - // ALWAYS returns a Promise. - applyFilter(name: string, initialValue: any, ...contextArgs: any[]): Promise; -} -``` - -**Note on Asynchronicity:** This API is designed to be fully asynchronous to support filters that need to perform async operations (e.g., API calls). `applyFilter` *always* returns a `Promise`, even if all registered filters are synchronous. Callers *must* use `await` or `.then()` to get the final result. - -#### Filter Naming Convention - -Filter names use a structured, hierarchical format similar to filters to ensure clarity and prevent collisions: - -`origin:[domain]:[sub-module/component]:attribute` - -- **`origin`**: (Required) Identifies the source. - - `core`: For filters originating from the NocoBase core. - - `plugin:[plugin-name]`: For filters from a specific plugin (e.g., `plugin:workflow`). -- **`domain`**: (Required) A high-level functional area (e.g., `block`, `collection`, `field`, `ui`, `data`, `auth`). **Note:** For common, unambiguously UI-related components (e.g., `modal`, `button`, `table`, `form`), the `ui` domain *may* be omitted for brevity if the component name clearly implies it. For other domains or less common components, the domain should be included. -- **`sub-module/component`**: (Optional but Recommended) A more specific component or context (e.g., `table`, `form`, `users`, `props`). -- **`attribute`**: (Required) The specific data aspect being filtered (e.g., `props`, `data`, `schema`, `options`, `height`, `validationRules`). - -**Rationale:** While this format can lead to longer names, the explicitness is crucial for: - -1. **Clarity**: Ensuring that the filter's origin and purpose are clear. -2. **Collision Avoidance**: Preventing naming conflicts between different filters. -3. **Debugging**: Facilitating easier debugging and maintenance. -4. **Scalability**: Allowing for future expansion without breaking existing code. -5. **Use Constants**: Using constants for filter names can help maintain consistency across the application. - -**Examples:** - -- `core:block:table:props`: Filter props for a core table block. -- `core:collection:users:schema`: Filter the schema for the core `users` collection. -- `plugin:workflow:node:approval:conditions`: Filter approval conditions for a node in the `workflow` plugin. -- `plugin:map:field:coordinates:format`: Filter the display format for a coordinates field from the `map` plugin. -- `core:field:text:validationRules`: Filter validation rules for a core text field. - -Registration requires using this full, structured name. Wildcards are not supported during registration. - -#### Methods - -##### addFilter - -```typescript -app.filterManager.addFilter( - name: string, - filter: FilterFunction, // Can be sync or async - options?: FilterOptions -): () => void // Returns an unregister function -``` - -**Parameters**: -- `name`: Filter name, following the convention `origin:[domain]:[sub-module]:attribute`. Wildcards are not supported. -- `filter`: The filter function. It receives the current value (output of the previous filter or the `initialValue` for the first filter) as its first argument, and any `contextArgs` passed to `applyFilter` as subsequent arguments. It can return the transformed value directly or a `Promise` resolving to the transformed value. -- `options`: Configuration options, currently only `priority` (default 0). Filters with lower priority run first. - -**Returns**: -- A function that, when called, unregisters this specific filter. - -##### removeFilter - -```typescript -app.filterManager.removeFilter(name: string, filter?: FilterFunction): void -``` - -**Parameters**: -- `name`: Filter name, following the convention. -- `filter`: (Optional) The specific filter function to remove. If omitted, *all* filters registered for `name` are removed. - -##### applyFilter - -```typescript -async applyFilter( - name: string, - initialValue: any, - ...contextArgs: any[] -): Promise -``` - -**Parameters**: -- `name`: The name of the filter chain to apply. -- `initialValue`: The starting value that will be passed as the first argument to the first filter function in the chain. -- `...contextArgs`: (Optional) Any additional arguments provided here will be passed as the second, third, etc., arguments to *every* filter function in the chain, after the `currentValue`. - -**Execution Flow**: -1. Retrieves all filter functions registered for `name`. -2. Sorts them based on `priority` (lower priority first). -3. Sequentially executes each filter function, passing the result of the previous filter (or `initialValue` for the first) and the `contextArgs`. -4. **Crucially:** If a filter function returns a `Promise`, the manager `await`s its resolution before proceeding to the next filter. - -**Returns**: -- A `Promise` that resolves with the final value after being processed by all matching filters in the chain. -- If any filter throws or rejects, the Promise is **rejected** with a contextualized error (see Error Handling section). -- **Breaking Change:** Callers must now use `await` or `.then()` and include `try...catch` blocks to handle potential rejections. - -#### Examples - -**Recommended Pattern: Filtering Component Props** - -Instead of chaining `applyFilter` calls within filters, it's often clearer to register multiple filters for the *same* top-level name (e.g., `core:block:table:props`) and use `priority` and `contextArgs` to manage the transformation. - -**Why Nesting `applyFilter` is Discouraged:** - -While technically possible, calling `app.filterManager.applyFilter` from *within* a filter function passed to `addFilter` is strongly discouraged for several reasons: - -1. **Bypasses Central Control:** The `FilterManager`'s primary role is to orchestrate the execution of filters based on their registered `priority`. When a filter function internally calls `applyFilter` for another name (or even the same name), that nested execution happens *outside* the main priority-sorted sequence managed by the initial `applyFilter` call. This bypasses the intended central control mechanism. -2. **Obscures Execution Flow:** It becomes much harder to understand the complete sequence of transformations applied for a given filter name (e.g., `core:block:table:props`) just by looking at the registered filters and their priorities. The effective flow depends on the internal logic of potentially many different filter functions. -3. **Increases Debugging Complexity:** Tracing how a value was transformed becomes more difficult. You can't rely solely on the `FilterManager`'s sorted list; you need to step into the code of each filter function to see if it triggers other, nested filter chains. - -**The Recommended Approach (Multiple Filters, Same Name):** - -- Register multiple, independent filter functions for the same broad filter name. -- Use `priority` to precisely control the order of execution. -- Pass necessary contextual data via the `contextArgs` of the main `applyFilter` call. - -This approach keeps individual filter functions focused, makes the overall transformation sequence explicit and centrally managed by the `FilterManager` based on priority, and simplifies debugging. - -```typescript -// Filter 1: Adjust table height based on context (Synchronous) -app.filterManager.addFilter('core:block:table:props', (props, context) => { - if (context?.compact) { - // Ensure props object is mutable or create a new one - const newProps = { ...props }; - newProps.height = Math.min(props.height || 600, 300); - return newProps; - } - return props; // Return original or potentially modified props -}, { priority: 10 }); // Runs relatively early - -// Filter 2: Add CSS class based on async user role check (Asynchronous) -app.filterManager.addFilter('core:block:table:props', async (props, context) => { - // Assume context.currentUser exists, but we need to fetch role details - const userRole = await fetchUserRole(context.currentUser.id); // Example async call - - if (userRole === 'admin') { - const newProps = { ...props }; - newProps.className = `${props.className || ''} admin-table`; - return newProps; - } - return props; -}, { priority: 20 }); // Runs after the height adjustment - -// --- Component Usage --- - -const MyTableComponent = (/* ... */) => { - const initialProps = useMemo(() => ({ /* base props */ }), [/* deps */]); - const context = useMemo(() => ({ /* context */ }), [/* deps */]); - const [filteredProps, setFilteredProps] = useState(initialProps); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - let isMounted = true; - const loadFilteredProps = async () => { - setIsLoading(true); - try { - // MUST await the result - const result = await app.filterManager.applyFilter( - 'core:block:table:props', - initialProps, - context - ); - if (isMounted) { - setFilteredProps(result); - } - } catch (error) { - console.error("Error applying filters:", error); - // Handle error appropriately - } finally { - if (isMounted) { - setIsLoading(false); - } - } - }; - - loadFilteredProps(); - - return () => { isMounted = false; }; // Cleanup - }, [initialProps, context]); - - if (isLoading) { - return ; // Show loading indicator while filters run - } - - return
; -} -``` - -**Explanation of Example:** -- The first filter remains synchronous. -- The second filter is now `async` and uses `await` to fetch user roles. -- **Crucially**, the component usage (`MyTableComponent`) now uses `useEffect` and `async`/`await` to call `applyFilter` and handle the resulting `Promise`. It also includes loading and error handling state. -- **Note:** Filter functions should be mindful of object mutability. Returning a new object (`{...props}`) is often safer than modifying the input directly, unless mutation is explicitly intended and documented for that filter chain. - -### Hooks - -#### useAddFilter - -```typescript -const useAddFilter = (name:string, filterFunction, options) => { - useEffect(() => { - return app.filterManager.addFilter(name, filterFunction, options); - }, [...]); -} -``` - -#### useApplyFilter - -```typescript -// This hook might need adjustment or a new async version depending on usage patterns. -// A simple version might just return the function without immediate execution. -const useApplyFilter = (name: string) => { - // Note: This returns the async function itself. - // The component using this hook would need to call it with await. - return useCallback(async (initialValue: any, ...contextArgs: any[]) => { - return app.filterManager.applyFilter(name, initialValue, ...contextArgs); - }, [app, name]); // Ensure app is stable if passed via context -} -``` - -### Event API - -The Event API manages event subscriptions and publications throughout the application. - -#### EventManager - -```typescript -class EventManager { // 单例, 可以挂载到app上 - // 注册事件监听器 - on(eventName, listener, options = {}) { ... } - - // 注册一次性事件监听器 - once(eventName, listener, options = {}) { ... } - - // 取消事件监听器 - off(eventName, listener) { ... } - - // 触发事件并收集结果 - // ALWAYS returns a Promise resolving to the ctx.results object. - async dispatchEvent(eventName: string | string[], ctx: EventContext): Promise>; // Returns ctx.results - - // 获取特定事件的所有监听器, 主要用于测试 - getListeners(eventName) { ... } -} -``` - -#### Event Naming Convention - -Event names follow a structured, hierarchical format similar to filters to ensure clarity and prevent collisions: - -`origin:[domain]:[sub-module/component]:action` - -- **`origin`**: (Required) `core` or `plugin:[plugin-name]`. -- **`domain`**: (Required) High-level area (e.g., `block`, `collection`, `ui`, `system`, `auth`). **Note:** For common, unambiguously UI-related components (e.g., `modal`, `button`, `table`, `form`), the `ui` domain *may* be omitted for brevity if the component name clearly implies it. For other domains or less common components, the domain should be included. -- **`sub-module/component`**: (Optional but Recommended) Specific context (e.g., `table`, `form`, `modal`, `row`, `users`). -- **`action`**: (Required) Describes the event, often combining the action verb with lifecycle prefixes/suffixes. Examples: `beforeCreate`, `afterUpdate`, `loadSuccess`, `validateError`, `submit`, `afterSubmit`, `click`, `open`. - -**Rationale & Recommendation:** Same as for Filter Naming Convention (clarity, collision avoidance, debugging, scalability, use constants). - -**Examples:** - -- `core:collection:posts:beforeCreate` // Before creating a record in the core `posts` collection. -- `core:block:table:row:select` // A row is selected in a core table block. -- `core:modal:open` // A generic core modal is opened (omitting `ui` domain). -- `core:form:afterSubmit` // After a generic form submission (using combined action). -- `core:auth:login:success` // Successful user login event. -- `plugin:workflow:execution:start` // A workflow execution starts (from `workflow` plugin). -- `plugin:audit-log:entry:created` // An audit log entry was created (from `audit-log` plugin). - -Listeners can potentially listen using wildcards (e.g., `core:block:table:**`), although dispatching must use a specific event name. (Note: Wildcard support needs confirmation based on `EventManager` implementation details). - -**Listener Wildcard Support:** - -Event listeners registered via `on()` and `once()` **support the use of wildcards** (`*`) in the `eventName` string to match multiple events. The wildcard `*` matches exactly one segment in the event name hierarchy. - -- Example: `core:collection:*:afterCreate` would match `core:collection:posts:afterCreate`, `core:collection:users:afterCreate`, etc. -- Example: `plugin:workflow:*:start` would match `plugin:workflow:execution:start`, `plugin:workflow:node:start`, etc. -- Example: `core:*:*:select` would match `core:block:table:select`, `core:field:user:select`, etc. - -**Note:** -- `dispatchEvent` **must** always use a specific, non-wildcard event name. -- Using many widespread wildcards (e.g., `core:**:**`) might have performance implications, as the `EventManager` needs to perform pattern matching for each dispatched event. - -Listeners can potentially listen using wildcards (e.g., `core:block:table:**`), although dispatching must use a specific event name. (Note: Wildcard support needs confirmation based on `EventManager` implementation details). - -#### EventContext - -```typescript -interface EventContext { - // 事件源 (dispatchEvent调用者) 信息 - // Listeners SHOULD treat this as read-only. - source?: { - id?: string; - type?: string; // 'user-interaction', 'system', 'workflow' - [key: string]: any // any extra information - }; - - // 用于指定接收者信息, 主要用于精准触发事件 - // Listeners SHOULD treat this as read-only. - target?: { - id?: string; // id - uischema?: ISchema; // ui schema - } - - // 事件相关数据 - // Listeners SHOULD treat this as read-only by convention. - // Use ctx.results for output. - payload?: T; - - // 元数据 - // Listeners SHOULD treat this as read-only. - meta?: { - timestamp?: number; // 事件触发的时间 - userId?: string; // 当前用户ID (可选) - event?: string | string[]; // 事件名, 可以通配符匹配 table:*, 可支持多个eventName组成的数组 - [table:refresh, form:refresh] - }; - - // 用于收集事件监听器的输出结果 - // Initialized as {} by dispatchEvent. Listeners add properties here. - // Can also include reserved control flags like `_stop` or `_errors`. - results: Record; - - // TODO: 是否需要支持事件中断执行? - // propagation: true; // 可能改成propagation.stop()是否停止继续执行 -} -``` - -**Listener Guidelines:** - -* **Context Immutability:** Listeners should treat the incoming context properties (`ctx.source`, `ctx.target`, `ctx.payload`, `ctx.meta`) as **read-only**. Modifying these shared objects directly can lead to unpredictable side effects for other listeners or the event dispatcher. -* **Providing Results:** To communicate results or data back to the caller or potentially other listeners (depending on execution order), listeners should **add properties** to the `ctx.results` object. The final state of this object is returned by `dispatchEvent`. -* **Stopping Propagation:** A listener can prevent subsequent listeners (for the current dispatch) from running by setting `ctx.results._stop = true`. The `dispatchEvent` promise will still resolve normally with the results collected up to that point. -* **Overwriting Results:** Be aware that if multiple listeners write to the same property key within `ctx.results`, the value set by the listener that finishes last will prevail in the final returned object. - -#### Listener Options - -```typescript -interface EventListenerOptions { - priority?: number; // 监听器优先级,数值越大优先级越高,默认为 0 - once?: boolean; // 是否只执行一次,默认为 false - blocking?: boolean; // 是否为阻塞监听器,默认false,即默认行为是不需要当前监听器结束再执行后续监听器 - uischema?: ISchema; // Json Schema,主要用于精准触发事件 - filter?: (ctx: EventContext, options: EventListenerOptions) => boolean; // 过滤函数,允许根据上下文来确定当前监听器是否执行,默认行为是:处理多播(dispatch时未指定target)和单播(dispatch时指定当前组件为target)事件。 -} -``` - -Default filter function: -```typescript -/** - * Default filter function for event listeners. - * Determines if a listener should execute based on the event context (ctx) - * and the listener's options. - * - * Logic: - * 1. If the event dispatch did not include a target (ctx.target is falsy), - * the listener should always run (multicast). - * 2. If the event dispatch included a target (ctx.target exists): - * a. If the listener options include specific targeting criteria (e.g., options.uischema), - * the listener runs only if the target criteria match the context's target. - * b. If the listener options do NOT include specific targeting criteria, - * the listener runs (it accepts any targeted event). - */ -function defaultListenerFilter(ctx: EventContext, options: EventListenerOptions): boolean { - // 1. Multicast: No target specified in dispatch? Listener runs. - if (!ctx.target) { - return true; - } - - // 2. Unicast: Target specified in dispatch. Check listener options. - if (options.uischema) { - // Listener has uischema targeting. Run only if target uischema exists and matches. - // Note: Actual matching logic might be more complex (e.g., comparing specific keys like 'x-uid'). - // This example assumes a direct comparison or key comparison is intended. - const targetSchema = ctx.target.uischema; - // Basic check: both schemas exist and have matching 'x-uid' (example key) - return !!(targetSchema && options.uischema['x-uid'] && options.uischema['x-uid'] === targetSchema['x-uid']); - } else { - // Listener has no specific targeting options defined (like uischema). - // Therefore, it should run even for targeted dispatches. - return true; - } -} -``` - -#### Methods - -##### on - -```typescript -on(event: string | string[], listener: EventListener, options?: EventListenerOptions): Unsubscriber -``` - -Used to register event listeners. When an event matching the `event` name (or pattern, if using wildcards) is triggered, the listener will execute. - -**Parameters:** -- `event`: The specific event name, wildcard pattern (using `*`), or array of names/patterns to listen for. -- `listener`: The function to execute when the event occurs. -- `options`: Configuration for the listener (priority, blocking, etc.). - -**Returns:** An `Unsubscriber` function to remove this specific listener. - -##### once - -```typescript -once(event: string | string[], listener: EventListener, options?: OmitEventListenerOptions, 'once'): Unsubscriber -``` - -Similar to `on()` but the listener is automatically removed after the first execution of *any* event matching the `event` name or pattern. - -**Parameters:** Same as `on()`. - -**Returns:** An `Unsubscriber` function. - -##### off - -```typescript -off(eventName) // 取消事件所有的监听器 -off(eventName, listener) // 取消事件指定监听器 -``` - -Removes event listeners. - -##### dispatchEvent - -```typescript -// Note: The timeout parameter might need reconsideration with async results -app.eventManager.dispatchEvent( - eventName: string | string[], - ctx: EventContext - // timeout?: number // Timeout removed for simplicity with async results for now -): Promise>; // Resolves with ctx.results -``` - -**Parameters**: -- `eventName`: Event name or array of names. -- `ctx`: Event context object. The `EventManager` will ensure `ctx.results` exists before passing it to listeners. - -**Execution Flow**: -1. Ensures `ctx.results` exists and is an empty object. -2. Finds all listeners matching `eventName`. -3. Executes listeners based on priority, blocking options, and filter functions. -4. Handles `async` listeners appropriately, awaiting their completion if necessary. -5. Listeners may add properties to the shared `ctx.results` object during their execution. -6. **After each listener completes:** Checks if `ctx.results._stop === true`. If so, stops processing further listeners for this dispatch. -7. Catches errors from listeners and handles them according to the Error Handling strategy (populating `ctx.results._errors` for non-blocking, potentially rejecting the main promise for blocking failures). - -**Returns**: -- A `Promise` that resolves with the **final state of the `ctx.results` object** after all relevant listeners have completed execution *or* propagation was stopped via `ctx.results._stop`. This object contains accumulated outputs and potentially control flags (`_stop`, `_errors`). -- If a **blocking** listener fails, the Promise is **rejected** with a contextualized error. -- Callers need to handle potential rejections and check `ctx.results._errors` and `ctx.results._stop`. - -## Error Handling Strategy - -This section outlines how errors are handled within the Filter and Event systems. - -**1. Filter System (`applyFilter`)** - -* **Strategy:** Fail Fast with Context. -* **Behavior:** If any filter function within the chain throws an error or returns a rejected Promise, the `FilterManager` immediately stops processing the remaining filters for that specific `applyFilter` call. -* **Error Propagation:** The `Promise` returned by `applyFilter` is **rejected**. -* **Error Context:** The `FilterManager` catches the internal error and augments it (or wraps it) before rejection, providing crucial debugging information. The rejected error object will typically include: - * `filterName`: The full name of the filter that failed. - * `filterPriority`: The priority of the failing filter. - * `originalError`: The original error thrown by the filter function. -* **Logging:** The contextualized error is logged internally by the `FilterManager` (e.g., via `console.error`). -* **Rationale:** Filter chains are often critical for data integrity. Immediate failure provides a clear signal, and added context aids debugging. - -**2. Event System (`dispatchEvent`)** - -* **Strategy:** Collect Errors for Non-Blocking, Fail Fast for Blocking. -* **Behavior (Non-Blocking Listeners):** If a non-blocking listener (`blocking: false` or default) throws an error or returns a rejected Promise, the `EventManager` **does not** stop processing other listeners for that `dispatchEvent` call. -* **Behavior (Blocking Listeners):** If a blocking listener (`blocking: true`) throws an error or returns a rejected Promise, the `EventManager` **stops** processing subsequent listeners for that specific `dispatchEvent` call. -* **Error Collection (Non-Blocking Failures):** - * The `EventManager` wraps listener executions to catch errors. - * Errors from non-blocking listeners are caught and contextualized with details like `listener` identifier (if possible), `eventName`, and the `originalError`. - * These contextualized errors are added to a dedicated array within the results object: `ctx.results._errors = []`. (The `_errors` property is reserved for this purpose). -* **Error Propagation & Return Value:** - * If a **blocking** listener fails, the `Promise` returned by `dispatchEvent` is **rejected** with the contextualized error from that listener. - * If **no blocking** listener fails (even if non-blocking listeners failed), the `Promise` returned by `dispatchEvent` **resolves successfully** with the final `ctx.results` object. Callers **must** check `ctx.results._errors` to detect partial failures from non-blocking listeners. -* **Logging:** All caught errors (from both blocking and non-blocking listeners) are logged internally by the `EventManager` with their context. -* **Rationale:** This provides resilience for non-critical side effects (non-blocking listeners) while ensuring critical workflows (blocking listeners) fail clearly. Error collection allows awareness of partial failures. - -## Form Submission Flow Example - -```mermaid -sequenceDiagram - participant User - participant Form - participant EventManager - participant BeforeSubmitListeners - participant SubmitListeners - participant AfterSubmitListeners - - User->>Form: Click Submit - Form->>EventManager: dispatchEvent(':beforeSubmit', ctx) - EventManager->>BeforeSubmitListeners: route to handlers - Note over BeforeSubmitListeners: Handlers may add to ctx.results - BeforeSubmitListeners-->>EventManager: complete - EventManager-->>Form: return Promise - Form->>Form: process results (e.g., check ctx.results.validation) - alt if validation failed or stop requested via ctx.results - Form-->>User: Show feedback (stop) - else proceed with submission - Form->>EventManager: dispatchEvent('form:submit', ctx) - EventManager->>SubmitListeners: route to handlers - Note over SubmitListeners: Handlers may add to ctx.results - SubmitListeners-->>EventManager: complete - EventManager-->>Form: return Promise - Form->>Form: process results - Form->>EventManager: dispatchEvent('form:afterSubmit', ctx) - EventManager->>AfterSubmitListeners: route to handlers - Note over AfterSubmitListeners: Handlers may add to ctx.results - AfterSubmitListeners-->>EventManager: complete - EventManager-->>Form: return Promise - Form-->>User: Show success feedback - end -``` - -## Design Considerations - -### Current Implementation Challenges - -1. **Object Immutability**: Currently `source`, `target`, `meta` properties can only be frozen using `Object.freeze`, but payload modification is not restricted. Should more attributes be protected? - -2. **Async Event Collection**: Determining whether `dispatchEvent` should collect and return values from handlers. If return values are needed, should they be from all handlers or specific ones? - -3. **Performance Optimization**: - - Preventing event storms - - Implementing debounce mechanisms - -4. **Error Handling**: Considering how to handle errors in event listeners reliably - -5. **Event Chaining**: Supporting complex event flows and preventing unintended circular dependencies - -### Integration Points - -1. **Component Actions**: - ```jsx - - - // 内核已经监听form:record:create事件 - app.on('form:record:create', (ctx) => { - openCreationModal(ctx); - }); - ``` - -2. **Form Submission Flow**: - ```jsx - // block-form.ts - async function submit() { - const beforeSubmitCtx = { - results: {}, - source: { - id: actionId - } - ... - }; - // Dispatch and get results - const beforeSubmitResults = await app.eventManager.dispatchEvent(':beforeSubmit', beforeSubmitCtx); - // Check results for stop conditions or validation failures - if (beforeSubmitResults.stopProcessing || beforeSubmitResults.validationFailed) { - return; - } - // 继续提交 - const submitResults = await app.eventManager.dispatchEvent('form:submit', ctx); - await app.eventManager.dispatchEvent('form:afterSubmit', ctx); - } - - // event-flow.ts - app.eventManager.on('form.beforeSubmit', (ctx) => { - await openDialog(ctx); - await openDialog(ctx); - }, { - filter: (eventCtx, listenerOptions) => { - return eventCtx.source.id === listenerOptions.id; - }, - id: actionId - }); - ``` - -3. **Table Component Integration**: - ```jsx - - const refresh = () => { - console.log('refresh table1 data'); - }; - app.eventManager.on('table:data:refresh', ()=> { - refresh(); - }, { - target: { - uischema: { 'x-uid': 'table1' } - } - }); - - - const refresh = () => { - console.log('refresh table1 data'); - }; - app.eventManager.on('table:data:refresh', ()=> { - refresh(); - }, { - target: { - uischema: { 'x-uid': 'table2' } - } - }); - -