发布于 2026-01-06 5 阅读
0

Effector 新手指南 DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

Effector 新手指南

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

本文将解答一些常见问题,并澄清关于状态管理器effector.js的一些常见误解。

你为什么需要它?因为它是一款能够真正帮助前端工程师简化日常工作的工具。毕竟,有了它,你几乎可以完全摆脱 props、props 类型、组件内部的业务逻辑,无需学习十几个其他运算符,也无需使用代理或装饰器,同时还能获得市面上最强大的数据流管理工具,而它仅仅提供函数和对象。

唯一的问题在于如何获得入门级的技术知识,因为你需要稍微转变一下思维方式。我相信我已经找到了一种更温和的入门方法,所以我在这篇文章中发布了完整的指南。

该应用程序是一个系统

是的,这是理解这一切为何必要的一个重要细节。

让我们一步一步地来探讨这个论点:

1)这些应用程序本质上是完整的吗?是的

2)应用程序可以根据某个特定功能进行划分吗?是的

3)哪一个?职责范围

4) 职责范围之间是否相互关联?是的,当然关联,因为它们是特定应用程序的组成部分。此外,它们之间还会相互作用。

5)什么是系统?系统是指相互关联的事物(责任领域)的集合,这些事物彼此相互作用。

仅用了5个步骤就得出了这篇论文。好!

返回效应器

我特意在开头强调了“数据流”这个词,因为在 JavaScript 生态系统中,状态管理更为常见,这容易导致误解。状态只是构建业务逻辑的一个单元。

说到单元,Effector 提供了四个单元,可用于构建任何复杂度的业务逻辑:事件、存储、效果和领域。

单位:事件

首先也是最重要的一点。事实上,作为一线运维人员,我们身处一个事件驱动的环境(DOM)。在构建Web应用程序的业务逻辑(即与DOM相关的逻辑)时,如果关注其他模型,那就显得很奇怪了。

即使在与管理层(产品负责人、首席执行官等)进行规划时,我们也会听到这样的措辞:“用户进入页面,我们酷炫的新功能就启动了!”(隐含的意思是事件)

根据词典确定事件

单位:商店

用于存储值的对象。必须设置默认值(除 undefined以外的任何值)。当收到重复值(与前一个值相同)时,store不会触发更新。

传入事件的处理程序是一个 reducer(我们改变当前状态),如果处理程序返回 undefined,则不会触发更新。

考虑到之前对责任范围的划分,可以提出以下建议:

整个应用程序不需要任何单独的商店。我是认真的。

每个职责范围内都有独立的简易商店。

如有必要,合并并不困难。

单位:效果

最难理解的单元

从技术上讲,效果至少具备以下属性之一:

-对系统外部环境的影响(服务器请求、本地存储等)

  • 受环境(process.env)影响

但从概念上讲,如果一个事件是每次都能成功触发的事情,那么该效果也提供了一种处理异常的方法(即不能保证处理程序会成功完成)。

我们什么时候可以捕获异常?

网络请求

-从本地存储工作

-与第三方API的交互

-一段随机代码片段,开发者需要在其中编写显式抛出异常。

该效果为我们提供了一个处理程序,所有此类可疑的代码片段都将存储在其中。

因此,通过执行处理函数,该效果会发出成功( `.done`)或失败(`.fail` )的事件。在执行过程中,还有一个布尔值 ` .pending`字段可用,该字段会清晰地指示该效果是否正在进行中。

对于那些不在乎结果的人来说,.finally事件是贴心提供的,并且总是会发出。

常规单位

以上提到的三个单位均为常规单位。

这是一项重要的澄清,因为从现在开始,短期内将使用该术语。

单位:域

域是所有常规单元的命名空间。

它提供了用于创建与此域关联的常规单元的钩子。这对于批量操作非常有用。
可以在域内自由创建域。域内的所有单元都可以通过`domain.history`输出。

PS 域对于 SSR 是必需的,编写涵盖我们大多数系统场景的测试时也需要用到。

数据准备

事件会在我们的系统中分发数据。
我们需要不时地对这些数据进行准备:例如,向数据中添加一些静态值,或者将接收到的数据乘以二。

完成这类任务可能需要三样东西:

1)或许,在作为发送方的常规单元和作为接收方的常规单元之间进行数据准备的最“扁平化”版本是采样运算符中的fn字段。但我会在几章之后再讨论它,因为一切都井然有序。

2) 其他选项是事件本身的方法。第一个方法是 ` event.map`,它允许你根据需要转换事件的有效负载,但有一个限制:转换后的函数必须是干净的(即,它不能包含副作用)。此事件方法将返回一个新事件,该新事件将与原始事件触发后立即发生的调用直接关联。

3) 最后一个选项是`event.prepend`。如果我们把 `.map` 作为后处理器,那么 `.prepend` 则恰恰相反,它会作为原始事件的预处理器。相应地,它会返回一个事件,该事件会执行一个转换函数,然后立即调用原始事件。这样做有什么用呢?

例如,获取某种货币余额的效果。所有货币的处理程序都相同,区别仅在于货币的静态代码。因此,可以创建一组“前置”事件,其函数转换器会将货币的静态值添加到调用参数中,从而解决问题。

存储数据准备

有时候,存储中的数据也需要进行预处理。例如,事件类型的存储就有一个 ` store.map`方法,你可以根据该方法内部的函数转换存储中的数据。这样的存储被称为计算型存储。

只有在原始数据更新的情况下才会进行计算。不多也不少。

使用场景?例如,你需要一个以关联数组(键值对)形式存储的数据和一个普通对象数组。

数据流。开始

我们已经初步探讨了如何处理单个单元内的数据。那么,当存在多个单元时又该如何处理呢?

最精彩的部分就此开始——单元的声明式连接!
最简单的操作符是`forward`
它的 API 非常清晰:`from` 和 `to` 字段,接收任何常规单元。它的执行意味着 `from`to字段显式地订阅了 `to` 字段的触发器(存储中的值更改或事件调用),`from` 字段随后将被触发。

数据流。过滤

我们有数据处理功能,以及简单的单元连接。但如果单元连接不遵循某些规则怎么办?这就需要用到守卫机制。守卫机制是一个包含三个字段的操作符:源、过滤器和目标。

源是发起通信的常规单元。

过滤器是它们通信中的规则。它接受一个谓词函数,用于检查来自源的数据是否为真。此外,谓词函数还可以接受一个布尔值存储。

Target 是一个常规单元,当过滤器返回真值时,它会立即从源接收数据。

但如果过滤还不够,您不仅需要过滤,还需要在结果为真时以某种方式转换有效负载,该怎么办?event.filterMap 可以帮到您。

好的,这一切都很棒,但是你看到的是一对一的单元链接,但是如果一个事件需要根据接收者的不同条件连接到多个事件呢?

这里还有一份食谱!分机操作员随时为您服务。

数据流。信号

经常会出现这样的情况:各个单元需要连接起来,但不仅要直接连接,甚至不需要通过条件连接,而是通过信号连接!或者更准确地说,是通过任何一个常规单元的触发器连接。

最明显的例子是组件挂载(挂载突然变成了一个事件),从某个存储中获取数据并调用效果。

sample({
  source: $store,
  clock: mount,
  fn: someCombinatorFn,
  target: effectFx
})
Enter fullscreen mode Exit fullscreen mode

时钟是关键字段。必要的信号就放置在这里。

正如我之前承诺的那样,我们将回归通过样本进行数据准备的方式

问题在于,除了这三个字段之外,示例组合器函数中还有一个可选字段 fn。它接受两个参数:源有效载荷和目标有效载荷clock(如果未指定则为 undefined)。此外,我们可以根据当前任务自由组合和转换这些值,当然,前提是不能超出此函数的纯粹性范围。

数据流组织

我们学会了如何在系统中构建任意复杂度的数据路径。但数据流的组织方式仍然存在疑问。我建议采用最简单也最直接的方案——按职责范围划分。

因此,我们有一个包含所有业务逻辑的文件夹。它根据相应的职责范围划分成多个子文件夹。

每个职责范围包含 2 个文件(通常少于 3 个,当存储位于单独的文件中时)。

第一个是索引文件,其中包含效应器所有单元的声明(createEvent,,)。createStorecreateEffect

第二个文件是初始化文件,它不会导出任何内容,只会导入。该文件的内容如下:

1)效果处理器

2) 存储相应作用域的处理程序

3)相邻职责范围(转发、守卫、拆分、采样)单元之间的交互。在考虑将连接放在哪个职责范围时,只需问自己一个问题:“是谁发起的这个连接?把它放在那里。

因此,在包含所有业务逻辑的文件夹根目录下,我们创建一个根初始化文件,并将所有职责范围内的初始化文件导入其中。然后,我们将这个根初始化文件导入到应用程序的根目录,并静态地初始化整个应用程序的架构图!

我们构建出图表了吗?结果表明,我们已经构建好了。

PS:如果您觉得职责范围文件开始变得很多,这并不是一个坏方法,而是您错过了职责范围变成多个职责范围的时机。

附注:我也在这里进行了更详细的描述。

重用和环境相关的代码

有时,我们可能会在数据流中使用某些功能,甚至在多个职责范围内使用事件。

我们该怎么办?放在哪里?放在 utils 目录下吗?
不行!
我们有一个名为 app 的职责范围!和其他职责范围一样,它存储着特定于该职责范围(即 app)的代码。

绑定也面临同样的问题。React 的绑定提供了一种名为 Gate 的机制。那么应该在哪里创建它们呢?是在特定的职责范围内,还是在视图中?

你应该在你的职责范围内创建它们,也就是所谓的应用程序。因为这是特定应用程序的特定代码。

初始化文件也是如此。那些触发门(挂载、组件年金或组件渲染器,门已更新其属性)的链接作为启动器应该放在那里(/app/init)。

因此,在测试过程中,您可以清楚地看到应该显式调用哪些事件(业务逻辑测试中没有像 React 那样的视图层)。

测试

我特意使用了“责任范围”这个词组,而不是简单的“领域”,以免造成混淆。因为领域指的是一个执行单元。

如果要进行业务逻辑测试,并且测试覆盖率要达到正常水平而不是单个测试,那么领域定义就变得必不可少了。

1)作为开发者,我们可以为整个系统创建一个域。

2) 将显式导入的 `<import>` createEvent、 `<import>` createStore、 `<import>` 等替换createEffect为 `myDomain.createEvent` 等。这样,整个系统就由单个域控制,并且可以进行 fork。fork(domain, config)

3) 此函数接受域和可选的配置参数,您可以在其中显式指定要模拟handlers键的副作用的处理程序,以及显式指定用于测试的存储值values

4) 调用 fork 函数将返回作用域 ( const scope = fork(domain, config)) - 您的域的虚拟实例。

5) 现在我们只需要选择场景的初始事件,并将其allSettled作为第一个参数传递给函数进行测试;第二个参数则放在脚本启动有效载荷之前。由于整个场景链可能需要超过一个游戏刻的时间,因此需要调用 allSettled 函数。

6) 通过scope.getState($store)检查被测脚本运行后系统的状态,可能需要通过测试库(例如 jest)检查事件/效果调用。

7) 您可以测试您的整个系统!

项目启动

我认为如果没有实际例子,你可能很难理解。
为此,我在夏末为 Odessa.js 和所有人制作了一个工作坊应用程序。它被分成多个分支。在主分支(master)中,你可以浏览各个章节,查看拉取请求,了解更改的内容。

文章来源:https://dev.to/effector/effector-s-beginner-guide-3jl4