我为什么不喜欢单文件组件
单文件组件(SFC)是 JavaScript UI 库常用的一种应用组织方式,其中每个文件代表一个完整的组件。它们通常类似于 HTML 文档,每个文件都包含 HTML 标签、样式标签和脚本标签。这是Vue和Svelte等 UI 框架的常见模式。
我一直在寻找一些关于这个主题的优秀文献,发现很多人都在讨论关注点分离。我并不是提倡严格遵循MVC架构,也不是提倡将代码和视图与样式等完全分离……我也不是提倡让组件文件导出多个组件。
我想谈谈SFC作为组件格式的局限性。对我来说,这个话题很像讨论Hooks相对于类生命周期的优势。我认为不使用典型的SFC有显而易见的优势。
组件边界
什么是组件?如何根据逻辑分解哪些部分可以构成组件?这对于初学者来说并不显而易见,即使随着经验的积累,这个问题依然难以解决。
有人可能会说,学校教给他们的是单一责任原则,即一个组件应该只做一件事。也许这是一种合理的启发式方法。
我知道初学者可能根本不想费心处理这些。他们会把太多东西都放在一个组件里,让所有代码都一目了然。他们不会去碰“props”、“events”、“context”或其他任何跨组件的底层机制。只是简单的代码而已。
有些框架,如果与变更传播系统(例如所有虚拟 DOM 库)绑定,则可能对组件边界有非常严格的规定。它们定义了哪些内容shouldComponentUpdate需要重新渲染,哪些不需要。随意修改组件边界会带来严重的后果。
那么,什么才算是一个组件呢?
理想情况下,一切都应以开发者的实际情况为准。Svelte 的创始人 Rich Harris 在谈到框架的消亡时曾说过:“框架是为了帮助你理清思路。” 组件只是这种思路的延伸。
所以,SFC 实际上处理得相当不错。目前为止没有问题。但让我们深入探讨一下。
组件成本
我对 UI 库中组件的成本进行了相当深入的性能测试。简而言之,大多数情况下,像 React 这样的虚拟 DOM 库在组件数量增加时扩展性良好,而其他库,尤其是响应式库,则不然。它们通常需要将响应式表达式与子组件内部实现同步,这会带来一定的性能开销。
去看一下使用响应式库和虚拟 DOM 库的基准测试,看看它们在使用组件方面有何不同。在测试创建成本时,响应式库使用多个组件的频率如何?在实际应用中,我们往往会用到很多组件。
题外话:我用一个类似 React 的库写了一个基准测试,其中包含一些我自己编写的子组件,代码量比 Svelte 少。我在一篇文章里提到了这件事,结果有人说我不为 Svelte 单独编写组件是不公平的。但我不能这么做,因为我想展示 Svelte 的性能。
我到底想表达什么呢?仅仅因为使用 SFC 的库没有强迫我们使用晦涩难懂的组件就表示赞赏,这是不够的。
组件重构
重构中最昂贵的部分是什么?我个人认为是重新定义边界。如果我们理想的组件是能够让我们自主选择边界的组件,那么我建议组件应该能够根据我们的需要进行扩展和拆分。
React 的组件模型在这方面其实非常方便。首先,它允许在单个文件中包含多个组件。当某些组件变得难以管理时,我们只需将其拆分即可。
或许只需让模板更易读即可。也许只是为了减少重复代码。就像你自然而然地决定把某个功能拆分成一个独立的函数一样。我的意思是,在 JavaScript 中如何才能写出更少的代码?写一个函数。
换句话说,想象一下你会如何在你选择的库中实现这个功能(我这里用的是 React)。假设你有一个组件,它会产生一个副作用,比如使用图表库并在使用后进行清理。
export default function Chart(props) {
const el = useRef();
useEffect(() => {
const c = new Chart(el.current, props.data);
return () => c.release();
}, []);
return (
<>
<h1>{props.header}</h1>
<div ref={el} />
</>
)
}
现在您有了新的要求,即根据布尔enabled属性有条件地应用它。
如果你完成了这个练习,并且将其保持为一个单独的组件,你应该意识到,要应用条件语句,最终需要在代码的视图部分和命令部分(mount、update 和 release)都应用它。
export default function Chart(props) {
const el = useRef();
useEffect(() => {
let c;
if (props.enabled) c = new Chart(el.current, props.data);
return () => if (c) c.release();
}, [props.enabled]);
return (
<>
<h1>{props.header}</h1>
{props.enabled && <div ref={el} />}
</>
)
}
或者使用 React,你只需将其拆分成另一个组件,逻辑基本保持不变。
function Chart(props) {
const el = useRef();
useEffect(() => {
const c = new Chart(el.current, props.data);
return () => c.release();
}, []);
return <div ref={el} />;
}
export default function ChartContainer(props) {
return (
<>
<h1>{props.header}</h1>
{props.enabled && <Chart data={props.data} />}
</>
)
}
这是一个简单的例子,但这种一次改动就能影响多个代码点的做法,正是 Hooks/Composition API/Svelte$能够生成比类生命周期更简洁、更易于维护的代码的原因。然而,我们在这里讨论的却是模板和 JavaScript 代码之间的区别。
这不仅适用于副作用,也适用于嵌套状态。React 方法最棒的地方在于它的灵活性。我不需要创建新文件。我还在学习这个组件的工作原理。如果需求再次改变怎么办?如果我还是个新手,刚刚入门怎么办?
SFCs的局限性
将文件限制在单个组件中的问题在于,我们只能处理单一级别的状态/生命周期。它无法扩展或轻松更改。当边界不匹配时,会导致额外的代码;而将多个文件不必要地拆分,则会增加认知负担。
SFC 库可以考虑支持嵌套语法。不过,大多数库,甚至非 SFC 库,都不支持嵌套语法。例如,React 不允许嵌套 Hook 或将其置于条件语句中。而且,大多数 SFC 库也不允许在模板中使用任意嵌套的 JavaScript 代码。据我所知, MarkoJS可能是唯一支持宏(嵌套组件)和内联 JavaScript 的SFC 库,但这远非主流做法。
或许你觉得这不够重要,但对于从新手到专家来说,从一开始就以可维护性为核心构建的应用架构都极具价值。它会随着用户的成长而逐步完善。这就是为什么我不喜欢 SFC,就像我更喜欢 Hooks 而不是类组件一样。
正因如此,SolidJS 的设计宗旨是随着应用程序的扩展提供最佳体验。它的组件堪称完美,兼具两者的优势。它不像 VDOM 库那样强制你创建大量不必要的组件,但也不会限制你这样做。它支持模板中的嵌套状态和效果,因此能够与你的应用程序一同成长。
换句话说,除了上述方法之外,你还可以嵌套效果和状态。你甚至可以使用 ref 回调来实现这种内联自定义指令:
export default function Chart(props) {
return (
<>
<h1>{props.header}</h1>
{
props.enabled && <div ref={el =>
createEffect(() => {
const c new Chart(el.current, props.data);
onCleanup(() => c.release());
})
} />
}
</>
)
}
Solid 通过独立于生命周期的声明式数据、可消失的组件、JSX 驱动的模板和高性能的细粒度响应式来实现这一点。
Hooks 和 Composition API 只是声明式数据模式功能的冰山一角。快来体验一下这个既熟悉又截然不同的 JS(TypeScript)框架吧!
https://github.com/ryansolid/solid
文章来源:https://dev.to/ryansolid/why-im-not-a-fan-of-single-file-components-3bfl