NgRx: Patterns and Techniques(Ng
翻译说明: 中英对照, 意译, 重新排版 。原文链接 (需翻墙)
Managing state is one of the hardest problems when building front-end applications.
构建前端应用时, 管理状态是最困难的问题之一。
Angular supports multiple ways of doing it. For instance, we can do it in the AngularJS 1-like way and mutate the state in place.
Angular 支持多种管理状态的方式。例如,我们可以用类似AngularJS 1的方式来做,并在适当的地方改变状态。
Over the last year a different approach has been getting a lot more traction — using NgRx.
在过去的一年里,一种不同的方法得到了更多的关注——使用NgRx。
NgRx is a library built around a few key primitives and it helps us manage state. NgRx is a very simple library and does not make a lot of assumptions about how we use it. That is why knowing patterns and techniques is crucial when using NgRx, and this is what this article is about.
NgRx是一个围绕一些关键原语构建的库,它帮助我们管理状态。NgRx是一个非常简单的库,对于我们如何使用它并没有太多的假设。这就是为什么了解模式和技术在使用NgRx时是至关重要的,这就是本文的目的。
Agenda 议程
This is a long and in-depth read, so let’s look at what we are going to learn first.
这是一篇很长很深入的文章,先来看看我们要学些什么。
-
I will start with a short overview of NgRx — just to make sure we are on the same page.
我会以 NgRx 的简短概览开始 —— 仅仅是保证我们在同一页上。 -
Then, I’ll switch and talk about actions, in particular, the categories of actions we see in most applications.
然后,我将分别讨论 actions,特别是我们在大多数应用程序中看到的actions类别。 -
I’ll also talk about how to implement a request-reply pattern using correlation ids.
我也会讨论如何使用关联 id 实现请求-响应模式 -
Finally, I’ll cover the store: its building blocks, common types of deciders and transformers, and how we can compose them to handle advanced scenarios.
最后, 我会概括 store: 它的构件块, 常见的决策器和转换器类型, 以及我们如何组合它们来处理高级场景。
Short Overview of NgRx (NgRx 的简短概述)
Programming with NgRx is programming with messages. In NgRx they are called actions.
使用NgRx编程就是使用消息进行编程。在 NgRx 中, 它们被称为 actions。
To start an interaction, a component or a service creates an action, populates it with data, and then dispatches it. The component does not mutate any state in place.
要开始交互时,某个组件或服务创建一个action,用数据填充后发送出去。组件并不会改变任何状态。
@Component({
selector: 'todos',
templateUrl: './todos.component.html'
})
class TodosComponent {
constructor(private store: Store<any>) {}
addTodo(data: {text: string}) {
this.store.dispatch({
type: 'ADD_TODO',
payload: data
});
}
}
todos.component.ts hosted with ❤ by GitHub
Then the action is received and processed, which can be done in two ways.
之后, action 会被接收并处理, 这可以用两种方式做到。
First, an action can be processed by a reducer — a synchronous pure function creating applications states. It does it by applying an action to the current state of the application.
第一种, action 可以被归约器(一种创建应用状态的同步的纯函数)处理 。 归约器通过对应用的当前状态应用 action 来实现。
function todosReducer(todos: Todo[] = [], action: any): Todo[] {
if (action.type === 'ADD_TODO') {
return [...todos, action.payload];
} else {
return todos;
}
}
todos.reducer.ts hosted with ❤ by GitHub
Second, an action can be processed by an effects class. An effects class taps into the Actions object (an observable of all the actions flowing through the app) to execute the needed side effects.
第二种, action 可以被 effects class 处理。 effects class 利用 Actions 对象(流经app的所有 actions 的observable)来执行所需的副作用。
class TodosEffects {
constructor(private actions: Actions, private http: Http) {}
@Effect() addTodo = this.actions.ofType('ADD_TODO')
.concatMap((todo) => this.http.post(...));
}
todos.effects.ts hosted with ❤ by GitHub
Many interactions require both an effects class and a reducer, as shown here.
很多交互会同时需要 effects class 和 归约器, 如下所示。
class TodosEffects {
constructor(private actions: Actions, private http: Http) {}
@Effect() addTodo = this.actions.ofType('ADD_TODO').
concatMap(todo => this.http.post(…).
map(() => ({type: 'TODO_ADDED', payload: todo})));
}
function todosReducer(todos: Todo[] = [], action: any): Todo[] {
if (action.type === 'TODO_ADDED') {
return [...todos, action.payload];
} else {
return todos;
}
}
todos.effects.ts hosted with ❤ by GitHub
In this example, we first make a post request in an effects class, and then update the app state in the reducer.
此例中, 我们首先在 effects 类中发出post请求, 然后在归约器中更新应用状态。(译者注: 处理流程是这样的, 首先在 归约器中查询是否有 ADD_TODO
, 此例中, 归约器中没有此项, 然后到 effects class 中看是否有相应的操作, effects class 中有相应操作, 执行后effects class 中又派发了新的 action TODO_ADDED
, action TODO_ADDED
又进入 归约器处理, effects class 中没有 TODO_ADDED
, 处理结束)
Finally, the component often reacts to the state changes by subscribing to the store.
最后, 组件一般会通过 订阅 store 来对状态改变做出响应。
The following component gets an observable of todos. Any time a todo gets added, removed, or updated, that observable will emit a new array, which the component will display.
下面的组件获取了 todos的 一个 observable。 只要有 todo 添加, 删除, 或者更新, observable 就会发射一个新的数组, 该数组会被组件显示。
@Component({
selector: 'todos',
templateUrl: './todos.component.html'
})
class TodosComponent {
public todos: Observable<Todo[]>;
constructor(private store: Store<any>) {
this.todos = store.select('todos');
}
addTodo(data: {text: string}) {
this.store.dispatch({
type: 'ADD_TODO',
payload: data
});
}
}
todos.component.ts hosted with ❤ by GitHub
Graphically, it looks like this.
用图形表示,看起来是这样子。
(译者注: 原文这里有误。图中 side effects 和 reducer 的顺序错位了, 应该是reducer 在 side effects 前面。这里已经修正, 文章后面多处相同的错误, 不再赘述。相关文章 stackoverflow)
The component dispatches an action, which is processed in the store. This often involves deciding what should handle the action and how it should be transformed.
这个组件派发了一个 在 store 中处理的 action。 这通常涉及到决定应该处理什么action以及应该如何转换action。
This is done by the effects classes. Then we execute the needed side effects. Next, the reducers create a new state object. And, finally, the component displays the updated state.
这是通过 effects 类实现的。 然后我们执行需要的 副作用。 然后, 归约器创建的一个新的状态对象。 最后, 组件显示更新的状态。 (译者注: 此处有误, 正确的顺序应当是 action 先由 归约器处理, 然后才是 effects class)
This is, in a nutshell, how NgRx works.
简单地说,这就是NgRx的工作原理。
Now let’s look at every part of this picture in detail.
现在, 我们来详细地看下这张图的每一部分。
Action
In NgRx, an action has two properties: type and payload. The type property is a string indicating what kind of action it is. The payload property is the extra information needed to process the action.
在 NgRx 中, 一个 action 有两个属性: type 和 payload. type 属性是个可以表明 action 类型的字符串。 payload 是需要处理 action 的附加信息。
@Component({
selector: 'todos',
templateUrl: './todos.component.html'
})
class TodosComponent {
constructor(private store: Store<any>) {}
addTodo(data: {text: string}) {
this.store.dispatch({
type: 'ADD_TODO',
payload: data
});
}
}
todos.component.ts hosted with ❤ by GitHub
As in most messaging systems, NgRx actions are reified, i.e., they are represented as concrete objects and they can be stored and passed around.
如大多数消息传递系统,NgRx actions 是具体化的,例如,它们可以表示为具体对象,可以被存储和传递。
NgRx is a very simple library, so it does not make a lot of assumptions about your actions. NgRx does not prescribe one way to construct them, nor it tells us how to define types and payloads. But this does not mean that all actions are alike.
NgRx是一个非常简单的库,所以它不会对您的操作做出很多假设。NgRx没有指定一种构造它们的方法,也没有告诉我们如何定义 type 和 payload。但这并不意味着所有的 actions 都是一样的。
Three Categories of Actions 三类Actions
Actions can be divided into the following three categories: commands, documents, events.
Actions 可以被分为以下三类: 命令, 文档, 事件。
Interestingly, the same categorization works well for most messaging systems.
有趣的是, 同样的分类对于大多数消息传递系统都很有效。
Commands 命令
{
type: 'ADD_TODO',
payload: data
}
command.ts hosted with ❤ by GitHub
Dispatching a command is akin to invoking a method: we want to do something specific. This means we can only have one place, one handler, for every command. This also means that we may expect a reply: either a value or an error.
派发一个命令类似于调用一个方法:我们想做一些特定的事情。这意味着对于每个命令只能有一个位置,一个handler。这也意味着我们可能期望得到一个回复:要么是一个值,要么是一个错误。
To indicate that an action is a command I name them starting with a verb.
为了表示 action 是命令,我将它们命名为以动词开头。
Documents 文档
{
type: 'TODO',
payload: data
}
document.ts hosted with ❤ by GitHub
When dispatching a document, we notify the application that some entity has been updated — we do not have a particular single handler in mind. This means that we do not get any result and there might be more than one handler. Dispatching a document is less procedural.
在派发文档时,我们会通知应用程序某个实体已经更新——我们并没有特定的处理程序。这意味着我们不会得到任何结果,并且可能有多个handler。分发文档不那么程序化。
Finally, I name my documents using nouns or noun phrases
最后, 我使用 名词或者名词短语命名我的文档。
Events 事件
{
type: 'APP_WENT_ONLINE',
payload: data
}
event.ts hosted with ❤ by GitHub
When dispatching an event, we notify the application about an occured change. As with documents, we do not get a reply and there might be more than one handler.
在派发事件时,我们将发生的更改通知应用程序。与文档一样,我们不会得到回复,可能有多个handler。
Naming Conventions 命名约定
I found using the naming convention to indicate the action category extremely useful, but we can go further than that and impose a certain schema on an action category.
我发现使用命名约定来指示 action 类别非常有用,但是我们可以更进一步,将特定的模式强加给 action 类别。
For instance, we can say that a document must have an ID, and an event must have a timestamp.
例如,我们可以说文档必须有ID,事件必须有时间戳。
Using Several Actions to Implement a Single Interaction 使用几个 actions 实现单个交互
We often use several actions to implement an interaction. Here, for instance, we use a command and an event to implement the todo addition.
我们经常使用几个 actions 来实现一个交互。 这里, 举个例子, 我们使用一个 命令和一个事件来实现 todo 添加。
We handle the ADD_TODO command in an effects class, and then the TODO_ADDED event in the reducer.
我们在 effects class 中 处理 ADD_TODO 命令, 然后再 归约器中 处理 TODO_ADDED 事件
class TodosEffects {
constructor(private actions: Actions, private http: Http) {}
@Effect() addTodo = this.actions.ofType('ADD_TODO').
concatMap(todo => this.http.post(…).
map(() => ({type: 'TODO_ADDED', payload: todo})));
}
function todosReducer(todos: Todo[] = [], action: any): Todo[] {
if (action.type === 'TODO_ADDED') {
return [...todos, action.payload];
} else {
return todos;
}
}
model.ts hosted with ❤ by GitHub
Request — Reply 请求——响应
As I mentioned, when dispatching a command, we often expect a reply. But the dispatch method does not return anything, so how do we get it?
正如我所讲, 当派发一个命令时, 我们经常期望会有个回应。 但是派发的方法并不会返回任何东西, 那么我们怎么获取到回应呢?
Let’s look at the following component managing a todo. In its delete method we want to confirm that the user has not clicked on the delete button by accident.
看下下面的这个管理 todo 的组件。 在它的 delete 方法中, 我们想要确保用户没有误点删除 按钮。
@Component({
selector: 'todo',
templateUrl: './todo.component.html'
})
class TodoComponent {
@Input() todo: Todo;
constructor(private store: Store<any>) {}
delete() {
this.store.dispatch({
type: 'CONFIRM_TODO_DELETION',
payload: {todoId: this.todo.id}
});
}
}
todo.component.ts hosted with ❤ by GitHub
This operation will probably display some confirmation dialog, which may result in a router navigation and a URL change.
该操作可能会显示一些确认对话框, 这可能导致路由导航以及 URL 改变。
These effects are not local to this component, and, as a result, must be handled by effects classes. This means that we have to dispatch an action.
这些effects 不是这个组件本地的,因此必须由effects类处理。这意味着我们必须派发 一个 action。
Now imagine some effects class handling the confirmation. It will show the dialog to the user, get the result and store it as part of the application state. What the todo component needs to do is to query the state to get the result.
现在想象下, 一些 effects class 处理这个确认操作。 它会显示对话框给用户, 获取到结果后, 将结果存储位应用状态的一部分。 todo 组件需要做的就是查询 state 获取结果。
@Component({
selector: 'todo',
templateUrl: './todo.component.html'
})
class TodoComponent {
@Input() todo: Todo;
constructor(private store: Store<any>) {}
delete() {
this.store.dispatch({
type: 'CONFIRM_TODO_DELETION',
payload: {todoId: this.todo.id}
});
this.store.select('confirmTodoDeletionResponse').
filter(t => t.id === this.todo.id).first().
subscribe(r => {
// r is either true or false
});
}
}
todo.component.ts hosted with ❤ by GitHub
Now how we are using the todo id to get the right reply. Ids used in this fashion are called correlation ids because we use them to correlate requests and replies. Entity ids tend to work well for this. But when they don’t, we can always generate a synthetic correlation id.
现在我们如何使用todo id来获得正确的应答。以这种方式使用的id称为关联 id,因为我们使用它们来关联请求和响应。实体 id 可以很好地实现这一点。但当它们不存在时,我们总是可以生成一个人造的关联 id。
@Component({
selector: 'todo',
templateUrl: './todo.component.html'
})
class TodoComponent {
@Input() todo: Todo;
constructor(private store: Store<any>) {}
delete() {
const correlationId = generateId();
this.store.dispatch({
type: 'CONFIRM_DELETION',
payload: {todoId: this.todo.id, correlationId }
});
this.store.select('confirmDeletionResponse').
filter(r => r.id === correlationId ).first().
subscribe(r => {
// r is either true or false
});
}
}
todo.component.ts hosted with ❤ by GitHub
Processing Actions 处理 Actions
A dispatched action can be processed by effects classes and reducers.
派发的action 可以被 归约器和 effects class 处理。
Reducers are simple: they are synchronous pure functions creating new application states.
归约器很简单: 它们是创建新的应用状态的同步的纯函数。
They are so simple because reducers don’t deal with asynchrony, process coordination, talking to the server, which are the hard part.
它们之所以如此简单,是因为归约器不处理异步、流程协调、与服务器通信,而这是最困难的部分。
Effects classes deal with all of these. And that’s where, it turns out, many patterns used for building message-based systems on the backend work really well.
Effects class 处理所有这些。 事实证明,在这一点上,许多用于在后端构建基于消息的系统的模式工作得非常好。
Effects Classes
(译者注: 原文此图有误, 此图已修正)
Effects classes have three roles:
Effects classes 有 3 个角色:
-
They decide on how to process actions.
它们决定如何处理 actions。 -
They transform actions into other actions.
它们将 actions 转化为其他的 actions。 -
They perform side effects.
它们执行 副作用。
It’s a good idea to keep these roles in mind when implementing effects classes. And, of course, it’s even better to express them in the code.
在实现effects class时,最好记住这些角色。当然,最好用代码来表达它们。
Let’s examine each of the roles in detail.
现在详细地测试下每个角色。
Action Deciders (Action 决策器)
An action decider determines if an effects class should process a particular action. A decider can also map an action to a different action.
action 决策器 确定effect class 是否应该处理某个特定的 action. 决策器同样也可以将 action 映射为另一个不同的 action.
Filtering Decider 过滤器 决策器
The most basic decider we all familiar with is the filtering decider. It is so common that NgRx comes with an operator implementing it: ofType.
我们都熟悉的最基本的决策器是过滤器决策器。 它非常常见,NgRx 有一个操作符实现了它: ofType。
class TodosEffects {
constructor(private actions: Actions, private http: Http) {}
@Effect() addTodo = this.actions.ofType('ADD_TODO').
concatMap(todo => this.http.post(…).
map(() => ({type: 'TODO_ADDED', payload: todo})));
}
effects.ts hosted with ❤ by GitHub
Content-Based Decider 基于内容的 决策器
A content-based decider uses the payload of an action to map it to a different action.
基于内容的决策器使用 action的payload 来将它映射为另一个不同的 action。
In the following example, we are mapping ADD_TODO to ether APPEND_TODO or INSERT_TODO, depending on the content of the payload.
在下面的例子中, 依赖于 payload的 内容, 我们将 ADD_TODO 映射为 APPEND_TODO 或者 INSERT_TODO,
class TodosEffects {
constructor(private actions: Actions) {}
@Effect() addTodo = this.actions.ofType('ADD_TODO').map(add => {
if (add.payload.addLast) {
return ({type: 'APPEND_TODO', payload: add.payload});
} else {
return ({type: 'INSERT_TODO', payload: add.payload});
}
});
}
effects.ts hosted with ❤ by GitHub
By using content-based deciders we introduce another level of indirection, which can be useful for several reasons.
通过基于内容的决策器, 我们引入了另一种层次的间接,这种间接很有用,有几个原因。
For instance, it allows us to change how certain actions are handled and what data they need, without affecting the component dispatching them.
举例来说, 它允许我们改变一些 action 的处理方式, actions 所需要的数据, 而不用影响派发actions 的组件。
Context-Based Decider 基于 上下文的 决策器
A context-based decider uses some information from the environment to map an action to a different action. Using it allows us to have distinct workflows the component dispatching the action is not aware of.
基于上下文的决策器使用环境中的一些信息将 action 映射到不同的 action。使用它可以让我们拥有不同的工作流,而这些工作流是派发 action 的组件所不知道的。
class TodosEffects {
constructor(private actions: Actions, private env: Env) {}
@Effect() addTodo = this.actions.ofType('ADD_TODO').map(addTodo => {
if (this.env.confirmationIsOn) {
return ({type: 'ADD_TODO_WITH_CONFIRMATION', payload: addTodo.payload});
} else {
return ({type: 'ADD_TODO_WITHOUT_CONFIRMATION', payload: addTodo.payload});
}
});
}
effects.ts hosted with ❤ by GitHub
Splitter 分流器
A splitter maps one action to an array of actions, i.e., it splits an action.
分流器将一个 action 映射为 一组 actions, 也就是说 它 分流一个 action。
class TodosEffects {
constructor(private actions: Actions) {}
@Effect() addTodo = this.actions.ofType('REQUEST_ADD_TODO').flatMap(add => [
{type: 'ADD_TODO', payload: add.payload},
{type: 'LOG_OPERATION', payload: {loggedAction: 'ADD_TODO', payload: add.payload}}
]);
}
effects.ts hosted with ❤ by GitHub
This is useful for exactly the same reasons as splitting a method into multiple methods: we can test, decorate, monitor every action independently.
这非常有用,和将一个方法分解为多个方法一样:我们可以独立地测试、修饰和监视每个 action。
Aggregator 聚合器
An aggregator maps an array of actions to a single action.
聚合器将一组 actions 映射为一个 action。
class TodosEffects {
constructor(private actions: Actions) {}
@Effect() aggregator = this.actions.ofType(‘ADD_TODO’).flatMap(a =>
zip(
// note how we use a correlation id to select the right action
this.actions.filter(t => t.type == 'TODO_ADDED' && t.payload.id === a.payload.id).first(),
this.actions.filter(t => t.type == 'LOGGED' && t.payload.id === a.payload.id).first(),
)
).map(pair => ({
type: 'ADD_TODO_COMPLETED',
payload: {id: pair[0].payload.id, log: pair[1].payload}
}));
}
effects.ts hosted with ❤ by GitHub
Aggregator are not as common as say splitters, so RxJs does not come with an operator implementing it. That’s why we had to add some boilerplate to do it ourselves. But could always introduce a custom RxJS operator to help with that.
聚合器并不如分流器常用, 所以 RxJS 没有实现聚合器的操作符。 这就是为什么我们必须自己添加一些模板来实现。 但是, 我们总是可以引入自定义RxJS 操作符来帮助我们。
class TodosEffects {
constructor(private actions: Actions) {}
@Effects() a = this.actions.ofType('ADD_TODO').
aggregate(['TODO_ADDED', 'OPERATION_ADDED'], (a, t) => t.payload.id === a.payload.id).
map(pair => ({type: 'ADD_TODO_COMPLETED', payload: {id: pair[0].id}}));
}
effects.ts hosted with ❤ by GitHub
Overview Deciders 决策器概览
These are the most common deciders we just looked at:
我们刚刚看了最常用的决策器:
-
A filtering decider uses the action type to filter actions.
过滤决策器使用 action 类型过滤 actions -
A content-based decider uses the action payload to map an action to a different action.
基于内容的决策器使用 action 的 payload 将一个 action 映射为另一个不同的 action -
A context-based decider uses some injected object to map an action to another one.
基于上下文的决策器使用一些注入的对象将一个action 映射为另一个 action。 -
A splitter maps an action to an array of actions.
分路器将一个 action 映射为 一组 actions -
A aggregator maps an array of actions to a single action.
聚合器将一组actions 映射为一个 action。
Action Transformers (Action 转换器)
Content Enricher 内容扩充器
This example is very basic: we merely add the already available current user to the payload. In a more interesting example we would fetch data from the backend and add it to the payload.
这个例子非常基础: 我们仅仅是将获取的当前用户添加到 payload 中。 在更有趣的例子中, 我们会从后端获取数据, 然后将它添加到 payload 中。
class TodosEffects {
constructor(private actions: Actions, private currentUser: User) {}
@Effect() addTodo = this.actions.ofType('ADD_TODO').
map(add => ({
action: 'ADD_TODO_BY_USER',
payload: {...add.payload, user: this.currentUser}
}));
}
effects.ts hosted with ❤ by GitHub
Normalizer & Canonical Actions 标准化和规范 actions
A normalizer maps a few similar actions to the same (canonical) action.
归一化器将几个类似的 actions 映射为同样(规范的)的 action
class TodosEffects {
constructor(private actions: Actions) {}
@Effect() insertTodo = this.actions.ofType('INSERT_TODO').
map(insert => ({
action: 'ADD_TODO',
payload: {...insert.payload, append: false}
}));
@Effect() appendTodo = this.actions.ofType('APPEND_TODO').
map(insert => ({
action: 'ADD_TODO',
payload: {...insert.payload, append: true}
}));
}
effects.ts hosted with ❤ by GitHub
Building Blocks 构建块
These are the common building blocks used to implement application logic using NgRx:
使用 NgRx 时, 可以用一些通用的 构造块来实现应用逻辑:
The best thing about them is how well they compose.
最棒的是它们可以很好地组合在一起。
Let’s look at a simple scenario first, where we select one type of action, execute needed side effects, and then update the state.
首先, 看个简单的场景, 我们选择某个类型的 action, 执行需要的 副作用, 然后更新状态。
Often that’s not enough, and we need to implement at a more complex scenario.
通常, 这还不够, 我们需要实现更加复杂的场景。
In this scenario we start with a filtering decider, next we use a splitter. We then use a content-based decider for the top action. The bottom one is simpler. After executing the side effects, we aggregate the results and pass them to the reducer.
在这个场景中, 我们开始用了一个过滤决策器, 然后用了分路器。 然后我们对上面那个action 使用了基于内容的决策器。 下面那个很简单。 执行完副作用后, 我们将结果聚合后传入归约器。
To make one thing clear, I’m not advocating splitting every interaction into ten separate classes. This is the last thing we should do. What I’m advocating is having a clear mental model and a language we can use to talk about these things with our fellow developers.
为了看起来更清晰, 我没有将每个交互分成十个单独的类。 这是最后需要做的。 我所提倡的是有个清晰地思维模型和语言, 这样我们可以和开发者用来讨论这些事情。
The next example, for instance, implements a complex interaction, but everything is done in one class.
下一个例子, 举例来说, 实现一个复杂的交互, 但是所有这些事情都是在一个类中实现的。
class TodosEffects {
constructor(private actions: Actions, private currentUser: User, private http: Http) {}
@Effect() addTodo =
this.actions.ofType('ADD_TODO'). // filtering decider
map(t => ({type: t.type, payload: {...payload, user: this.currentUser}})). // content enricher
map(t => t.append ? // content-based decider
({type: 'APPEND_TODO', payload: t.payload}) :
({type: 'INSERT_TODO', payload: t.payload})).
flatMap(t => [t, {type: 'LOG_OPERATION', payload: t}]); // splitter
@Effect() appendTodo =
this.actions.ofType('APPEND_TODO').
mergeMap(t => this.http.post(...));
@Effect() insertTodo =
this.actions.ofType('INSERT_TODO').
mergeMap(t => this.http.post(...));
}
effects.ts hosted with ❤ by GitHub
But even though everything is implemented in a single class, we can still talk about every aspect of it using the language and the patterns we learned in this article.
即使所有的东西都是在一个类中实现, 我们仍然可以用我们在本文中学到的语言和模式讨论它的每个方面。
Using the labels forces us to be more intentional about the design of our effects classes. Here, for instance, we can see that the addTodo effect does not execute any side effects. And the appendTodo and insertTodo effects only execute side effects. This alone has huge implications on how these things should be tested, monitored, etc..
使用标签迫使我们对 effects类的设计更加用心。例如,我们可以知道 addTodo effect没有执行任何副作用。而 appendTodo 和 insertTodo 只执行副作用。这本身就对如何测试、监控这些东西产生了巨大的影响。
Summary 总结
-
Programming with NgRx is programming with messages. In NgRx they are called ‘actions’.
用 NgRx 编程就是用消息编程。 在NgRx中, 称呼消息为 ‘actions’ -
Components create and dispatch actions.
组件创建以及派发 actions。 -
Actions can be categorized as commands, events, and documents.
Actions 可以分类为 命令, 事件 和 文档。 -
A command has a single handler, and we often expect a reply after dispatching it.
命令有一个 handler, 我们经常期望派发命令后收到回应。 -
Events and documents can have multiple handlers, and we do not expect replies.
事件和文档可以有多个 handlers, 我们并不期望有回应。 -
Deciders, transformers, reducers, and side effects are the building blocks we use to express our application logic. They compose well.
决策器, 转换器, 归约器 和副作用使我们用于表达应用逻辑的构件块。 它们可以很好地组合在一起。
Enterprise Integration Patterns
This article is based on this book. The title “Enterprise Integration Patterns” may sound a bit scary, but this is the best book on messaging I know of. So I highly recommend you to check it out.
本文基于此书。 标题 “企业集成模式” 可能听起来有点吓人, 但是这是我所了解的本关于消息的最好的书。我强烈推荐你读下。