你认为React中最复杂的事情是什么?重新渲染?语境?门户网站?并发?
不。
React 最难的部分是它周围的一切非 React。 “上面列出的那些东西是如何工作的?”这个问题的答案很简单:只需遵循算法并做笔记即可。结果将是确定的并且始终相同(如果您正确追踪)。这只是科学和事实。
但是“什么让组件好呢?”或“实施……(某事)的正确方法是什么?”甚至“我应该使用库还是构建自己的解决方案?”这里唯一正确的答案是“这取决于情况”。它恰好是最没有帮助的一个。
我想为新文章找到比这更好的东西。但由于这些类型的问题不可能有简单的答案和通用的解决方案,因此这篇文章更多地是我的思维过程的演练,而不是“这就是答案,永远这样做”。希望它仍然有用。
那么,如何才能将功能从想法转变为可投入生产的解决方案呢?让我们尝试实现一个简单的模态对话框并看看。那有什么可能是复杂的呢? ?
让我们从有时被称为“尖峰”的东西开始 - 最简单的实现,可以帮助探索潜在的解决方案并收集进一步的需求。我知道我正在实现一个模式对话框。假设我有一个像这样的漂亮设计:
对话框基本上是屏幕上的一个元素,当单击按钮之类的内容时会出现该元素。这正是我要开始的地方。
export default function Page() { const [isOpen, setIsOpen] = useState(false); return ( <> <button onClick={() => setIsOpen(true)}> Click me </button> {isOpen ? ( <div className="dialog">some content</div> ) : null} </> ); }
状态,一个监听点击的按钮,以及当状态为 true 时显示的未来对话框。对话框还应该有一个“关闭”操作:
<button className="close-button" onClick={() => setIsOpen(false)} > Close </button>
它还有一个“背景” - 一个可点击的半透明 div,覆盖内容并在单击时触发模式消失。
<div className="backdrop" onClick={() => setIsOpen(false)} ></div>
大家在一起:
export default function Page() { const [isOpen, setIsOpen] = useState(false); return ( <> <button onClick={() => setIsOpen(true)}> Click me </button> {isOpen ? ( <> <div className="backdrop" onClick={() => setIsOpen(false)} ></div> <div className="dialog"> <button className="close-button" onClick={() => setIsOpen(false)} > Close </button> </div> </> ) : null} </> ); }
我通常也会尽早添加合适的样式。看到我正在实现的功能以与预期相同的外观出现在屏幕上,这有助于我思考。另外,它还可以通知功能的布局,这正是此对话框将发生的情况。
让我们快速为背景添加 CSS - 它没什么特别的,只是 div 上的半透明背景,位置固定:占据整个屏幕:
export default function Page() { const [isOpen, setIsOpen] = useState(false); return ( <> <button onClick={() => setIsOpen(true)}> Click me </button> {isOpen ? ( <div className="dialog">some content</div> ) : null} </> ); }
该对话框稍微有趣一些,因为它需要放置在屏幕中间。当然,CSS 中有 1001 种方法可以实现这一目标,但我最喜欢的也可能是最简单的一种是:
<button className="close-button" onClick={() => setIsOpen(false)} > Close </button>
我们使用“固定”位置来摆脱布局约束,添加 50% 的左侧和顶部以将 div 移动到中间位置,然后将其变换回 50%。 left 和 top 将相对于屏幕进行计算,变换将相对于 div 本身的宽度/高度,因此,无论其宽度或屏幕宽度如何,它都会出现在中间。
此步骤中 CSS 的最后一点是正确设置对话框本身和“关闭”按钮的样式。这里就不复制粘贴了,实际的样式并不重要,看一下例子:
现在我已经粗略地实现了该功能,是时候让它变得“真实”了。为此,我们需要详细了解我们到底要解决什么问题以及为谁解决问题。从技术上讲,我们应该明白在编码任何东西之前,所以很多时候,这一步应该是第1步。
此对话框是否是原型的一部分,需要尽快实施,向投资者展示一次,然后不再使用?或者它可能是您要在 npm 和开源上发布的通用库的一部分?或者它可能是您的 5,000 人组织将使用的设计系统的一部分?或者它只是您的 3 人小型团队的内部工具的一部分,仅此而已?或者,也许您在 TikTok 等公司工作,并且此对话框将成为仅在移动设备上可用的网络应用程序的一部分?或者您可能在一家只为政府编写应用程序的机构工作?
回答这些问题可以确定下一步编码的方向。
如果只是一个原型,用一次,可能就已经足够了。
如果它要作为库的一部分开源,它需要有一个非常好的通用 API,世界上任何开发人员都可以使用和理解,大量的测试和良好的文档。
作为 5,000 人组织的设计系统一部分的对话框需要遵守组织的设计准则,并且可能会限制将哪些外部依赖项带入存储库。因此,您可能需要从头开始实现许多事情,而不是执行 npm install new-fancy-tool。
为政府建立的机构的对话可能需要成为宇宙中最容易访问且符合法规的对话。否则,该机构可能会失去政府合同并破产。
等等等等。
出于本文的目的,我们假设该对话框是现有大型商业网站当前正在进行的全新重新设计的一部分,该网站每天有来自世界各地的数千名用户。重新设计正在进行中,我得到的唯一带有对话框的设计是这样的:
剩下的稍后再说,设计师们都忙不过来了。此外,我是负责重新设计和维护网站的永久团队的一员,而不是为单个项目雇用的外部承包商。
在这种情况下,仅凭这张图片并了解我们公司的目标就可以为我提供足够的信息来做出合理的假设并实现 90% 的对话。剩下的10%可以稍后再微调。
这些是我根据上述信息可以做出的假设:
现有网站每天有来自世界各地的数千名用户,因此我需要确保该对话框至少可以在大屏幕和移动屏幕以及不同的浏览器上运行。理想情况下,我需要检查现有分析才能绝对确定,但这是一个非常安全的选择。
不止一位开发人员正在为此编写代码,并且代码将保留下来。网站规模很大,已经拥有数千名用户;对于投资者来说,这不是一个快速的原型。所以,我需要确保代码可读,API有意义,可用且可维护,并且没有明显的脚枪。
公司关心其形象和网站的质量 - 否则,他们为什么要进行重新设计? (我们假设这里有积极的意图?)。这意味着需要达到一定的质量水平,我需要提前思考并预测常见场景和边缘情况,即使它们还不是当前设计的一部分。
许多用户可能意味着并非所有人都专门使用鼠标与网站交互。该对话框还必须可以通过键盘交互甚至屏幕阅读器等辅助技术来使用。
现有的大型代码库(记住,这是重新设计!)意味着我可以为此功能带来的外部依赖项可能存在限制。任何外部依赖都是有代价的,尤其是在大型和旧的代码库中。出于本文的目的,我们假设我可以使用外部库,但我需要对此有一个很好的理由。
最后,更多的设计即将到来,所以我需要从设计和用户的角度预测它会走向何方,并确保代码可以尽早处理它。
现在我知道了需求并有了合理的猜测,我可以制作实际的对话框组件了。首先,从这段代码来看:
export default function Page() { const [isOpen, setIsOpen] = useState(false); return ( <> <button onClick={() => setIsOpen(true)}> Click me </button> {isOpen ? ( <div className="dialog">some content</div> ) : null} </> ); }
我绝对需要将对话框部分提取到可重用组件中 - 将有大量基于对话框的功能需要实现。
<button className="close-button" onClick={() => setIsOpen(false)} > Close </button>
对话框将有一个 onClose 属性 - 当单击“关闭”按钮或背景时,它将通知父组件。然后,父组件仍将具有状态并呈现对话框,如下所示:
<div className="backdrop" onClick={() => setIsOpen(false)} ></div>
现在,让我们再次看看设计并更多地考虑对话框:
对话框中显然会有一些带有操作按钮的“页脚”部分。这些按钮很可能会有很多变化 - 一个、两个、三个、左对齐、右对齐、中间有空格等等。此外,此对话框没有 标题 ,但是它非常非常有可能具有某些标题的对话框是一种非常常见的模式。这里绝对会有一个内容区域,其中包含完全随机的内容 - 从确认文本到表格,再到互动体验,再到没有人阅读的很长的“条款和条件”可滚动文本。
最后是尺寸。设计中的对话框很小,只是一个确认对话框。大表格或长文本不适合那里。因此,考虑到我们在步骤 2 中收集的信息,可以非常安全地假设对话框的大小需要更改。此时,考虑到设计师可能有设计指南,我们可以假设对话框有三种变体:“小”、“中”和“大”。
所有这些意味着我们需要在 ModalDialog 上有 props:页脚和 header 将只是接受 ReactNode 的常规 props,大小将只是字符串的联合,而内容区域作为主要部分将进入孩子们:
export default function Page() { const [isOpen, setIsOpen] = useState(false); return ( <> <button onClick={() => setIsOpen(true)}> Click me </button> {isOpen ? ( <> <div className="backdrop" onClick={() => setIsOpen(false)} ></div> <div className="dialog"> <button className="close-button" onClick={() => setIsOpen(false)} > Close </button> </div> </> ) : null} </> ); }
我们将使用来自道具的附加 className 来控制对话框的大小。但在现实生活中,它将很大程度上取决于存储库中使用的样式解决方案。
然而,在这个变体中,对话框太灵活了 - 几乎任何东西都可以去任何地方。例如,在页脚中,大多数时候,我们只需要一两个按钮,仅此而已。这些按钮必须一致地排列在整个网站的各处。我们需要一个包装器来对齐它们:
.backdrop { background: rgba(0, 0, 0, 0.3); position: fixed; top: 0; left: 0; width: 100%; height: 100%; }
与内容相同 - 至少,它需要一些周围的填充和滚动能力。标题可能需要一些文本样式。于是布局就变成了这样:
export default function Page() { const [isOpen, setIsOpen] = useState(false); return ( <> <button onClick={() => setIsOpen(true)}> Click me </button> {isOpen ? ( <div className="dialog">some content</div> ) : null} </> ); }
但不幸的是,我们无法保证这一点。在某些时候,很可能有人希望在页脚中添加除按钮之外的更多内容。或者某些对话框需要在已售背景上有标题。或者有时,内容不需要填充。
我在这里要指出的是,有一天我们需要能够设计页眉/内容/页脚部分的样式。而且可能比预期要早。
当然,我们可以只使用 props 传递该配置,并使用 headerClassName、contentClassName 和 footerClassName 等 props。实际上,对于某些情况来说,这可能没问题。但对于像重新设计的漂亮对话框这样的东西,我们可以做得更好。
解决这个问题的一个非常巧妙的方法是将我们的页眉/内容/页脚提取到它们自己的组件中,如下所示:
<button className="close-button" onClick={() => setIsOpen(false)} > Close </button>
并将 ModalDialog 代码恢复为没有包装器的代码:
<div className="backdrop" onClick={() => setIsOpen(false)} ></div>
这样,在父应用程序中,如果我想要对话框部分的默认设计,我会使用这些微小的组件:
export default function Page() { const [isOpen, setIsOpen] = useState(false); return ( <> <button onClick={() => setIsOpen(true)}> Click me </button> {isOpen ? ( <> <div className="backdrop" onClick={() => setIsOpen(false)} ></div> <div className="dialog"> <button className="close-button" onClick={() => setIsOpen(false)} > Close </button> </div> </> ) : null} </> ); }
如果我想要完全自定义的东西,我会实现一个具有自己的自定义样式的新组件,而不会弄乱 ModalDialog 本身:
.backdrop { background: rgba(0, 0, 0, 0.3); position: fixed; top: 0; left: 0; width: 100%; height: 100%; }
就此而言,我什至不再需要页眉和页脚道具。我可以将 DialogHeader 和 DialogFooter 传递给子级,进一步简化 ModalDialog,并拥有更好的 API,具有相同级别的灵活性,同时在各处都具有一致的设计。
父组件将如下所示:
.dialog { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); }
对话框的 API 将如下所示:
export default function Page() { const [isOpen, setIsOpen] = useState(false); return ( <> <button onClick={() => setIsOpen(true)}> Click me </button> {isOpen ? ( <> <div className="backdrop" onClick={() => setIsOpen(false)} ></div> <div className="dialog"> <button className="close-button" onClick={() => setIsOpen(false)} > Close </button> </div> </> ) : null} </> ); }
到目前为止我对此非常满意。它足够灵活,可以以设计可能需要的任何方式进行扩展,但它也足够清晰和合理,可以轻松地在整个应用程序中实现一致的 UI。
这是可以使用的实例:
现在 Modal 的 API 已经足够好了,是时候解决我实现的明显的脚枪问题了。如果你读够了我的文章,你可能已经大声尖叫,“你在做什么???重新渲染!!”最后十分钟?当然,你是对的:
const ModalDialog = ({ onClose }) => { return ( <> <div className="backdrop" onClick={onClose}></div> <div className="dialog"> <button className="close-button" onClick={onClose}> Close </button> </div> </> ); };
这里的Page组件是有状态的。每次模式打开或关闭时,状态都会发生变化,并且会导致整个组件及其内部所有内容的重新渲染。是的,“过早的优化是万恶之源”,是的,在实际测量性能之前不要优化性能,在这种情况下,我们可以安全地忽略传统观点。
有两个原因。首先,我知道一个事实是,整个应用程序中会分散很多模式。这不是一个没有人会使用的一次性隐藏功能。因此,有人将状态放置在不应该使用这样的 API 的地方的可能性非常高。其次,从一开始就不需要花费太多时间和精力来防止重新渲染问题的发生。只要1分钟的努力,我们根本不需要考虑这里的性能。
我们需要做的就是封装状态并引入“不受控组件”的想法:
export default function Page() { const [isOpen, setIsOpen] = useState(false); return ( <> <button onClick={() => setIsOpen(true)}> Click me </button> {isOpen ? ( <div className="dialog">some content</div> ) : null} </> ); }
BaseModalDialog 与我们之前的对话框完全相同,我只是重命名了它。
然后传递一个应该触发对话框的组件作为触发道具:
<button className="close-button" onClick={() => setIsOpen(false)} > Close </button>
页面组件将如下所示:
<div className="backdrop" onClick={() => setIsOpen(false)} ></div>
页面内不再有状态,不再有潜在危险的重新渲染。
这样的 API 应该涵盖 95% 的用例,因为大多数时候,用户需要单击某些内容才能显示对话框。在极少数情况下,当对话框需要独立显示时,例如,在快捷方式上或作为入门的一部分,我仍然可以使用 BaseModalDialog 并手动处理状态。
从 React 的角度来看,ModalDialog 组件的 API 非常可靠,但工作还远未完成。考虑到我在第 2 步中收集的必备条件,我还需要解决更多问题。
问题 1:我将触发器包装到一个额外的跨度中 - 在某些情况下,这可能会破坏页面的布局。我需要以某种方式去掉包装纸。
问题 2:如果我在创建新堆叠上下文的元素内渲染对话框,则模式将出现在某些元素下方。我需要在 Portal 内渲染它,而不是像我现在一样直接在布局内渲染。
问题 3:目前键盘访问非常糟糕。当正确实现的模式对话框打开时,焦点应该跳到里面。当它关闭时 - 焦点应该返回到触发对话框的元素。当对话框打开时,焦点应该被“困”在里面,而外面的元素不应该是可聚焦的。按 ESC 按钮应关闭该对话框。目前这些都还没有实现。
问题 1 和 2 有点烦人,但可以相对快速地解决。然而,手动完成第 3 个问题是一件非常痛苦的事情。另外,这肯定是一个已解决的问题 - 每个地方的每个对话框都需要此功能。
“我自己做的巨大痛苦”“看起来肯定是一个已解决的问题”的组合是我寻找现有库的地方。
考虑到我已经完成的所有前期工作,现在选择合适的就很容易了。
我可以使用任何现有的 UI 组件库,例如 Ant Design 或 Material UI,并使用其中的对话框。但如果重新设计不使用它们,将他们的设计调整为我需要的,会带来比他们解决的更多的痛苦。所以对于这种情况,立即否定。
我可以使用“无头”UI 库之一,例如 Radix 或 React Aria。它们实现了状态和触发器等功能以及所有可访问性,但将设计留给了消费者。在查看他们的 API 时,我需要仔细检查它们是否允许我控制对话框的状态,如果我确实需要它来手动触发对话框(他们确实这样做)。
如果由于某种原因,我无法使用无头库,我至少会尝试使用处理焦点陷阱功能的库。
为了本文的目的,我们假设我可以带任何我想要的库。在这种情况下,我将使用 Radix - 它非常易于使用,并且对话框的 API 看起来与我已经实现的非常相似,因此重构应该是轻而易举的。
我们需要稍微更改一下对话框本身的 API:
export default function Page() { const [isOpen, setIsOpen] = useState(false); return ( <> <button onClick={() => setIsOpen(true)}> Click me </button> {isOpen ? ( <div className="dialog">some content</div> ) : null} </> ); }
和我以前的几乎一样。只是,我没有使用 div,而是使用 Radix 原语。
不受控制的对话框用法根本没有改变:
<button className="close-button" onClick={() => setIsOpen(false)} > Close </button>
受控对话框略有变化 - 我需要将道具传递给它而不是条件渲染:
<div className="backdrop" onClick={() => setIsOpen(false)} ></div>
查看下面的示例并尝试使用键盘进行导航。一切都按照我的需要进行,这有多酷?
作为奖励,Radix 还可以处理 Portal 问题,并且它不会将触发器包装在一个跨度中。我不再需要解决边缘情况,所以我可以继续最后一步。
该功能还没有完成! ?该对话框现在看起来和感觉都相当可靠,因此现阶段我不会对其实现进行任何重大更改。但对于我正在解决的用例,它仍然需要一些东西才能被认为是“完美”对话框。
One:设计师要求我做的第一件事(如果他们还没有做的话)就是在对话框打开时添加一个微妙的动画。需要预见它并记住如何在 React 中制作动画。
两个:我需要向对话框添加最大宽度和最大高度,以便在小屏幕上它仍然看起来不错。想想它在大屏幕上的样子。
三:我需要与设计师讨论对话框在移动设备上的行为方式。他们很可能会要求我将其做成一个滑入式面板,无论对话框的大小如何,它都会占据大部分屏幕。
四个:我需要至少引入 DialogTitle 和 DialogDescription 组件 - Radix 会要求将它们用于辅助功能。
五:测试!该对话框将保留下来并由其他人维护,因此在这种情况下测试几乎是强制性的。
也许还有很多我现在忘记的小事情,稍后会出现。更不用说实现对话框内容的实际设计了。
如果将上面的“对话框”替换为“SomeNewFeature”,这或多或少是我用来实现几乎所有新功能的算法。
解决方案的快速“峰值”→收集功能需求→使其工作→使其高性能→使其完整→使其完美。
对于像实际对话框这样的东西,我已经实现了数百次,我会在 10 秒内在脑海中完成第一步,然后立即从步骤 2 开始。
对于非常复杂和未知的事情,第 1 步可能会更长,并且涉及立即探索不同的解决方案和库。
一些不完全未知的东西,只是“我们需要做的常规功能”,可能会跳过步骤 1,因为可能没有什么可探索的。
很多时候,尤其是在“敏捷”环境中,它更像是螺旋而不是直线,需求是增量提供的并且经常变化,我们会定期返回前两个步骤。
希望此类文章有用! ??如果您想要更多这样的内容或者更喜欢通常的“事情如何运作”的内容,请告诉我。
并期待听到你们所有人的头脑中这个过程有何不同?
最初发布于 https://www.developerway.com。网站还有更多这样的文章吗?
看看《Advanced React》一书,将您的 React 知识提升到一个新的水平。
订阅时事通讯、在 LinkedIn 上联系或在 Twitter 上关注,以便在下一篇文章发布时立即收到通知。
顺便说一句,最后一件事:如果您很快就要开始一个新项目,并且没有设计师,也没有时间来完善所描述的设计体验 - 我最近花了几个小时(又几个小时)来实现一个新的项目本例的 UI 组件库。它具有可复制粘贴的组件和常见模式、Radix 和 Tailwind、深色模式、可访问性以及开箱即用的移动支持。包括上面完美的模态对话框! ?
尝试一下:https://www.buckets-ui.com/
以上是存在主义的 React 问题和完美的模态对话框的详细内容。更多信息请关注PHP中文网其他相关文章!