复杂性管理

保持 Web 应用的简单性可能出奇地复杂。在本模块中,您将了解 Web API 如何与线程一起工作,以及如何将其用于常见的 PWA 模式,例如状态管理。

简单性与复杂性

Rich Hickey 在他的演讲 Simple Made Easy 中讨论了简单事物与复杂事物的品质。他将简单事物描述为专注于

“一个角色、一项任务、一个概念或一个维度。”

但强调简单性并非关于

“拥有一个实例或执行一个操作。”

某事物是否简单取决于它的互连程度。

复杂性来自绑定、编织,或者用 Rich 的术语来说,将事物复杂化地组合在一起。您可以通过计算某事物管理的角色、任务、概念或维度的数量来计算复杂性。

简单性在软件开发中至关重要,因为简单的代码更容易理解和维护。简单性对于 Web 应用也是必要的,因为它可能有助于使我们的应用在每种可能的上下文中都快速且易于访问。

管理 PWA 复杂性

我们为 Web 编写的所有 JavaScript 代码都会在某个时候触及主线程。然而,主线程本身就带有许多开箱即用的复杂性,而您作为开发人员无法控制这些复杂性。

主线程是

  • 负责绘制页面,这本身就是一个复杂的多步骤过程,包括计算样式、更新和合成图层以及绘制到屏幕。
  • 负责侦听和响应事件,包括滚动等事件。
  • 负责加载和卸载页面。
  • 管理媒体、安全性和身份。这都是在您编写的任何代码甚至可以在该线程上执行之前完成的,例如
  • 操作 DOM。
  • 访问敏感的 API,例如,设备功能或媒体/安全/身份。

正如 Surma 在他的 2019 Chrome 开发者峰会演讲 中所说,主线程工作过度且报酬过低。

然而,大多数应用程序代码也存在于主线程上。

所有这些代码都增加了主线程的复杂性。主线程是浏览器唯一可以用来在屏幕上布局和渲染内容的线程。因此,当您的代码需要越来越多的处理能力才能完成时,我们需要快速运行它,因为执行应用程序逻辑的每一秒都是浏览器无法响应用户输入或重绘页面的秒数。

当交互未连接到输入、当帧丢失或使用网站时间过长时,用户会感到沮丧,他们会觉得应用程序已损坏,并且他们对其的信心会降低。

坏消息?向主线程添加复杂性几乎是肯定会使实现这些目标变得困难的方法。好消息?因为主线程需要做什么很明确:它可以作为指导,帮助减少应用程序其余部分对其的依赖。

关注点分离

Web 应用程序执行许多不同种类的工作,但从广义上讲,您可以将其分解为直接触及 UI 的工作和不触及 UI 的工作。UI 工作是

  • 直接触及 DOM。
  • 使用触及设备功能的 API,例如,通知或文件系统访问。
  • 触及身份,例如,用户 Cookie、本地存储或会话存储。
  • 管理媒体,例如,图像、音频或视频。
  • 具有安全含义,需要用户干预才能批准,例如 Web Serial API。

非 UI 工作可以包括

  • 纯计算。
  • 数据访问(fetch、IndexedDB 等)。
  • 加密。
  • 消息传递。
  • Blob 或流创建或操作。

非 UI 工作通常以 UI 工作为开头和结尾:用户单击一个按钮,该按钮触发对 API 的网络请求,该 API 返回解析后的结果,然后用于更新 DOM。编写代码时,通常会考虑这种端到端体验,但该流程的每个部分通常位于何处则不考虑。UI 工作和非 UI 工作之间的边界与端到端体验一样重要,因为它们是您可以减少主线程复杂性的第一个地方。

专注于单一任务

简化代码的最直接方法之一是分解函数,以便每个函数都专注于单一任务。可以通过遍历端到端体验来确定的边界来确定任务

  • 首先,响应用户输入。这是 UI 工作。
  • 接下来,发出 API 请求。这是非 UI 工作。
  • 接下来,解析 API 请求。这又是非 UI 工作。
  • 接下来,确定对 DOM 的更改。这可能是 UI 工作,或者如果您使用虚拟 DOM 实现之类的东西,则可能不是 UI 工作。
  • 最后,对 DOM 进行更改。这是 UI 工作。

第一个明确的边界是 UI 工作和非 UI 工作之间。然后需要做出判断调用:发出和解析 API 请求是一项任务还是两项任务?如果 DOM 更改是非 UI 工作,它们是否与 API 工作捆绑在一起?在同一线程中?在不同的线程中?此处的适当分离级别对于简化您的代码库以及能够成功地将部分代码移出主线程至关重要。

可组合性

要将大型端到端工作流程分解为较小的部分,您需要考虑代码库的可组合性。从函数式编程中汲取灵感,考虑

  • 对应用程序执行的工作类型进行分类。
  • 为其构建通用的输入和输出接口。

例如,所有 API 检索任务都接收 API 端点并返回标准对象数组,并且所有数据处理函数都接收和返回标准对象数组。

JavaScript 有一个 结构化克隆算法,用于复制复杂的 JavaScript 对象。Web Worker 在发送消息时使用它,IndexedDB 使用它来存储对象。选择可以与结构化克隆算法一起使用的接口将使它们在运行时更加灵活。

考虑到这一点,您可以通过对代码进行分类并为这些类别创建通用 I/O 接口来创建可组合功能库。可组合代码是简单代码库的标志:松散耦合、可互换的部件可以“并排”放置并相互构建,而不是深度互连且因此无法轻易分离的复杂代码。在 Web 上,可组合代码可能意味着过度使用主线程或不使用主线程之间的区别。

有了可组合代码,现在可以将其中一些代码移出主线程了。

使用 Web Worker 减少复杂性

Web Worker 是一种经常被低估但广泛可用的 Web 功能,可让您将工作移出主线程。

Web Worker 让 PWA 在主线程之外运行(部分)JavaScript。

有三种类型的 Worker。

专用 Worker 是描述 Web Worker 时最常想到的,可以由 PWA 的单个运行实例中的单个脚本使用。在可能的情况下,不直接与 DOM 交互的工作应移至 Web Worker 以提高性能。

共享 Worker 与专用 Worker 类似,不同之处在于多个脚本可以在多个打开的窗口之间共享它们。这提供了专用 Worker 的好处,但窗口和脚本之间具有共享状态和内部上下文。

例如,共享 Worker 可以管理 PWA 的 IndexedDB 的访问和事务,并在所有调用脚本中广播事务结果,以使它们能够对更改做出反应。

最后一个 Web Worker 是本课程广泛介绍的一种:Service Worker,它充当网络请求的代理,并在 PWA 的所有实例之间共享。

亲自尝试

是时候编写代码了!根据您在本模块中学到的一切,从头开始构建一个 PWA。

资源