复杂性管理

保持 Web 应用简单明了可能会非常复杂。此单元将介绍 Web API 如何与线程处理,以及如何将其用于常见的 PWA 模式(例如状态管理)。

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

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

但需要强调,简单明了并不在于:

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

一个事物是否简单取决于它的相互关联程度。

复杂性源自绑定、编织,或者用里奇的术语来说,就是将事物拼合在一起。您可以通过计算项目管理的角色、任务、概念或维度的数量来计算复杂性。

简洁性在软件开发中至关重要,因为简单的代码更易于理解和维护。Web 应用也需要简洁,因为它有助于提高我们的应用的速度,并使其在任何可能的环境中都可访问。

管理 PWA 复杂性

我们为 Web 编写的所有 JavaScript 都会在某个时间点接触主线程。然而,主线程带来大量开箱即用的复杂问题,作为开发者,您无法控制这些复杂任务。

主线程为:

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

正如 Surma 在其 2019 年 Chrome 开发者峰会演讲中所说,主线程工作过多,而且收入不足。

不过,大多数应用代码也存在于主线程上。

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

如果互动无法与输入连接、丢帧或者需要很长时间才能使用网站,用户会感到失望,认为应用无法正常运作,对应用的信心会降低。

坏消息?增加主线程的复杂性几乎可以肯定地说,难以实现这些目标。好消息是,因为主线程需要执行的操作很明确:您可以将其作为指导,帮助降低对应用其余部分的依赖。

分离关注点

Web 应用可执行多种不同工作,但一般来说,您可以将其分解为直接涉及界面的工作,而不是直接涉及界面的工作。界面工作具有以下特点:

  • 直接处理 DOM。
  • 使用涉及设备功能的 API,例如通知或文件系统访问。
  • 涉及身份信息,例如用户 Cookie、本地存储或会话存储。
  • 管理媒体,例如图片、音频或视频。
  • 存在安全隐患,需要用户干预才能批准,例如 Web 串行 API。

非界面工作可能包括以下内容:

  • 纯粹的计算。
  • 数据访问(提取、IndexedDB 等)。
  • 加密。
  • 消息功能。
  • 创建或处理 Blob 或数据流。

非界面工作通常被界面工作所预订:用户点击按钮会触发对 API 的网络请求,该 API 会返回解析的结果,然后用于更新 DOM。在编写代码时,通常会考虑这种端到端的体验,但通常不会考虑该流程的每个部分。界面工作和非界面工作之间的界限与端到端体验一样重要,因为它们是您降低主线程复杂性的首要位置。

专注于一项任务

简化代码的一种方式是,分解函数,让每个函数专注于一项任务。可以通过浏览端到端体验确定的边界来确定任务:

  • 首先,响应用户输入。这属于界面工作。
  • 接下来,发出 API 请求。这是一项非界面工作。
  • 接下来,解析 API 请求。同样,这属于非界面工作。
  • 接下来,确定对 DOM 所做的更改。这可能是界面工作的原因,或者,如果您使用的是虚拟 DOM 实现之类的功能,界面就可能无法正常运行。
  • 最后,更改 DOM。这属于界面工作。

首先,界面工作和非界面工作之间有着明确的界限。然后,还需要进行判断调用:发出并解析一个 API 请求是完成一项任务还是两项任务?如果 DOM 更改属于非界面工作,它们是否会与 API 工作捆绑在一起?在同一消息串中?在其他消息串中?此处适当的分离级别对于简化代码库以及能够成功地将其部分内容移出主线程至关重要。

可组合性

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

  • 对应用执行的工作类型进行分类。
  • 为这些设备构建通用的输入和输出接口。

例如,所有 API 检索任务均接受 API 端点并返回一组标准对象,所有数据处理函数均接受并返回一组标准对象。

JavaScript 提供结构化克隆算法,用于复制复杂的 JavaScript 对象。Web 工作器使用它发送消息,而 IndexedDB 使用它来存储对象。选择可与结构化克隆算法搭配使用的接口可提高其运行灵活性。

考虑到这一点,您可以通过对代码进行分类并为这些类别创建通用 I/O 接口来创建可组合功能库。可组合代码是简单代码库的特点:松散耦合、可互换的部分可以彼此“相邻”并相互构建;相比之下,复杂代码深度互连,因此无法轻松分离。在 Web 上,可组合代码可能决定着主线程是否过度劳累。

有了可组合代码后,就可以从主线程中移除部分代码了。

使用 Web Worker 降低复杂性

Web Worker 是一种通常未得到充分利用但广泛使用的 Web 功能,可让您将工作移出主线程。

Web Worker 可以让 PWA 在主线程之外运行(某些)JavaScript。

有三种类型的 worker。

专用工作器是描述 Web 工作器时最常用的概念,可通过在单个正在运行的 PWA 实例中由单个脚本使用。应尽可能将不与 DOM 直接交互的工作移至 Web Worker,以提高性能。

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

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

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

自己试试

代码时间到!根据您在本单元中学到的所有内容,从头开始构建 PWA。

资源