平面设计十年过去了UI 框架还停滞在原地Bsport体育……
栏目:Bsport体育 发布时间:2023-10-06
 【CSDN 编者按】这篇文章是一位前端开发者揭示了主流 UI 框架的局限性,认为它们都在误导开发者,隐藏了 DOM 节点的真实复杂性。作者指出 HTML 语法并不是描述 UI 的最佳抽象,而是 DOM 树的一种投影。HTML 无法有效地表达 DOM 节点的七类属性,而只能将它们混合在一起。作者认为开发者应该面对并理解 DOM 节点的自然复杂性Bsport体育,而不是被框架所迷惑。作者也提到了

  【CSDN 编者按】这篇文章是一位前端开发者揭示了主流 UI 框架的局限性,认为它们都在误导开发者,隐藏了 DOM 节点的真实复杂性。作者指出 HTML 语法并不是描述 UI 的最佳抽象,而是 DOM 树的一种投影。HTML 无法有效地表达 DOM 节点的七类属性,而只能将它们混合在一起。作者认为开发者应该面对并理解 DOM 节点的自然复杂性Bsport体育,而不是被框架所迷惑。作者也提到了 Svelte 5 的 runes 特性,表示对其有一定的期待。

  几天前,Svelte 5 的预览版本随着对 runes 的详细介绍 被公开发布。该消息令包括我在内众多人士激动不已。$props、$derived、$effects、$state、信号等概念,我并非第一次接触,我在 5-6 年前就见过这样的响应式处理机制。我认为 Svelte 正沿着正确的方向演进。虽然 Svelte 仍然使用了一些主流但不太适合解决复杂 Web 问题的方案,我仍然期望他们能够克服这些困难。不过Bsport体育,这并非本文今天要探讨的核心议题。

  为什么组件的响应性解决方案仍然需要编译阶段?为何我们还在使用非标准的 HTML 语法和一系列自定义指令?为何 UI 的描述仍然是以命令式的方式进行?为什么技术界还在努力模仿 HTML?

  这个问题可能会引发不少争议,但事实上,HTML 仅仅是 DOM(文档对象模型)树的一种表现形式,而这种表现形式并非一定是最优的。确切地说,浏览器处理的不是 HTML,而是 DOM 节点。一个完整的 DOM 节点应该包括以下七类属性:

  遗憾的是,许多开发者要么没有意识到这种复杂性是不可避免的,要么就是不愿承认。几乎所有现有的 UI 解决方案都在使用一种过于简化的方式试图把这种复杂性忽略掉,这是不可取的。

  这些解决方案都试图把 DOM 节点属性的多样性简化为一个扁平的属性列表,这种做法显然不切实际。即便将七大类 DOM 节点属性简化为一张扁平的属性列表,这些属性的多样性仍然会存在,只是变成了一堆难以管理的碎片信息。

  复杂性主要有两类:人为引入的复杂性和自然存在的复杂性。人为引入的复杂性通常来自库、框架、编程语言和设计范式等。而自然复杂性则是平台本身固有的,用于解决特定领域的基本问题。优秀的工程师会努力减少人为引入的复杂性,同时积极面对和解决自然复杂性。我们应当不再回避这种自然存在的复杂性,而是应更加尊重和理解我们所使用的平台。

  Rich Harris 发表了一则精彩的视频,详细解释了 getter 和 setter 的实际运作原理,并回应了公众对 Svelte 新响应机制的疑惑。然而,他没有对“需要编写更多代码”这一观点进行充分解释。最终目标不仅仅是编写更少的代码,而是用最少量的代码明确地表达应用的意图。如果某项技术所强调或唯一提供的就是“简单性”,那么你可能就忽视了一些关键的细节。这些问题迟早会从其他角度出现。

  令人遗憾的是,大多数人几乎没有多余的时间来做这些事情。我不怪罪于他们,每个人都有自己的局限性。但这种局面每年令我感到越发沮丧。难道你们不觉得对于 HOCs、render-props、不断变化的自定义语法以及其它一些很快就被我们遗弃的技术,浪费的时间太可惜了?

  我越来越认为这其中有直接的联系:应用程序的开发和维护仍然是一项困难和成本高昂的任务。而我们却在消耗精力进行各种妥协,而非学习如何更有效地利用至少一个平台。

  人们常对 React 的 JSX 语法、Vue 的模板语法,或者 Svelte 的组件方式有不少批评。这些批评并非没有道理,但更重要的一点是:它们受到质疑并不是因为缺乏优势。而是因为这些方案本质上是不准确的编程抽象。各框架之间的差异远非表面上看起来那么简单。

  首先,让我们聊聊视图树(View Tree)内部的 if 语句。是否可以用空值或某种插件组件作为回退呢?这是一种指令还是模板块?需要明确的是,这种设计在 DOM API 中是不存在的,只能称其为一种权宜之计。问题不仅在于命名,更在于整个概念的设立。例如,v-if 和 {#if ...} 就是这样的表现。

  Vue 无疑是这一现象的重要代表。你有两种选择:要么频繁地创建和销毁组件,要么简单地隐藏组件(通过 display: none 等)。在 2023 年,这种实践已经过时,是对浏览器和 DOM API 的一种不尊重。

  当然,问题并非仅存在于 Vue。以 React 为例,其函数组件由于 hooks 机制的特性,内容经常充满副作用。这导致了即使没有必要,也会过度依赖重新渲染来重新计算副作用和更新数据。

  常见的观点是:“React 会导致额外的重新渲染,但其核心机制目的是优化性能并保持 UI 与应用数据的同步。”然而,实际操作中,大多数开发者都在努力减少重新渲染。像 useMemo 这样的优化措施,并不能保证避免额外的渲染。

  显然,这些都是权宜之计和不必要的妥协。快速和优化的重新渲染并不能解决根本问题。真正的解决方案应在于消除重新渲染这一现象。

  这可以通过整个界面树的静态初始化来达到。简言之,每个元素(或更准确地说,堆栈内元素的回调函数)应只计算和调用一次,以实现响应式值和节点的绑定。此后,只需根据 DOM 结构图来处理流程和事件。

  各种自定义语法、模板和指令显现眼前。然而,谁能确保这些实现在未来不会发生变化呢?事实上,这种情况在过去已经出现过,如 React 和 Vue。如果这些技术一旦失去主流地位,又将如何防止它们沦为难以维护的遗留系统?

  这引发出一个新的问题:为什么无论是开发者还是框架的创造者,都持续地采用与平台习惯相违背的技术?

  继续讨论现有问题的潜在解决方案,让我们将目光转向 DOM API。这是一个历经多年精研且功能丰富的库。有些功能实际上是你不能仅通过属性(props)来规避的。

  这里需要指出的是,许多框架或库在组件设计时,没有给予 DOM 节点状态管理足够的重视。实际上,组件自身应当负责管理与 DOM 树连接的节点以及这些节点的子节点的状态,而不应该由外部模块来进行。考虑以下代码示例:

  这里并没有使用任何特殊的语法或扩展,也没有试图隐藏任何基础逻辑。它仅仅是一个用于便捷 DOM 操作的普通 JavaScript 函数。在应用程序中,这样的组件依然可以像普通函数一样使用:

  这种设计思路受到了 SwiftUI 和 Flutter 的影响。其中,第二个回调参数是 SwiftUI 的嵌套组件块的替代品,而 visible 属性则与 Flutter 中的同名属性相对应。值得注意的是,这里的 visible 并非 Vue 的“hack”,而是用于直接插入或移除 DOM 子树的属性。

  总而言之,我们无需额外发明抽象语法来模拟我们需要的行为。JavaScript 作为前端开发的“本土”语言,拥有其独特的优势和功能。试图用替代方案来规避它,最终只会使问题变得更加复杂。这一点在以往的开发实践中已经得到了充分的证明。

  在当然,visible 属性的应用逻辑相当直观。接着要解释如何渲染一个组件列表ui设计

  更值得注意的是,代码中所有用到的变量或属性都支持响应式更新。这意味着,当用户列表或者其相关属性有所改变,这些改变会即时反映在最终的布局中。

  此外,这种 list 方法实现并不像表面上看起来那么简单。系统会预生成用于该应用的模板(这里的模板指的是 JS 模板,与 Vue 或其他框架的模板不同)。所以,每当响应式变量 users 发生变化时,我们只需利用已经预设好的模板生成一个新的实例,而不是在运行时重新计算所有元素。

  但不幸的是,许多现代解决方案利用虚拟 DOM 和调和(Reconciliation),引入了阶段来双重检查从组件返回的结构的变化。这就导致了重绘和性能问题。以及一些人为的约束。不得不说,Svelte 做得很好。Svelte 不依赖于虚拟 DOM,而是使用编译器将组件转换为 JavaScript。这个 JS 代码会非常高效,但是,遗憾的是,其他问题也出现了:不必要的构建步骤,Svelte 特有的代码并没有真正从最终的包中移除。而且我们仍然有重渲染的问题。

  对于事件处理器和属性规范,该如何优雅地管理呢?以下是一个实用的代码示例:

  在这段代码中,changeUsername 和 changePassword 是用于响应用户输入并动态更新相应值的事件处理器。而 fields 是一个包含相关属性的响应式对象。实际上,这个 fields 对象就是一个数据存储,不论个人喜好如何。我们还采用了 map() 方法来创建一个派生属性,这在 Svelte 中对应 $derived。这个派生属性会在用户名或密码发生变更时同步更新,从而改变提交按钮的状态。

  首先,代码中并没有什么异常的内容,这些都是基础的 JavaScript 函数。具体来说:

  classList - 一个包含节点类名的数组,该名称与 DOM API 的官方命名 一致。

  spec - 实质上是一个包装函数,用于描述节点属性类别。当组件的回调函数内有子元素时,你可以在组件的最外层(或者回调函数中的任何地方,尽管这并不是重点)设定一组属性。

  确实,相比于 React、Vue、Svelte、Solid 等,这种方式更显繁琐。但这样的设计方式不会让你对前端的复杂性有所误解,也不会给你一个所谓的“简单解决方案”。事实上,你应该面对这些现实,而不是逃避。这会让你更清晰地了解应用是如何构建的。虽然这种方法比较繁琐,但它真的复杂到让你难以理解吗?我相信你完全能够理解每一行代码的作用。

  其次,你并不需要直接操作 DOM API。你真正需要的是一个简洁的 JavaScript API 用于与 DOM 进行交互。我坚信,视图树的管理应该由原生工具来完成。那些需要手动添加、删除、更新树结构的操作都可以由底层技术来完成。

  我要再次强调,我的目的不是推崇某个特定的新技术解决方案。相反,我希望能指出现有方案中存在的问题,并讨论如何用原生工具来解决这些问题,而无需重新发明轮子。

  最后,我想提醒大家,尊重你所使用的平台是很重要的。其他平台的开发人员都已经学会了如何与他们的平台和谐共处。与此不同,前端开发人员有时会尝试用新的、还不够成熟的解决方案来解决问题平面设计。

  用简单的例子来展示实际场景是有难度的,因为某些问题在简单的例子中可能不会显现。

  无需深究这里的 createStore 和 createEvent,Store 本质上是一个响应式数据结构,而 Event 则是用于修改这些数据或触发某种效果的信号,这些都可以从任何库中获取。

  这里的关键点是如何描述视图和视图逻辑。即使视图描述存在差异,也不意味着一定要寻求全新的解决方案。你是否确信现有方案已经是最优的?如果不是,你能明确指出原因吗?

  你也许会误以为我对现有的主流解决方案持批判态度,但事实并非如此。我相信这些技术在一定程度上都是必要的。或者说,曾经是必要的,至少对于一般的前端开发而言。但我不喜欢的是,我们似乎陷入了过去十年的思维模式,没有人在主流中试图提醒开发者注意这个问题。结果,我们的应用程序仍然没有可重现性,而且即使是简单的任务,也需要很高的劳动强度。

  我并不建议我们抛弃所有现有的解决方案,这样做是愚蠢的。我也不建议每次都自己手动操作 DOM。这些工作应由库 / 框架 / 技术 / API / 或其他何种形式来完成。我只想说,也许是时候停止实施存在严重设计缺陷的独特的“雪花”类型解决方案了?并开始利用我们自己的平台提供给我们的东西,发挥其作用。也许不是以之前呈现的形式,但以某种其他形式。至少在我看来,存在潜在的可能性。

  然而,很多人并没有认识到当前做法的局限性,反而继续在一些独特的但有严重设计缺陷的解决方案中做选择。

  前端开发者应该尊重自己的平台,不要被过时的技术所束缚,而要勇于面对现实和进行技术创新。

  你认为 UI 架构在过去十年停滞的原因是什么?你认为应该从哪些方面进行创新性突破?欢迎在评论区留言讨论。

  欢迎参与 CSDN 重磅发起的《2023 AI 开发者生态调查问卷》,分享您真实的 AI 使用体验,更有精美好礼等你拿!Bsport体育Bsport体育Bsport体育Bsport体育