十年过去了,UI 框架还停滞在原地……

| CSDN(ID:CSDNnews)

Svelte 的全新篇章

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

令我不解的是,人们在解决 UI 问题时仍采用同样的权宜之计。

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

让我们从最后一个问题开始探讨。

HTML 究竟是不是合适的抽象层?

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

属性(Attributes)

事件处理器(Event Handlers)

样式(Styles)

自定义数据属性(Data-* Attributes)

可见性(Visibility)

文本内容(Text Content)

子节点(Children)

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

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

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

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

实际上,选择onClick={…}, on:click={…}和 @click=”…”并没有多少差异。

这种解决方案之所以存在,某种程度上是可以理解的:

技术栈使用起来简单,但设计困难。

早期应用通常较为简单,因此基础的模板即可满足 DOM API 的需求。

这种代码的性能直到近几年(约 4-5 年)才真正达到了足够好的水平。

主要原因实际上是:

你需要投入大量时间进行试验,并愿意接受新的现实和当前方法的不足。

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

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

被“篡改”的语法

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

下面我将通过代码示例来阐明我的观点:

React

function Component {

return (

div

h1Hey there/h1

/div

Vue

template

div

h1Hey there/h1

/div

/template

Svelte

div

h1Hey there/h1

/div

实际上,它们看起来都不错。

现在,让我们尝试添加一些条件渲染:

React

function ConditionalComponent({ showMessage }) {

return (

div

{showMessage ? (

h1Hey there/h1

) : null}

/div

ConditionalComponent showMessage={true} /

Vue

templatedivh1 v-if=”showMessage”Hey there/h1/div/template

export default {name: ‘ConditionalComponent’,props: {showMessage: Boolean}}/

ConditionalComponent :showMessage=”true” /

Svelte

!– Svelte –div{#if showMessage}h1Hey there/h1{/if}/div…ConditionalComponent showMessage={true} /

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

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

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

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

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

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

当然,其他技术同样存在不足,但这是另一个话题。那么,渲染列表呢?

React

function UserList {return (divul{users.map(user = (li key={user.id}{user.name}/li))}/ul/div);}

Vue

templatedivulli v-for=”user in users” :key=”user.id”{{ user.name }}/li/ul/div/template

Svelte

divul{#each users as user (user.id)}li{user.name}/li{/each}/ul/div

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

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

深入探索 DOM API

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

当面对条件渲染的需求时,DOM API 提供了多个解决方案,如 node.append 和 node. 方法,还有 node.remove 方法以及 node.isConnected 属性。这些 API 让我们能够随时向 DOM 树添加或移除节点,并检测节点是否与 DOM 树连接。React 就是基于这样的原理进行操作的。

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

export function Component({ showMessage }) {h(‘div’, = {h(‘h1’, {text: ‘Hey there’,visible: showMessage,})})}

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

using(body, = {Component({ showMessage: true })})

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

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

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

首先,让我们看一下相关的代码实现:

export const function User({ key, name, isRestricted }) {h(‘li’, {attr: { id: key },text: name,visible: isRestricted,classList: [“border-gray-200”]})}

using(document.body, = {h(‘ul’, = {list(users, ({ store: user, key: idx }), = {User({ key: idx, name: user.name, isRestricted: user.isRestricted })})})})

这种实现方式在某种程度上借鉴了 SwiftUI 的设计:

List(users) { user in// 使用 user}

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

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

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

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

using(document.body, = {h(‘section’, = {spec({ style: {width: ’15em’} });

h(‘form’, = {spec({handler: {config: { prevent: true },on: { submit },},style: {display: ‘flex’,flexDirection: ‘column’,},});

h(‘input’, {attr: { placeholder: ‘Username’ },handler: { input: changeUsername },});

h(‘input’, {attr: { type: ‘password’, placeholder: ‘Password’ },classList: [‘w-full’, ‘py-2’, ‘px-4’],handler: { input: changePassword },});

h(‘button’, {text: ‘Submit’,attr: {disabled: fields.map(fields = !(fields.usernamefields.password),),},});});});});

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

对于这段代码,你初次浏览可能会有以下几种看法:

这种写法不太常见

代码过于繁琐

需要手动处理 DOM API 的各个细节

然而,事实真的是这样吗?

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

attr – 用于定义节点属性的对象。

style – 用于设置节点样式的对象。

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

handler – 节点事件处理器的配置对象,如你所见 config: { prevent: true }。

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

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

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

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

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

事情并没那么简单

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

比如,在一个真实应用中,你可能会这样描述一个表单:

export const Auth = = {h(“div”, = {spec({classList: [“mt-10”, “max-w-sm”, “w-full”],});

h(“form”, = {Input({type: “email”,label: “电子邮件”,inputChanged: authForm.fields.email.changed,errorText: authForm.fields.email.$errorText,errorVisible: authForm.fields.email.$errors.map(Boolean),});

Input({type: “password”,label: “密码”,inputChanged: authForm.fields.password.changed,errorText: authForm.fields.password.$errorText,errorVisible: authForm.fields.password.$errors.map(Boolean),});

Button({text: “创建”,event: authForm.submit,size: “base”,prevent: true,variant: “default”,});

ErrorHint($authError, $authError.map(Boolean));});});};

export const Input = ({value,type,label,required,inputChanged,errorVisible,errorText,}: {value?: Storestring;type: string;label: string;required?: boolean;inputChanged: Eventany;errorVisible?: Storeboolean;errorText?: Storestring;}) = {h(“div”, = {spec({classList: [“mb-6”],});

h(“label”, = {spec({classList: [“block”, “mb-2”, “text-sm”, “font-medium”, “text-gray-900”, “dark:text-white”],text: label,});});

h(“input”, = {const localInputChanged = createEventany;sample({source: localInputChanged,fn: (event) = event.target.value,target: inputChanged,});

spec({classList: [“bg-gray-50″,”border”,”border-gray-300″,”text-gray-900″,”text-sm”,”rounded-lg”,”focus:ring-blue-500″,”focus:border-blue-500″,”block”,”w-full”,”p-2.5″,”dark:bg-gray-700″,”dark:border-gray-600″,”dark:placeholder-gray-400″,”dark:text-white”,”dark:focus:ring-blue-500″,”dark:focus:border-blue-500″,],attr: { type: type, required: Boolean(required), value: value || createStore(“”) },handler: { on: { input: localInputChanged } },});});

ErrorHint(errorText, errorVisible);});};

export const ErrorHint = (text: Storestring | string | undefined, visible: Storeboolean | undefined) = {h(“p”, {classList: [“mt-2”, “text-sm”, “text-red-600”, “dark:text-red-400”],visible: visible || createStore(false),text: text || createStore(“”),});};

使用带有标签、属性和动态内容的预定义卡片来描述一些日志列表怎么样?

export const LogsList = = {h(“div”, = {spec({classList: [“flex”, “flex-col”, “space-y-6”, “mt-2”],});

list(logModel.$logsGroups, ({ store: group }) = {CardHeaded({tags: group.map((g) = g.tags),href: group.map((g) = `${g.schema_name}/${g.group_hash}`),content: = {LogsTable(group.map((g) = g.logs));},withMore: true,});});});};

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

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

为什么我们需要改变思维方式

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

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

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

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

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

参考链接

runes 的详细介绍:

node.append:

node.:/

node.remove:

node.isConnected:

DOM API 的官方命名:

声明:本文内容整理自网络,观点仅代表原作者本人,投稿号仅提供信息发布服务。如有侵权,请联系管理员。

投稿号的头像投稿号注册会员
上一篇 2023年10月8日
下一篇 2023年10月8日

热点推荐

  • 熊猫站立身高(熊猫站)

    大家好,今天给各位分享熊猫站的一些知识,其中也会对熊猫站立身高进行解释,文章篇幅可能偏长,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在就马上开始吧! 候家塘到长沙地质中学坐地铁到哪站下车? 您好,从候家塘到长沙地质中学,您…

    2023年5月15日
    590
  • 励志短句(励志短句)

    1、梦想这东西和经典一样,永远不会因为时间而褪色,反而更显珍贵。2、我们有时从错误中学到的东西,可能比从美德中学到的还要多。3、莫找借口失败,只找理由成功。不为失败找理由,要为成功找方法。4、既然一切都会过去,那我们一定要抓住现在…

    2022年10月12日
    850
  • 我忘记第几次陷入失落可我们再也不会见面了是什么歌(我忘记第几次陷入失落)

    抖音作为最受欢迎的短视频软件,里面的歌曲传唱度非常高,最近一段时间我忘记第几次陷入失落可我们再也不会见面了非常火爆,不少小伙伴在问我忘记第几次陷入失落是什么歌?谁唱的?歌词完整版有吗?下面小编为大家带来我忘记第几次陷入失落歌名及歌…

    2023年7月7日
    480
  • 请问type-c接口是什么意思?

    01 type-c是最新的USB接口外形标准,这种接口没有正反方向区别,可以随意插拔。另外,Type-C是一种既可以应用于PC(主设备)又可以应用于外部设备(从设备,如手机)的接口类型,这是划时代的。 USB接口有三种不同外观的接…

    2023年8月24日
    430
  • 埃隆·马斯克确认特斯拉Cybertruck支持双向充电

    埃隆·马斯克近日确认,特斯拉的Cybertruck将支持双向充电,能够通过车尾的电源插座为露营车等实现车对车充电。然而,特斯拉并未透露关于双向充电的具体细节。 据网友Greggertruck发布的宣传彩页显示,Cybertruck…

    2023年7月16日
    450
  • 百度竞价暴利网赚深度解析!(百度推广怎么赚钱)

    百度推广怎么赚钱(从月入3000到月赚10万,百度竞价暴利网赚深度解析!) 作为从业者,我们一定听说过百度竞价暴利网赚项目,那是一个神秘而又让SEMer向往的方向,仿佛可以看到大把大把的钞票和将自己的所学转化成价值的幸福感。 一个…

    2022年10月11日 热点
    940
  • 大家一起肺痒痒歌词是什么(大家一起肺痒痒歌词)

    最近网上又有很多好听的歌曲备受关注,不过也有部分网友对这些歌曲的歌词部分感兴趣。大家一起肺痒痒歌词是什么?近日关于大家一起肺痒痒这首歌曲网上人气挺高的,因此有不少网友通过这句歌词来查询歌曲信息,下面来看下关于大家一起肺痒痒歌词完整…

    2023年9月20日
    390
  • 营业执照年度申报资金数额怎么填(营业执照年度申报)

    今天,我想和大家分享一些关于营业执照年度申报以及营业执照年度申报资金数额怎么填的问题。以下是小编对这个问题的总结。让我们看一看。 营业执照怎么申报年检 1、营业执照年检流程如下:(1)企业申领、报送年检报告书和其他有关材料;(2)…

    2023年6月13日
    530
  • 台风“泰利”来袭 澳门悬挂八号风球

    台风“泰利”来袭 澳门悬挂八号风球 受台风“泰利”(强热带风暴级)的影响,澳门地球物理暨气象局17日早上5时30分发出八号风球。从早上5时30分起,澳门特区进入实时预防状态。 澳门气象部门预计八号风球将在17日上午维持,风力将会增…

    2023年7月30日
    470
  • 女子寺庙里晕倒无人敢扶?官方回应

    5月2日,一段“大岭山观音寺一女子晕倒无人敢扶”的视频引发关注。 据网传视频显示,一女子在该寺庙的广场一旁跪拜完后,突然起身转圈,手舞足蹈地移向广场中间位置,后倒地口吐白沫。从视频看,其身旁有多名游客围观,但没一人敢上前给她帮助。…

    2023年5月8日
    590