📋 目录1. 核心问题:如何设计可扩展的功能块系统2. 解决方案:策略模式 + 注册表机制3. 架构设计:统一的Handler接口规范4. 核心实现一:功能块注册表5. 核心实现二:元数据驱动的UI生成6. 核心实现三:参数验证与默认值填充7. 核心实现四:热更新机制8. 核心实现五:BlocksGroup组合与复用9. 进阶优化:性能监控与懒加载10. 最容易踩的5个坑11. 功能测试清单12. 经验总结1. 核心问题:如何设计可扩展的功能块系统1.1 功能块系统的挑战在Automa中,我们需要支持53种不同的功能块:基础操作类(15种): • Click(点击元素) • Type Text(输入文本) • Scroll(滚动页面) • Hover(悬停) • ... 数据处理类(12种): • Extract Data(提取数据) • Export CSV(导出CSV) • JSON Parse(JSON解析) • ... 控制流类(10种): • Loop(循环) • Condition(条件分支) • Delay(延迟) • ... 网络请求类(8种): • HTTP Request(HTTP请求) • Fetch API(Fetch调用) • WebSocket(WebSocket通信) • ... 浏览器扩展类(8种): • Get Tab(获取标签页) • Create Tab(创建标签页) • Execute Script(执行脚本) • ... 用户痛点: ⚠️ 每新增一个功能块,需要修改多处代码 ⚠️ 不同功能块的参数格式不统一 ⚠️ UI配置项硬编码,维护困难 ⚠️ 缺少统一的错误处理机制 ⚠️ 功能块之间的依赖关系混乱1.2 工程级挑战假设要新增一个"截图"功能块: 传统做法(无系统设计): 1. 在编辑器中添加节点类型 → 修改switch-case 2. 在执行引擎中添加处理逻辑 → 新增if-else分支 3. 在UI组件中添加配置表单 → 硬编码HTML表单 4. 在参数验证中添加规则 → 手动编写验证逻辑 5. 在文档中补充说明 → 手动更新README ❌ 问题: • 每次新增都要修改5+个文件 • 容易遗漏某个环节导致bug • 代码耦合度高,难以维护 • 新人上手成本高 采用插件化架构后: ✅ 只需创建一个handler文件(约100行) ✅ 注册到BlockRegistry即可自动生效 ✅ UI根据元数据自动生成 ✅ 参数验证、错误处理全部自动化 ✅ 新人只需关注业务逻辑 核心价值:开闭原则(对扩展开放,对修改关闭)2. 解决方案:策略模式 + 注册表机制2.1 设计模式选择/** * 为什么选择策略模式? * * 场景特征: * 1. 多种"策略"(每个功能块是一种策略) * 2. 策略之间可以互相替换(运行时动态选择) * 3. 需要统一管理所有策略 * * 对比其他模式: * • 工厂模式:适合创建对象,但不适合管理行为 * • 观察者模式:适合事件通知,不适合功能封装 * • 适配器模式:适合接口转换,不适合功能扩展 */// 策略模式的核心结构interfaceBlockStrategy{// 初始化(只执行一次)init(params:BlockParams):void;// 执行(每次工作流运行都调用)execute(context:ExecutionContext):PromiseBlockResult;// 销毁(清理资源)destroy():void;}// 注册表管理classBlockRegistry{privateregistry:Mapstring,BlockStrategy=newMap();register(blockId:string,strategy:BlockStrategy):void{this.registry.set(blockId,strategy);}get(blockId:string):BlockStrategy|undefined{returnthis.registry.get(blockId);}}2.2 整体架构图功能块开发者创建Handler文件实现init/execute/destroy定义元数据metadata注册到BlockRegistry工作流编辑器工作流执行引擎根据metadata生成UI运行时调用execute用户配置参数返回执行结果2.3 目录结构设计src/blocks/ ├── index.js # 统一导出 ├── BlockRegistry.js # 注册表核心 ├── BaseBlock.js # 基类(可选) │ ├── handlers/ # 具体功能块实现 │ ├── handlerClick.js # 点击块 │ ├── handlerTypeText.js # 输入块 │ ├── handlerExtractData.js # 数据提取块 │ ├── handlerHTTPRequest.js # HTTP请求块 │ ├── handlerLoop.js # 循环块 │ ├── handlerCondition.js # 条件块 │ └── ... (共53个) │ ├── metadata/ # 元数据定义 │ ├── click.json # 点击块元数据 │ ├── typeText.json # 输入块元数据 │ └── ... (共53个) │ └── BlocksGroup/ # 组合块 ├── index.js └── blocksGroupHelper.js3. 架构设计:统一的Handler接口规范3.1 Handler接口定义/** * 功能块Handler的标准接口规范 * 所有功能块必须遵循此规范 */classBaseBlock{/** * 构造函数(可选) * @param {Object} options - 配置选项 */constructor(options={}){this.options=options;this.state='idle';// idle | running | completed | failed}/** * 初始化阶段(工作流启动时调用一次) * @param {Object} params - 用户配置的参数 * @returns {void} */init(params){// 子类实现thrownewError('Must implement init()');}/** * 执行阶段(每次节点被触发时调用) * @param {ExecutionContext} context - 执行上下文 * @returns {PromiseObject} - 执行结果 */asyncexecute(context){thrownewError('Must implement execute()');}/** * 销毁阶段(工作流结束时调用) * @returns {void} */destroy(){// 清理资源、取消定时器等}/** * 获取块的状态 * @returns {string} */getState(){returnthis.state;}/** * 设置块的状态 * @param {string} newState */setState(newState){this.state=newState;}}module.exports=BaseBlock;3.2 执行上下文(ExecutionContext)/** * 执行上下文:传递给每个功能块的execute方法 * 提供统一的API访问入口 */classExecutionContext{constructor({workflow,node,globalData}){// 工作流级别的变量this.globalData=globalData||{};// 当前节点的配置this.node=node;// 工作流实例引用this.workflow=workflow;// 循环相关数据this.loopData=null;// 表格数据this.table={};// 之前节点的输出this.previousOutputs=newMap();// 日志记录器this.logger=newLogger(node.id);}/** * 获取变量(支持模板字符串插值) * @param {string} key - 变量名或模板字符串 * @returns {*} */getVariable(key){if(typeofkey==='string'key.includes('{ {')){returnthis.interpolate(key);}returnthis.resolvePath(key);}/** * 模板字符串插值 * @param {string} template - 如 "Hello { {user.name}}" * @returns {string} */interpolate(template){returntemplate.replace(/\{\{([^}]+)\}\}/g,(match,path)={constvalue=this.resolvePath(path.trim());returnvalue!==undefined?value:match;});}/** * 解析路径(支持嵌套属性访问) * @param {string} path - 如 "user.name" * @returns {*} */resolvePath(path){constkeys=path.split('.');letresult=this.globalData;for(constkeyofkeys){if(result===undefined||result===null){returnundefined;}result=result[key];}returnresult;}/** * 等待元素出现 * @param {string} selector - CSS选择器 * @param {Object} options - 配置选项 * @returns {PromiseElement} */asyncwaitForSelector(selector,options={}){consttimeout=options.timeout||5000;constinterval=options.interval||100;conststartTime=Date.now();while(Date.now()-startTimetimeout){constelement=document.querySelector(selector);if(element){returnelement;}awaitthis.sleep(interval);}thrownewError(`Element not found:${selector}`);}/** * 休眠 * @param {number} ms - 毫秒数 * @returns {Promisevoid} */sleep(ms){returnnewPromise(resolve=setTimeout(resolve,ms));}/** * 保存变量到全局作用域 * @param {string} key * @param {*} value */setGlobalVariable(key,value){this.globalData[key]=value;}/** * 获取前驱节点的输出 * @param {string} nodeId - 节点ID * @returns {Object} */getPreviousOutput(nodeId){returnthis.previousOutputs.get(nodeId);}}module.exports=ExecutionContext;4. 核心实现一:功能块注册表4.1 BlockRegistry核心实现/** * 功能块注册表 * 管理所有已注册的功能块 */classBlockRegistry{constructor(){// 注册表:blockId - handler实例this.registry=newMap();// 元数据:blockId - metadatathis.metadata=newMap();// 分类索引:category - [blockId]this.categories=newMap();// 是否已初始化this.initialized=false;}/** * 注册功能块 * @param {string} blockId - 功能块唯一标识 * @param {BaseBlock} handlerClass - Handler类 * @param {Object} metadata - 元数据 */register(blockId,handlerClass,metadata){if(this.registry.has(blockId)){console.warn(`Block${blockId}already registered, overwriting`);}// 存储元数据this.metadata.set(blockId,metadata);// 创建handler实例并注册consthandler=newhandlerClass();this.registry.set(blockId,handler);// 更新分类索引constcategory=metadata.category||'general';if(!this.categories.has(category)){this.categories.set(category,[]);}this.categories.get(category).push(blockId);console.log(`✓ Registered block:${blockId}(${metadata.label})`);}/** * 获取功能块handler * @param {string} blockId * @returns {BaseBlock|undefined} */get(blockId){returnthis.registry.get(blockId);}/** * 获取功能块元数据 * @param {string} blockId * @returns {Object|undefined} */getMetadata(blockId){returnthis.metadata.get(blockId);}/** * 获取所有功能块列表 * @returns {Array} */getAll(){constblocks=[];for(const[blockId,handler]ofthis.registry.entries()){constmetadata=this.metadata.get(blockId);blocks.push({id:blockId,handler,metadata});}returnblocks;}/** * 按分类获取功能块 * @param {string} category * @returns {Array} */getByCategory(category){constblockIds=this.categories
前端——Automa 功能块管理系统:从零实现生产级插件化架构(策略模式+注册表+热更新)
📋 目录1. 核心问题:如何设计可扩展的功能块系统2. 解决方案:策略模式 + 注册表机制3. 架构设计:统一的Handler接口规范4. 核心实现一:功能块注册表5. 核心实现二:元数据驱动的UI生成6. 核心实现三:参数验证与默认值填充7. 核心实现四:热更新机制8. 核心实现五:BlocksGroup组合与复用9. 进阶优化:性能监控与懒加载10. 最容易踩的5个坑11. 功能测试清单12. 经验总结1. 核心问题:如何设计可扩展的功能块系统1.1 功能块系统的挑战在Automa中,我们需要支持53种不同的功能块:基础操作类(15种): • Click(点击元素) • Type Text(输入文本) • Scroll(滚动页面) • Hover(悬停) • ... 数据处理类(12种): • Extract Data(提取数据) • Export CSV(导出CSV) • JSON Parse(JSON解析) • ... 控制流类(10种): • Loop(循环) • Condition(条件分支) • Delay(延迟) • ... 网络请求类(8种): • HTTP Request(HTTP请求) • Fetch API(Fetch调用) • WebSocket(WebSocket通信) • ... 浏览器扩展类(8种): • Get Tab(获取标签页) • Create Tab(创建标签页) • Execute Script(执行脚本) • ... 用户痛点: ⚠️ 每新增一个功能块,需要修改多处代码 ⚠️ 不同功能块的参数格式不统一 ⚠️ UI配置项硬编码,维护困难 ⚠️ 缺少统一的错误处理机制 ⚠️ 功能块之间的依赖关系混乱1.2 工程级挑战假设要新增一个"截图"功能块: 传统做法(无系统设计): 1. 在编辑器中添加节点类型 → 修改switch-case 2. 在执行引擎中添加处理逻辑 → 新增if-else分支 3. 在UI组件中添加配置表单 → 硬编码HTML表单 4. 在参数验证中添加规则 → 手动编写验证逻辑 5. 在文档中补充说明 → 手动更新README ❌ 问题: • 每次新增都要修改5+个文件 • 容易遗漏某个环节导致bug • 代码耦合度高,难以维护 • 新人上手成本高 采用插件化架构后: ✅ 只需创建一个handler文件(约100行) ✅ 注册到BlockRegistry即可自动生效 ✅ UI根据元数据自动生成 ✅ 参数验证、错误处理全部自动化 ✅ 新人只需关注业务逻辑 核心价值:开闭原则(对扩展开放,对修改关闭)2. 解决方案:策略模式 + 注册表机制2.1 设计模式选择/** * 为什么选择策略模式? * * 场景特征: * 1. 多种"策略"(每个功能块是一种策略) * 2. 策略之间可以互相替换(运行时动态选择) * 3. 需要统一管理所有策略 * * 对比其他模式: * • 工厂模式:适合创建对象,但不适合管理行为 * • 观察者模式:适合事件通知,不适合功能封装 * • 适配器模式:适合接口转换,不适合功能扩展 */// 策略模式的核心结构interfaceBlockStrategy{// 初始化(只执行一次)init(params:BlockParams):void;// 执行(每次工作流运行都调用)execute(context:ExecutionContext):PromiseBlockResult;// 销毁(清理资源)destroy():void;}// 注册表管理classBlockRegistry{privateregistry:Mapstring,BlockStrategy=newMap();register(blockId:string,strategy:BlockStrategy):void{this.registry.set(blockId,strategy);}get(blockId:string):BlockStrategy|undefined{returnthis.registry.get(blockId);}}2.2 整体架构图功能块开发者创建Handler文件实现init/execute/destroy定义元数据metadata注册到BlockRegistry工作流编辑器工作流执行引擎根据metadata生成UI运行时调用execute用户配置参数返回执行结果2.3 目录结构设计src/blocks/ ├── index.js # 统一导出 ├── BlockRegistry.js # 注册表核心 ├── BaseBlock.js # 基类(可选) │ ├── handlers/ # 具体功能块实现 │ ├── handlerClick.js # 点击块 │ ├── handlerTypeText.js # 输入块 │ ├── handlerExtractData.js # 数据提取块 │ ├── handlerHTTPRequest.js # HTTP请求块 │ ├── handlerLoop.js # 循环块 │ ├── handlerCondition.js # 条件块 │ └── ... (共53个) │ ├── metadata/ # 元数据定义 │ ├── click.json # 点击块元数据 │ ├── typeText.json # 输入块元数据 │ └── ... (共53个) │ └── BlocksGroup/ # 组合块 ├── index.js └── blocksGroupHelper.js3. 架构设计:统一的Handler接口规范3.1 Handler接口定义/** * 功能块Handler的标准接口规范 * 所有功能块必须遵循此规范 */classBaseBlock{/** * 构造函数(可选) * @param {Object} options - 配置选项 */constructor(options={}){this.options=options;this.state='idle';// idle | running | completed | failed}/** * 初始化阶段(工作流启动时调用一次) * @param {Object} params - 用户配置的参数 * @returns {void} */init(params){// 子类实现thrownewError('Must implement init()');}/** * 执行阶段(每次节点被触发时调用) * @param {ExecutionContext} context - 执行上下文 * @returns {PromiseObject} - 执行结果 */asyncexecute(context){thrownewError('Must implement execute()');}/** * 销毁阶段(工作流结束时调用) * @returns {void} */destroy(){// 清理资源、取消定时器等}/** * 获取块的状态 * @returns {string} */getState(){returnthis.state;}/** * 设置块的状态 * @param {string} newState */setState(newState){this.state=newState;}}module.exports=BaseBlock;3.2 执行上下文(ExecutionContext)/** * 执行上下文:传递给每个功能块的execute方法 * 提供统一的API访问入口 */classExecutionContext{constructor({workflow,node,globalData}){// 工作流级别的变量this.globalData=globalData||{};// 当前节点的配置this.node=node;// 工作流实例引用this.workflow=workflow;// 循环相关数据this.loopData=null;// 表格数据this.table={};// 之前节点的输出this.previousOutputs=newMap();// 日志记录器this.logger=newLogger(node.id);}/** * 获取变量(支持模板字符串插值) * @param {string} key - 变量名或模板字符串 * @returns {*} */getVariable(key){if(typeofkey==='string'key.includes('{ {')){returnthis.interpolate(key);}returnthis.resolvePath(key);}/** * 模板字符串插值 * @param {string} template - 如 "Hello { {user.name}}" * @returns {string} */interpolate(template){returntemplate.replace(/\{\{([^}]+)\}\}/g,(match,path)={constvalue=this.resolvePath(path.trim());returnvalue!==undefined?value:match;});}/** * 解析路径(支持嵌套属性访问) * @param {string} path - 如 "user.name" * @returns {*} */resolvePath(path){constkeys=path.split('.');letresult=this.globalData;for(constkeyofkeys){if(result===undefined||result===null){returnundefined;}result=result[key];}returnresult;}/** * 等待元素出现 * @param {string} selector - CSS选择器 * @param {Object} options - 配置选项 * @returns {PromiseElement} */asyncwaitForSelector(selector,options={}){consttimeout=options.timeout||5000;constinterval=options.interval||100;conststartTime=Date.now();while(Date.now()-startTimetimeout){constelement=document.querySelector(selector);if(element){returnelement;}awaitthis.sleep(interval);}thrownewError(`Element not found:${selector}`);}/** * 休眠 * @param {number} ms - 毫秒数 * @returns {Promisevoid} */sleep(ms){returnnewPromise(resolve=setTimeout(resolve,ms));}/** * 保存变量到全局作用域 * @param {string} key * @param {*} value */setGlobalVariable(key,value){this.globalData[key]=value;}/** * 获取前驱节点的输出 * @param {string} nodeId - 节点ID * @returns {Object} */getPreviousOutput(nodeId){returnthis.previousOutputs.get(nodeId);}}module.exports=ExecutionContext;4. 核心实现一:功能块注册表4.1 BlockRegistry核心实现/** * 功能块注册表 * 管理所有已注册的功能块 */classBlockRegistry{constructor(){// 注册表:blockId - handler实例this.registry=newMap();// 元数据:blockId - metadatathis.metadata=newMap();// 分类索引:category - [blockId]this.categories=newMap();// 是否已初始化this.initialized=false;}/** * 注册功能块 * @param {string} blockId - 功能块唯一标识 * @param {BaseBlock} handlerClass - Handler类 * @param {Object} metadata - 元数据 */register(blockId,handlerClass,metadata){if(this.registry.has(blockId)){console.warn(`Block${blockId}already registered, overwriting`);}// 存储元数据this.metadata.set(blockId,metadata);// 创建handler实例并注册consthandler=newhandlerClass();this.registry.set(blockId,handler);// 更新分类索引constcategory=metadata.category||'general';if(!this.categories.has(category)){this.categories.set(category,[]);}this.categories.get(category).push(blockId);console.log(`✓ Registered block:${blockId}(${metadata.label})`);}/** * 获取功能块handler * @param {string} blockId * @returns {BaseBlock|undefined} */get(blockId){returnthis.registry.get(blockId);}/** * 获取功能块元数据 * @param {string} blockId * @returns {Object|undefined} */getMetadata(blockId){returnthis.metadata.get(blockId);}/** * 获取所有功能块列表 * @returns {Array} */getAll(){constblocks=[];for(const[blockId,handler]ofthis.registry.entries()){constmetadata=this.metadata.get(blockId);blocks.push({id:blockId,handler,metadata});}returnblocks;}/** * 按分类获取功能块 * @param {string} category * @returns {Array} */getByCategory(category){constblockIds=this.categories