前端向架构突围系列 - 框架设计(三):用开闭原则拯救你的组件库

写在前面
兄弟们,回想一下,你有没有接过这种需求:
产品经理跑来说:“咱们那个通用的表格组件,现在需要在某一列加个自定义的渲染逻辑,以前是纯文本,现在要变成个带图标的按钮,还能点击弹窗。”
你心想:“这还不简单?”
于是你打开了那个祖传的CommonTable.vueTable.tsx,找到了渲染单元格的地方,熟练地写下了一个if-else
过了两天,产品又来了:“那啥,另一列也要改,这次要加个进度条。”
你又熟练地加了一个else-if
几个月后,这个组件的源码已经突破了 2000 行,光那个if-else的判断逻辑就占了半屏。后来的同事接手时,看着这坨代码,只想把你拉黑。
这种“改哪哪疼,牵一发而动全身”的代码,就是典型的违反了开闭原则 (Open/Closed Principle, OCP)。今天咱们就来聊聊,怎么用 OCP 把这坨代码重构成“人话”。



什么是开闭原则 (OCP)?
开闭原则,听起来很高大上,其实说人话就是八个字:
对扩展开放,对修改关闭。

  • 对扩展开放 (Open for extension):当有新需求来了,你应该能通过“增加新代码”的方式来满足,而不是去改旧代码。
  • 对修改关闭 (Closed for modification):那个已经写好、测试过、稳定运行的核心代码,你尽量别去动它。

想象一下你的电脑主机。你想加个显卡,是直接把主板焊开接线(修改),还是找个 PCI-E 插槽插上去(扩展)?显然后者更靠谱。
在前端领域,OCP 最典型的应用场景就是组件设计插件系统


案例分析:一个“违反 OCP”的糟糕组件
咱们就拿最常见的通用列表项组件来举例。假设我们有一个ListItem组件,用来展示用户信息。
原始需求
需求很简单:展示用户的头像和名字。

// ListItem.tsx (V1) interface User { id: string; name: string; avatar: string; } const ListItem = ({ user }: { user: User }) => { return ( <div className="list-item"> <img src={user.avatar} alt={user.name} /> <span>{user.name}</span> </div> ); };

这代码看起来没毛病,清爽、简单。
需求变更 1:加个 VIP 标志
产品说:“有些用户是 VIP,名字后面得加个金灿灿的皇冠图标。”
你心想,小case,一把梭:

// ListItem.tsx (V2 - 开始变味了) interface User { id: string; name: string; avatar: string; isVip?: boolean; // 新增字段 } const ListItem = ({ user }: { user: User }) => { return ( <div className="list-item"> <img src={user.avatar} alt={user.name} /> <span>{user.name}</span> {/* 修改点:硬编码逻辑 */} {user.isVip && <span className="vip-icon"></span>} </div> ); };

你为了这个新需求,修改ListItem组件的内部实现。虽然只加了一行,但坏头已经开了。
需求变更 2:再加个在线状态
产品又来了:“得显示用户在不在线,在线的头像旁边亮个绿灯。”
你叹了口气,继续梭:

// ListItem.tsx (V3 - 味道越来越冲) interface User { id: string; name: string; avatar: string; isVip?: boolean; isOnline?: boolean; // 又新增字段 } const ListItem = ({ user }: { user: User }) => { return ( <div className="list-item"> <div className="avatar-wrapper"> <img src={user.avatar} alt={user.name} /> {/* 修改点:又硬编码逻辑 */} {user.isOnline && <span className="online-dot"></span>} </div> <span>{user.name}</span> {user.isVip && <span className="vip-icon"></span>} </div> ); };

问题来了:

  1. 组件越来越臃肿:每次新需求都要改这个文件,代码量蹭蹭涨。
  2. 耦合度极高ListItem竟然要知道什么是 VIP,什么是在线状态。如果明天要加个“等级勋章”、“活动挂件”呢?
  3. 测试困难:每次改动都得把以前的 VIP、在线状态全测一遍,生怕改坏了。

这就是典型的违反了对修改关闭。核心组件被迫了解太多它不该知道的业务逻辑。


重构:用 OCP 把“屎山”铲平
怎么让ListItem既能支持各种花里胡哨的展示,又不用每次都改它呢?
答案就是:把变化的部分抽离出去,留下不变的骨架。

  • 不变的部分:列表项的基本结构(左边是图,右边是文字)。
  • 变化的部分:头像旁边要加什么装饰?文字后面要挂什么配件?

我们可以利用 React 的组合 (Composition)特性,比如children或者Render Props(插槽槽位)。
重构 V1:使用插槽 (Slots / Render Props)
我们改造一下ListItem,让它别管那么多闲事,只负责提供“坑位”。

// ListItem.tsx (OCP版本) interface ListItemProps { avatar: React.ReactNode; // 不再只传字符串,直接传节点 title: React.ReactNode; // 同上 // 预留两个扩展槽位 avatarAddon?: React.ReactNode; titleAddon?: React.ReactNode; } // 这个组件现在稳定得一批,几乎不需要再修改了 const ListItem = ({ avatar, title, avatarAddon, titleAddon }: ListItemProps) => { return ( <div className="list-item"> <div className="avatar-wrapper"> {avatar} {/* 扩展点:头像装饰 */} {avatarAddon} </div> <div className="title-wrapper"> {title} {/* 扩展点:标题装饰 */} {titleAddon} </div> </div> ); };

现在,核心组件ListItem对修改是关闭的。那怎么扩展新需求呢?
在使用它的地方进行扩展(对扩展开放):

// UserList.tsx (业务层) import ListItem from './ListItem'; const UserList = ({ users }) => { return ( <div> {users.map(user => ( <ListItem key={user.id} // 基础信息 avatar={<img src={user.avatar} />} title={<span>{user.name}</span>} // 扩展需求1:在线状态 avatarAddon={user.isOnline ? <OnlineDot /> : null} // 扩展需求2:VIP标识 titleAddon={user.isVip ? <VipCrown /> : null} /> ))} </div> ); };

看!世界清静了。

  • ListItem组件不知道也不关心什么是 VIP。它只知道:“如果有人给了我titleAddon,那我就把它渲染在标题后面。”
  • 如果明天产品要加个“等级勋章”,你只需要写个<LevelBadge />组件,然后传给titleAddon即可。ListItem.tsx文件一个字都不用改。

这就是 OCP 的魅力。


进阶:策略模式与配置化
在更复杂的场景下,比如我们开头提到的通用表格组件,每一列的渲染逻辑可能千奇百怪。这时候光用插槽可能还不够灵活。
我们可以借鉴策略模式的思想,结合配置化来实现 OCP。
假设我们有一个复杂的后台管理表格。
糟糕的设计 (违反 OCP)

// BadTableColumn.tsx const renderCell = (value, columnType) => { // 地狱 if-else if (columnType === 'text') { return <span>{value}</span>; } else if (columnType === 'image') { return <img src={value} />; } else if (columnType === 'link') { // ...要加新类型就得改这里 } else if (columnType === 'status') { // ...越来越长 } // ... };

符合 OCP 的设计
我们定义一个策略注册表,把每种类型的渲染逻辑注册进去。

// renderStrategies.tsx (策略定义) const strategies = { text: (value) => <span>{value}</span>, image: (value) => <img src={value} className="table-img" />, // 新需求:状态标签 status: (value) => <Tag color={value === 'active' ? 'green' : 'red'}>{value}</Tag>, }; // 提供注册入口(对扩展开放) export const registerStrategy = (type, renderer) => { strategies[type] = renderer; }; // 提供获取入口 export const getStrategy = (type) => { return strategies[type] || strategies['text']; };

然后,表格组件只负责调用策略:

// GoodTableColumn.tsx import { getStrategy } from './renderStrategies'; const TableCell = ({ value, columnType }) => { // 核心组件对修改关闭:它不需要知道具体怎么渲染 const renderer = getStrategy(columnType); return <td>{renderer(value)}</td>; };

当你要新增一种“进度条”类型的列时,你根本不需要碰TableCell组件,只需要在项目的入口文件里注册一个新的策略:

// main.js (应用入口) import { registerStrategy } from './renderStrategies'; import ProgressBar from './components/ProgressBar'; // 扩展新能力 registerStrategy('progress', (value) => <ProgressBar percent={value} />);

这就实现了一个简易的插件化系统。核心库稳定不变,业务方通过注册机制无限扩展能力。


总结:别让自己成为“改Bug机器”
开闭原则不是什么高深的理论,它就是为了让你少加班、少背锅而生的。
记住这几个实战要点:

  1. 识别变化点:做组件之前先想想,哪些是铁打不动的骨架,哪些是流水易变的皮肉。
  2. 多用组合/插槽:React 的children和 Render Props,Vue 的slot,都是实现 OCP 的利器。把决定权交给使用者,而不是自己大包大揽。
  3. 善用策略/配置:遇到复杂的if-else逻辑判断渲染类型时,考虑用映射表(Map 对象)代替硬编码,把逻辑抽离出去。

下次再遇到产品经理不断提新需求,希望你能自信地打开代码,优雅地新增一个文件,而不是痛苦地在那坨几千行的祖传代码里加if-else
Keep coding, keep open!


互动话题
你的项目里有没有那种因为违反 OCP 而变得维护困难的“超级组件”?你又是怎么重构它的?欢迎在评论区吐槽交流!

原文: https://juejin.cn/post/75929960

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1137027.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何在 Linux 中使用 file 命令识别文件类型

在 Linux 系统中&#xff0c;file 命令是一款强大的工具&#xff0c;用于确定文件类型&#xff0c;例如普通文件、压缩归档文件、符号链接以及其他特殊文件类型。与仅依赖文件扩展名的方法不同&#xff0c;file 命令通过引用“magic file database”数据库来识别文件类型。该数…

WebM转MP4在线转换工具

WebM转MP4在线转换工具 - 88box视频格式转换助手 工具核心信息 工具名称&#xff1a;88box视频格式转换工具访问地址&#xff1a;https://88box.top/video-tools/transcode核心功能&#xff1a;支持WebM与MP4格式双向转换&#xff0c;兼容多场景视频格式适配需求 工具详细介…

SSM校园人才市场391d8(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面

系统程序文件列表系统项目功能&#xff1a;学生,企业,招聘信息,岗位类别,投递简历,参加面试,面试结果,学生评价SSM校园人才市场开题报告一、课题研究背景与意义&#xff08;一&#xff09;研究背景当前高校毕业生就业压力逐年增大&#xff0c;校园招聘作为毕业生求职的核心渠道…

图解说明RS232串口调试工具在自动化产线中的连接方式

RS232串口调试工具如何接&#xff1f;一张图讲清自动化产线中的“通信听诊器”用法在现代自动化车间里&#xff0c;PLC、伺服驱动器、条码扫描仪、温控表这些设备高速协同运转。一旦通信出问题&#xff0c;整条产线可能就得停摆。这时候&#xff0c;工程师往往会掏出一个不起眼…

2026“芯片战局”白热化,AMD/微美全息加固“护城河”竞逐AI算力制高点

1月6日至9日&#xff0c;全球规模最大&#xff0c;影响力最广泛的国际消费电子展&#xff08;CES&#xff09;在美国拉斯维加斯开幕。这个全球最重要的消费电子展会已经有近60年历史&#xff0c;从1978年开始固定在美国。AI芯片巨头集体站台在刚开锣的2026 CES上&#xff0c;芯…

HBuilderX运行项目不启动浏览器?一文说清常见故障点

HBuilderX运行项目不启动浏览器&#xff1f;别急&#xff0c;这5个坑我替你踩过了你有没有过这样的经历&#xff1a;兴冲冲打开HBuilderX&#xff0c;写完一段代码&#xff0c;信心满满地点击“运行到浏览器”&#xff0c;结果——什么都没发生。没有弹出Chrome&#xff0c;没有…

2026年软考高项报名全攻略,一文读懂!

软考高项(信息系统项目管理师作为计算机技术与软件领域的国家级高级资格认证&#xff0c;是职场晋升、职称评定的核心凭证。但报名流程涉及属地审核、材料上传等多个关键环节&#xff0c;稍不注意易踩坑。本文结合2025年的考务要求&#xff0c;整理26年的报名全流程指南&#x…

申请发布Profile

前提条件 已创建HarmonyOS应用 | 创建元服务。 已申请发布证书。 &#xff08;如需使用ACL权限&#xff09;已申请并获取ACL权限。 操作步骤 登录AppGallery Connect&#xff0c;选择“证书、APP ID和Profile”。 在左侧导航栏选择“证书、APP ID和Profile > Profile”&…

轻松将 iPhone 中的短信导出为 PDF

如果您曾经想保护 iPhone 上的重要短信&#xff0c;那么您并不孤单。文本可以保存关键信息&#xff0c;无论是用于法律文件、工作目的还是个人记录。将这些消息转换为 PDF 格式可以更轻松地访问、存储和防止数据丢失。为此&#xff0c;我们在本指南中提供了 3 种有效的方法来帮…

es客户端基础概念全面讲解:索引与文档操作指南

深入理解Elasticsearch客户端&#xff1a;从索引管理到文档操作的实战指南你有没有遇到过这样的场景&#xff1f;系统日志越积越多&#xff0c;用户搜索响应越来越慢&#xff1b;商品数据频繁更新&#xff0c;但前端总是“看”不到最新价格&#xff1b;成千上万条记录需要导入E…

快速理解Intel平台下USB3.0传输速度不达标原因

为什么你的USB3.0跑不满5Gbps&#xff1f;深度剖析Intel平台下的真实瓶颈 你有没有遇到过这种情况&#xff1a;买了一块标称读写速度500MB/s的USB3.0固态U盘&#xff0c;插在电脑上复制大文件时&#xff0c;任务管理器里却只显示180MB/s&#xff1f;甚至更低&#xff1f; 别急…

手把手教程:编写基础Virtual Serial Port Driver

从零构建虚拟串行端口驱动&#xff1a;深入内核的通信模拟实践 你有没有遇到过这样的场景&#xff1f;手头开发一个工业HMI软件&#xff0c;依赖COM口与PLC通信&#xff0c;但测试阶段根本没有真实设备可用&#xff1b;或者想验证串口协议栈的容错能力&#xff0c;却无法轻易“…

如何轻松地将文件从 PC 传输到 iPhone

传统上&#xff0c;您可以使用 iTunes 将文件从电脑传输到 iPhone&#xff0c;但现在&#xff0c;iTunes 已不再是唯一选择。有多种其他有效方法可以帮助您传输文件。在今天的指南中&#xff0c;您可以了解 8 种使用或不使用 iTunes 传输文件的方法&#xff0c;包括联系人、照片…

大数据领域数据架构的分布式存储设计

大数据架构实战&#xff1a;分布式存储设计从原理到落地 标题选项 《大数据架构实战&#xff1a;分布式存储设计从原理到落地》《拆解大数据存储&#xff1a;分布式系统设计的核心逻辑与实践》《大数据时代的存储基石&#xff1a;分布式存储设计全解析》《从0到1构建大数据架构…

图解说明LVGL在工业控制器上的移植流程

从零开始&#xff1a;如何在工业控制器上跑通LVGL图形界面&#xff1f;你有没有遇到过这样的场景&#xff1f;客户拿着一台PLC设备走过来&#xff0c;指着那块黑白小屏说&#xff1a;“能不能做得像手机一样流畅&#xff1f;”——这背后&#xff0c;其实是现代工业对人机交互体…

如何以 9 种方式将照片从手机传输到笔记本电脑

使用 USB 电缆可以将照片从智能手机复制到计算机。但是&#xff0c;如果没有 USB 数据线&#xff0c;如何将照片从手机无线传输到笔记本电脑呢&#xff1f;为了解决这个问题&#xff0c;我们搜索并测试了不同的应用程序&#xff0c;然后总结了本指南中分享的 9 个有效选项。您可…

WinDbg下载后怎么装?系统学习安装步骤

从零开始搭建WinDbg调试环境&#xff1a;下载、安装与实战入门 你是不是也遇到过这样的场景&#xff1f;系统突然蓝屏&#xff0c;重启后只留下一个 MEMORY.DMP 文件&#xff1b;或者开发的驱动程序一加载就崩溃&#xff0c;却找不到原因。这时候&#xff0c;很多人第一反应…

eide代码自动补全与语法高亮设置教程

让你的嵌入式编码更高效&#xff1a;eide自动补全与语法高亮实战配置指南你有没有过这样的经历&#xff1f;写一个外设初始化函数时&#xff0c;RCC_APB2PeriphClockCmd到底怎么拼的又得翻手册&#xff1b;或者打开一份老同事留下的代码&#xff0c;满屏灰白文字看得头晕眼花&a…

HBuilderX在Windows系统下无法唤起浏览器解决方案

HBuilderX 在 Windows 下打不开浏览器&#xff1f;一文彻底解决“运行到浏览器”失效问题你有没有遇到过这种情况&#xff1a;在 HBuilderX 里辛辛苦苦写完代码&#xff0c;信心满满地点击“运行到浏览器”&#xff0c;结果——毫无反应&#xff1f;弹出个空白页&#xff1f;甚…

图解说明ES6模块化:加载机制与执行顺序分析

深入理解 ES6 模块化&#xff1a;从加载机制到执行顺序的完整图解 你有没有遇到过这样的情况&#xff1f;在写一个简单的 import 语句时&#xff0c;发现导入的变量是 undefined &#xff1b;或者明明模块只应该执行一次&#xff0c;却因为循环引用产生了意外行为。这些问题…