前端响应式架构指南-全-
前端响应式架构指南(全)
一、什么是反应式编程?
千里之行,始于足下。—老子,陶德昌
Have you ever heard about Reactive Programming or Reactive Systems? Do you think React.js is a reactive library? Have you ever thought about why you should use Rx.JS inside an Angular project? Is Rx.JS the new Loadash? If at least one of these questions is often in your mind, this is exactly the right book to find an answer! In these pages you will have a chance to learn more about reactive programming and reactive architecture for front-end development: a programming paradigm that is becoming more popular, every day, in the front-end community; but these days it is probably one of the most misunderstood and abused paradigm. The main goal of this book is to provide a good understanding of what reactive programming is, how to use it in our projects, and particularly how to create fully reactive architectures for creating resilient and maintainable projects. During this journey you will learn the following:
- 什么是反应式编程,为什么它很重要
- 这种编程范式的最佳用例是什么
- 如何用不同的框架构建一个完全反应式的架构
- 在前端生态系统中,反应式编程的未来会是怎样的
If you are wondering if the concepts learned inside this book are applicable also on a back-end architecture, my answer would be YES, or at least, the majority of them could be applied to your back-end architecture too. Bear in mind that this book will focus the attention on front-end architectures with JavaScript, but some of the concepts illustrated should be easily portable to other back-end programming languages such as Node.js, for instance. This book assumes that you already have good understanding of JavaScript, in particular ECMAScript 6 and 7 syntax; object-oriented programming; and possibly some knowledge of functional programming, but it’s not mandatory. Let the journey begin!
什么是反应式编程?
Every day when we open an editor or IDE to develop a program, we use our favorite programming language; sometimes we study a new one, but, consciously or unconsciously, we are making the decision of what kind of programming paradigm we are going to work with. Reactive programming is not a new paradigm: it’s one of the buzzwords we are used to hearing about in the JavaScript community in the past year or so, and it will become more than just a buzzword in the future. I don’t want to begin immediately by using too many technical terms because we will have enough time to learn about them while reading this book, but it’s important that you understand what is the benefit of working in a “reactive way.” If you read blog posts or articles on the Web, few of them are going to explain reactive programming with the spreadsheet cells example, where spreadsheet cells are reacting to changes happening in other cells after user input. This is definitely a good example but we can do better than this. I’m sure you are familiar with the dependency injection pattern where an object is injected via the constructor or in a public method exposed by a class or module. This pattern leverages several benefits like decoupling between two objects and the possibility of testing the hosting object in isolation without creating dependencies and so on. In some programming languages when we use dependency injection we are going to define an interface as function’s argument in the hosting object and then we can interact with the methods available in the injected object. The injected object in this case is used as an interactive object, because the host knows exactly what the contract is and how to use it. In reactive programming instead, the hosting object will just subscribe to the injected one, and it will react to the propagation of changes during the application lifetime. See Figure 1-1 . Figure 1-1 Interactive vs. Reactive programming : in Reactive Programming the producer is A and the consumer is B Looking at the image above, we can immediately grasp the main difference between the two approaches :
- 在交互的例子中,对象 A 知道调用哪个方法,因为它确切地知道 B 的对象契约,而且如果我们必须了解谁影响了被摄取对象的状态,我们将搜索所有与我们交互的项目。
- 在反应型中,契约是标准的,对象 A 对对象 B 中发生的变化作出反应,最重要的是,我们确信任何操作都会发生在被注入的对象内部;因此,我们将对对象之间的关注点进行更强的分离。
- 因为宿主对象对注入的对象内部传播的任何值做出反应,所以我们的程序将是最新的,而不需要实现任何额外的逻辑。
It’s time for a simple example before we move ahead with other concepts. Let’s assume we have a class called Calculator with a method sum and a class Receipt with a method print as shown in Listing 1-1. class Calculator { sum(a, b){ return a + b; } } class Receipt { constructor(calculator){ this.calc = calculator; } print(itemA, itemB){ const total = this.calc.sum(itemA, itemB); console.log(total receipt £${total}
); } } const pizza = 6.00; const beer = 5.00; const calc = new Calculator(); const receipt = new Receipt(calc); receipt.print(pizza, beer); Listing 1-1Check Snippet1.js As you can imagine, the program outputs “total receipt £11.” What we are doing in this example is creating the Calculator object and a Receipt object, and then we inject the Calculator instance called calc and we call the method print from the receipt instance with few arguments. Inside the print method we are writing in the console the total price of the elements passed. Checking the Receipt class implementation , you can spot in the print method that we are interacting with the method sum of the Calculator class and then getting the final result. Now let’s try to implement the same example in a reactive way in Listing 1-2. class Calculator { constructor(itemA, itemB){ const obs = Rx.Observable.of(itemA, itemB); const sum$ = obs.reduce((acc, item) => (acc + item)); return { observable: sum$ } } } class Receipt { constructor(observable\(){ observable\).subscribe(value => console.log(total receipt: £${value}
)) } } const pizza = 6.00; const beer = 5.00; const calc = new Calculator(pizza, beer); const receipt = new Receipt(calc.observable); Listing 1-2Check Snippet2.js As you can see in this example, the Receipt class is subscribing to an object called observable, injected via the constructor, and all the logic of how to sum the prices and propagate them is delegated to the Calculator class. Therefore, the Receipt class is just reacting to a change, happening in a certain moment of the program’s lifetime, displaying in the console the value emitted by the Calculator instance. Another thing to highlight here is the contract between the objects: instead of knowing exactly what method we should call, we pass an, with a default contract, and we react when something changes inside it. Overall, it’s a very simple example but I hope it helps you to understand the shift of mindset we are going to have when we work with reactive programming; in the next few chapters we will see more reactive examples like this one.
编程范例
It’s time to spend some words on programming paradigms to shed some light on reactive concepts and comparing them with functional and imperative programming.
命令式编程
Imperative programming is probably one of the most well-known programming paradigms. Often, it’s the first paradigm that we learn for understanding how a computer elaborates a program: it gives us all the tools for defining implementation details in depth and specifying exactly how a program should behave step by step. Let’s see an example of imperative programming: class Calculator { constructor(){ this.VAT = 22; } sum(...items){ let total = 0; let i = 0; for(i; i < items.length; i++){ total = total + items[i]; total = total + items[i] * this.VAT/100; } return total; } } class Receipt { constructor(calculator){ this.calc = calculator; } print(...items){ let total = this.calc.sum(...items); console.log(total receipt £${total.toFixed(2)}
); } } const JEANS = 80.00; const SHIRT = 35.00; const SHOES = 90.00; const COAT = 140.00; const HAT = 29.00; const calc = new Calculator(); const receipt = new Receipt(calc); receipt.print(JEANS, SHIRT, SHOES, COAT, HAT); //"total receipt £456.28" Similar to the example discussed before, the sum method of the calculator object is accepting multiple arguments instead of just a couple, and we are summing all of them and applying the VAT value calculated per item. As you can see, we are describing the exact implementation we want to perform: from defining a for statement in order to iterate trough the values in the array until expressing the VAT calculation for each single item. Basically what we are doing is focusing on any implementation detail; potentially we could change the way we were iterating through the array’s elements and start from the last element in the array instead of the first one or use a different variable name from “i”; these are the levels of detail we usually handle with imperative programming. Now it’s time to see how this example would be handled in functional and reactive programming.
函数式编程
Functional programming is getting more famous on a daily base. Many languages arise embracing this paradigm and many existing languages are embracing it too for the readability, maintainability, and testability improvements. If you are asking yourself why Functional Programming is becoming so popular, the answer can be found behind the concepts of this paradigm. When we talk about Functional Programming we are talking about functions: in particular we are talking about pure functions. A pure function is a function that, given an argument, is always returning the same result; it’s predictable, easy to test, and doesn’t generate any side effect inside the program. Another important topic related to functional programming is the concept of immutability. Immutability means that a specific value won’t ever change during its life cycle, but if we need to manipulate it, we will create a new instance that contains the manipulated version of the original object. Even if you are not familiar with this concept or have never dealt with it, I want you to see at least one concrete example. Usually when you are dealing with an array and you want to iterate trough the values and interact with them in an imperative programming way, you would write something like this: const originalArray = [1, 4, 8, 12]; for(let i = 0; i < originalArray.length; i++){ originalArray[i] = originalArray[i] + 1; } console.log(originalArray) //[2, 5, 9, 13] At this stage we have completely lost the initial values of the array called originalArray; if we want to have a copy of the initial values, we would create a new array that contains the modified values and then we would check that one. In functional programming, instead, we are working by default with immutable objects; therefore, every modification we need to apply won’t affect the original value but will generate a new one. Taking into consideration the previous example, we could write in a functional programming way like this: const originalArray = [1, 4, 8, 12]; const finalArray = originalArray.map(value => value+1); console.log(finalArray); //[2, 5, 9, 13] As you can see from these simple examples, functional programming is focused on what you are trying to achieve more than its implementation details. That’s a fundamental distinction compared to imperative programming. In fact, with functional programming we are describing our program, focusing on each action; and we need to do describe the data flow of our program more than focusing on each single detail such as which variable we have to define for iterating an array or how to increment the variable inside the for statement. Another important aspect of software programming is how to deal with state management . In Object-Oriented Programming we are used to encapsulating a state inside an object and changing it via different methods described in the same object. But in functional programming we are trying to compose it via pure functions that accept as the argument a state and return a new state. Redux ( redux.js.org/
), a very well-known library in the React community, aims to resolve the problem of state management by implementing a state machine pattern. In Redux, when we want to change the application state, we will need to call a method that accepts two arguments: the previous application state and an action. And it is returning the new state without mutating the previous one. An Action is a simple object used for identifying in which state the application should transform. Relying to pure functions will make our code more modular and more reusable, will create less bugs, and will suddenly become more testable! Let’s try now to convert the previous imperative example, porting it to functional programming. See Listing 1-3. class Calculator { getTotal(...items){ const total = items.map(::this.addVAT) .reduce(this.sumElements); return total; } addVAT(itemValue){ return itemValue + this.calculateVAT(itemValue); } calculateVAT(value){ const VAT = 22; return value * VAT/100; } sumElements(accumulator, value){ return accumulator + value } } class Receipt { print(total){ console.log(total receipt £${total.toFixed(2)}
); } } const JEANS = 80.00; const SHIRT = 35.00; const SHOES = 90.00; const COAT = 140.00; const HAT = 29.00; const calc = new Calculator(); const receipt = new Receipt(); receipt.print(calc.getTotal(JEANS, SHIRT, SHOES, COAT, HAT)); // "total receipt £456.28" Listing 1-3Check Snippet3.js As we can see in the sum method implementation, we are focusing more on the actions we want to implement more than how to apply them; therefore, first we know that we need to calculate the VAT value for each single element (map method) and then to sum the items for retrieving the total (reduce method). In this implementation we don’t need to specify how we are iterating through the array elements or specifying variables to keep the state of the iteration; we just focus on our goal. Another thing to notice in this implementation is how we are using functions as an argument of other functions; this mechanism is called high-order functions and it is another cornerstone of Functional Programming. Obviously, Functional Programming is not just that: it is a broad topic with many other patterns to take in consideration like currying, practical application, memoization, and so on, but this is not the main topic of the book. Bear in mind these concepts and possibly read a few posts online regarding them for mastering the different implementations because being familiar with Functional Programing will allow you to embrace Reactive Programming more easily. If you are not used to thinking functionally, I strongly suggest peaking at a book that describes this paradigm in your favorite language; it’s a great time investment, and you won’t regret it.
反应式编程
We have just briefly seen Imperative and Functional Programming, but then what about Reactive Programming? Reactive Programming is not a new concept – it’s been a while since it has been used on server-side programming – but it’s becoming very popular on the front-end ecosystem also. We have seen how to easily turn a quick example to Reactive Programming, but it’s not just that. With Reactive Programming we could easily transform our code to Imperative Reactive Programming or Functional Reactive Programming. The main concept behind this paradigm is the data flow and how an object observes and reacts to changes that happened during the life cycle of an application. Let’s start defining what Reactive Programming means : Reactive Programming is a paradigm based on asynchronous data streams that propagate changes during the application life cycle. What does it mean in practice? Let’s assume we have to develop a financial dashboard, and all the data are coming from a server that is aggregating them for client visualization. For the client we need to establish a polling mechanism or open a WebSocket communication for retrieving these pieces of information, and then we need to translate them into a nice user interface that will be consumed by our users. Trying to decompose the challenge we want to implement and how we would be reasoning without Reactive Programming, consider the following:
- 1.我们需要创建一个代理、一个动作或一个命令来每隔几秒钟从服务器检索数据,从而触发轮询机制。
- 2.在检索数据之后,我们需要分析这些数据,可能修改或分析它们以提供更友好的可视化。
- 3.然后,我们通过中介、存储、控制器或任何其他与视图耦合的层将这些数据传递给不同的组件,以便更新数据。
- 4.在最后一部分,我们将从视图中更新 DOM,可能只突出显示自上次更改以来已经更改的数据(在这种情况下,虚拟 DOM 机制可以帮助减少工作量)。
There a quite a few parts to take care and lots of events, commands, or signals to use in order to make these data flowing from different parts of our application to display them in our views. What would you say if you knew there is a better way to do it? It’s a paradigm that will allow us to write less code and become more expressive and pluggable inside our current applications. Obviously, I’m not saying we won’t implement all these tasks – Reactive Programming is not a silver bullet or a magic wand – but we will learn soon that this paradigm could have quite a few surprises in store regarding its simplicity and expressiveness. Another important concept on Reactive Programming is the way we are communicating within objects . If you remember, a few paragraphs before, I showed an example of how we could solve a simple problem of communication between objects by injecting an observable instance instead of an instance of a specific class or module. This could lead to many interesting scenarios that are currently not fully explored in the front-end panorama. What would you say if the contract between objects become a standard one, and the instance we inject will have a predefined contract that will allow the hosting object to subscribe to changes from the injected instance? This is not a new concept either: a similar idea was created in 1978 with Communicating Sequential Processes (CSP) . Despite the name, CSP allows you to work with sequential and parallel processes ; a process is not more than a “channel” used for the asynchronous communication between objects. In this way you are decoupling the objects using a channel for the communication; this channel though will allow you to not only pass data through it but to also do the following:
- 汇总数据
- 转换数据
- 减少数据
- 装饰数据
On top of that, we will be able to manipulate channels (splitting, piping, merging, and so on) for creating sophisticated interactions between objects (Figure 1-2). Figure 1-2In this diagram we are injecting a channel to the view and the controller, presentation model, mediator, or model for allowing the update of the view when the main state changes CSP is a fine-grained implementation of what we are going to explore in the Reactive world. If you are interested in spending some time with CSP, I’d suggest checking the library called CSP-JS (
github.com/ubolonton/js-csp
) ). Understanding how CSP works will speed up your learning process on Reactive Programming. After this brief digression we can move back to the main topic of this book; it’s time now to see Reactive Programming in action in association with Imperative and Functional Programming. Let’s start with the Imperative example shown in Listing 1-4. class Calculator { constructor(){ this.VAT = 22; } sum(items){ const items = items; } } class Receipt { constructor(calculator){ this.calc = calculator; } print(...items){ const total$ = this.calc.sum(items); total{total.toFixed(2)}`)); } } const JEANS = 80.00; const SHIRT = 35.00; const SHOES = 90.00; const COAT = 140.00; const HAT = 29.00; const calc = new Calculator(); const receipt = new Receipt(calc); receipt.print(JEANS, SHIRT, SHOES, COAT, HAT); Listing 1-4Check Snippet4.js In this example, we didn’t change much compared to the imperative one, but let’s analyze what we have done:
- 在 Receipt 对象的 print 方法中,我们将参数转换为可观察值。
- 我们将可观察对象注入到计算器类的 sum 方法中。
- 在这里,我们首先通过 map 方法将 VAT 应用于每个单个元素。
- 然后我们将这些值相加,返回另一个可观察的对象。
- 最后但同样重要的是,我们订阅由 sum 方法返回的可观察对象,并在控制台中显示总价。
At this stage, I don’t want to go in too much depth with reactive terminology; but for now think about an observable as an object that is wrapping the data and exposes some methods for manipulating the values – a sort of channel where data are flowing inside and we can apply transformation to these data. After understanding how to implement some reactivity to Imperative Programming, let’s see how the Functional example would look like, as shown in Listing 1-5. class Calculator { getTotal(...items){ const items = items; } addVAT(itemValue){ return itemValue + this.calculateVAT(itemValue); } calculateVAT(value){ const VAT = 22; return value * VAT/100; } sumElements(accumulator, value){ return accumulator + value } } class Receipt { print(total\(){ total\).subscribe(total => console.log(total receipt £${total.toFixed(2)}
)); } } const JEANS = 80.00; const SHIRT = 35.00; const SHOES = 90.00; const COAT = 140.00; const HAT = 29.00; const calc = new Calculator(); const receipt = new Receipt(); receipt.print(calc.getTotal(JEANS, SHIRT, SHOES, COAT, HAT)); Listing 1-5Check Snippet5.js Also in this case, the example is pretty much the same but now the Receipt is using the observable called total$ that got a different signature from the previous example where we got just a simple number. Once again, an observable allows subscribing to it and retrieving the values that are flowing inside it. At first glance these concepts could seem unimportant, but they will help us a lot when we try to create a full reactive architecture.
什么时候应该使用反应式编程?
Often, when a new trend is rising, a lot of developers are used to abusing the new technology or framework (hype-driven development). As we understood during this chapter, Reactive Programming is used for handling the propagation of data during the life cycle of an application. Therefore, a perfect fit for this paradigm would be a real-time data application like a financial dashboard or any monitoring system for instance. In a nutshell, we can say that any application that is heavily data driven could be a great fit for Reactive Programming. Obviously, it doesn’t mean you shouldn’t use this paradigm in other applications, but real-time web apps and applications with a large amount of asynchronous transactions and mutations are where Reactive Programming really shines. If we decide to use Reactive Programming inside existing architectures, such as Angular or Redux, for instance, it could be a good design decision because it could facilitate the update of our views or the state propagation inside components. Nowadays the Web is full of plenty of reactive examples, libraries and frameworks are raising them with great success, and embracing them will impose a shift of mindset in order to embrace the real power of this paradigm. Another great benefit of Reactive Programming is the simplicity of testing your code and describing data flows in a concise but clear manner. Reactive Programming is already implemented in production environments of several large organizations such as Netflix, Google, and Microsoft. Microsoft and Google, for instance, are a great contributor of the Reactive Programming movement ( reactivex.io/
). Netflix, as well, is another company that is contributing heavily to the evolution of this paradigm with Rx.JS 5, and Reactive Programming is applied in several implementations currently in production.
你应该如何编写一个完全反应式的应用程序?
One night I went to a meetup in London where we were discussing the JavaScript trends for 2017. Funny enough, Reactive Programming was one of these trends and the facilitator asked a question to all of the attendees: “How many of you are working with Reactive Programming now?” And half of the room raised their hands asserting a positive answer. After this question, the facilitator asked the following one: “How many of you are using React.js only as Reactive library?” Then over 85% of the people that raised their hand at the previous question raised their hand again. That wasn’t surprising for me because it provides an understanding how few people are interpreting Reactive Programming in a correct way. We could decide to structure our projects with a fully Reactive architecture where the communication/interaction between objects, the state management, and the interaction with endpoints are all handled in a Reactive paradigm. Reactive Programming on the front end and back end brought up different architectures and implementations that are interesting to use but at the same time different from what we are used to dealing with. In this book we are going to explore different approaches currently available inside the front-end Reactive community.
二、架构比较
如果你觉得好的建筑很贵,那就试试差的建筑。—布莱恩·富特
In this chapter we are going to explore the current status of front-end architectures . When we pick a framework for our projects, someone else made decisions for us on how to structure an architecture following best practices and design patterns for giving us the freedom to make design decisions, focusing mainly on what our applications should achieve more than how to structure them. It’s important here to highlight the difference between architecture and design because often these terms are misunderstood. When we talk about architectures, we are defining how our system is going to interact between different elements. For example, think about the communication between a model and a view. Usually when we define architecture we are defining the relationship between objects, how they communicate between each other, and so on. Architectural decisions are hard to change because they usually drive a project in a specific direction, which would require a huge effort for moving it in a different direction. Design decisions , instead, are local decisions like choosing a specific library, algorithm, or design pattern for solving a problem in our project. When we make a design decision on a project, it often doesn’t require a huge effort recovering from it, but in certain situations making poor design decisions will lead to a long refactoring of our system. Let’s assume we need to solve a problem where every few minutes we need to refresh the data inside a specific view without refreshing the entire page but only the elements that need to change; we could decide to use React.js for its diff algorithm or create our own algorithm where we could have more control defining a diff algorithm close to our needs. Sorting out the difference between architecture and design decisions , it’s time to see what we are going to explore in this chapter. The front-end ecosystem is in continuous evolution, in particular in the past few years where we were experiencing different but fundamental changes that are improving the creation and maintainability of our projects. We are probably living the most exciting decade of the past 10 years, overwhelmed by revolutionizing concepts that are often coming from the past but with a new twist, transforming them in useful ways and taking actual approaches for solving our daily challenges. The front-end architectures changed a lot in the past 30 years, as we moved from the classic Model View Control (MVC) to more actual architectures that nowadays are present in many contemporary frameworks. This could be a representation in a timeline of this evolution. During this chapter we are going to see in action the most famous architecture and we are going to compare them. Figure 2-1 shows a timeline where I highlighted all the architectures and implementations we are going to explore and in what year they were created. You will see that many concepts from the 1980s or ‘90s are very contemporary and used in the most famous framework implementations currently available in the JavaScript ecosystem. Figure 2-1Architectures timeline The most important thing to remember is that these architectures are not obsoletes but that they are still valid concepts to use, and they can add a lot of value to our projects by drastically facilitating the development and maintenance of them. Creating well-structured and flexible architectures also provide us the agility needed to embrace not only design changes but architectural ones as well. Also, we need to bear in mind that these architectures can be converted to Reactive architectures if we apply the Reactive concepts in a proper way; therefore if our project is currently using one of them, we can still embrace the Reactive model, applying a few changes to enhance them and moving to a more reactive paradigm.
MV*架构
In this section, we are going to dig into the base concepts of MV* architectures ; in order to do that we will work on a simple project (a basic calculator) for understanding how these architectures are composed, and how objects are communicating by embracing the SOLID principles that any project should take in consideration in order to be maintainable and extensible. The three architectures we are going to explore in this section are MVC, MVP, and MVVM; let’s start with the oldest one, MVC! S.O.L.I.D. SOLID is a set of principles created by Uncle Bob. SOLID is an acronym that stands for: S – Single-responsibility principle O – Open-closed principle L – Loskop substitution principle I – Interface segregation principle D – Dependency inversion principle If you are interested in knowing more about them, I suggest watching this Uncle Bob lecture: www.youtube.com/watch?v=t86v3N4OshQ
Bear in mind though, that all the code presented in this chapter are just highlights of the final examples; therefore if you have a hard time following the snippets, feel free to download the chapter examples first and switch from this book to your favorite editor or IDE for consulting the code. Remember first to install all the dependencies with the command: npm install And then you can run the n.p script called build in this way: npm run build
模型视图控件
Model View Control (MVC) is an architectural pattern introduced in Smalltalk in the late 1980s by Trivet Reenskaug, and it is probably the most popular architecture of the past 30 years, used by millions of developers in any project independently of the language used (Figure 2-2). Its main characteristic is the separation of concerns and the single responsibility of its actors; the main innovation of this pattern was finally separating the data from their visual representation, a concept not fully explored until then. In fact the model objects are completely separated from their representation; therefore there isn’t any knowledge of the view inside the model. This detail becomes important when multiple views or controllers are accessing the same data because our model objects could be reused across different screens without any problem. MVC is based on three basic principles :
- 模型:应用程序状态和领域数据所在的位置
- 视图:用户交互的应用程序的用户界面
- 控制器:模型和视图之间的粘合剂,通常负责协调应用程序内部的通信流
Figure 2-2Model View Control diagram (MVC)
MVC 如何工作
Usually the controller instantiates the view and the model in the constructor or, in certain implementations, are injected by the main application class via dependency injection. The relation between a controller and the views could be one to many, so a controller could handle multiple views: the same relationship is valid for the models as well. When we want to scale up this architecture for large projects usually we try to split up these relationships in order to have almost a 1 to 1 relation between these three objects so we can reuse components that are self-contained and architected in the same way the entire application works, like a Russian doll. As described before the main aim of a model is storing the application state and everything that should be persistent across a single or multiple views, usually every time the application state changes or data are updated; the model is triggering an event in order to update the view. In the vast majority of implementations the object listening for any state or data change is the view, but in certain implementations we can also find a total separation between the model and the view where the controller in this case is listening to the change and is propagating this information to the view. The view is simply responsible for displaying the application data and listening for any user interactions. Figure 2-3 shows how MVC works with an implementation of a calculator in JavaScript. This is the final result of what we want to achieve. Figure 2-3This is the output of our Calculator application It is a simple calculator where every time we are clicking a button, we add the value on the display on the top and when the user is going to click the button “=” we will display the result. Let’s start to explore our MVC example from the controller. See Listing 2-1. initialize(){ const emitter = this.initEmitter(); this.model = this.initModel(emitter); this.initView(emitter, this.model); } Listing 2-1CalculatorController.js In our implementation, the controller is going to instantiate the view and the model, and it’s injecting an event emitter for communicating between objects via events. This will immediately improve the decoupling between objects because if in the future we want to reuse some of these objects in other projects, we won’t need to copy more than the object we are interested in, considering they are communicating via events, and as long as we respect the contract our code becomes reusable and flexible. We are going to use React.js for handling the views of our project. React usually renders the components again when there is a properties update, but in our case what we implement is using the event bus for notifying the view that a new result should be rendered on the calculator’s display, and then the view will retrieve the data from the model, updating with the new string the display. In order to do that, we need to inject the model and the emitter instance inside the view. See Listing 2-2. initView(emitter, model){ const cont = document.getElementById("app"); ReactDOM.render(
模型视图演示者
Model View Presenter (MVP) is an architecture created in the 1990s, and one of its first appearances was made in IBM software (Figure 2-4). From my point of view, MVP shines when we need to reuse views or behaviors in different projects or different areas of the same application; with this architecture we start to give more importance to the modularization of our front-end applications and provide architecture specific for a front end more than a generic one that could fit a back-end or front-end application like for MVC. MVP is very helpful, in particular, when we work on cross-platform applications and we want to reuse the application data, communication layer, and behaviors or when we want to swap the views changing them at runtime or compile/transpile time. The main differences between MVP and MVC could be summarized in the following list:
- 用一个演示者代替一个控制者,我们马上就会看到演示者带来的好处。
- 视图和演示者之间的关系不像 MVC 中那样是一对多的,而总是一对一的。
- 拥有可重用组件的最佳 MVP 实现是当我们将视图设计为被动视图时,因为只要表示者和视图之间的契约得到尊重,交换它们就变得更容易。
Figure 2-4MVP diagram where the view is unaware of the model’s existence
MVP 如何工作
The presenter object is inspired by the presentation model pattern, and my favorite implementation is when the presenter is designed as a Supervising Controller where it retrieves all the data useful for a view from the model, and at the same time it should handle any user interaction updating the model. As mentioned before, the views are passive or if you prefer dumb, they just know how the rendering logic works, possible animations, integration with CSS styles, and so on. On top, the presenter is also dealing with updating the model and retrieving the information needed for rendering a view. Usually, in complex applications, you could have a persistent model (or more than one model maybe exposed by a façade) across the entire life cycle of an application and multiple presenters that retrieve and update the application data in the models. Another important point to highlight is the fact that the model and the view should not be aware of each other; maintaining these two completely isolated from each other will help a lot in the case of large applications or when we need to swap views for targeting different devices. Imagine for a moment that our assignment is a project where we need to target different devices like browsers, mobile, and smartTVs – exactly the same application but different UI for different targets considering that each target has different input methods and UI patterns. With an MVP architecture, maintaining the behaviors inside the presenter, the business domain in the model and having just passive views for the UI will allow us to have similar behaviors across the application, reusing the same code for the models and changing the views – adapting them for the device we are targeting without much effort. Passive View A passive view is a view without any knowledge of how the system works but just relying on another object for handling the communication with the system. A Passive view doesn’t even update itself by retrieving data from the model; this view is completely passive, and its main scope is focusing on what to render on the screen when a specific render function is called from a controller or presenter. Supervising Controller A supervising controller is a specific type of controller that is handling user interaction as well as manipulating the view for updating it. When a supervising controller is present in an architecture, the view needs only to redirect the user events to the supervising controller (in MVP the presenter is our supervising controller), and it will take care of handling the logic and updating the view with new data. The supervising controller is responsible for the communication in the system and it’s taking care to update the view it is associated with. It’s time to see MVP in action with the Calculator example discussed above, but this time with the Model-View-Presenter in mind. We can start from the App.js file where in the constructor we are going to create the model and the presenter, and we import the view called Calculator.jsx. We then inject React component and model inside the presenter as shown in Listing 2-7. export default class App{ constructor(){ const mainModel = new CalculatorModel(); const mainPresenter = new CalculatorPresenter(); mainPresenter.init(mainModel, Calculator); } } Listing 2-7App.js Then we can move inside the presenter where we are going to store all the objects injected in variables and then we render the React component injected. See Listing 2-8. initialize(model, view){ this.model = model; this.view = view; this.cont = document.getElementById("app"); this.renderView(); } renderView(){ const component = React.createElement(this.view, {result: this.model.total, onBtnClicked: ::this.onBtnClicked}); ReactDOM.render(component, this.cont) } Listing 2-8 CalculatorPresenter.js In our Presentation model we are injecting the model and the view for having complete controls on them; we then call the method renderView that will be our trigger for communicating to a React component to render again because something happened inside the application and the UI should be updated. As you can see, the view doesn’t have any knowledge of the model but we pass the result to display in our calculator via the props object exposed by React. Now it’s time to take a look at the view; and as we defined at the beginning of this section, the view should be a passive view, so in this case it is taking care of what to render and how nothing else should be integrated in a passive view. The communication with the presenter is happening via a method passed via the props object. Like we have seen in the renderView method, the presenter is passing a callback that should be invoked every time the user is selecting a button of our calculator. See Listing 2-9. import React from "react"; import ReactDOM from "react-dom"; import {ulStyle, acStyle, btnStyle, displayStyle} from "./Calculator.css"; export default class Calculator extends React.Component{ constructor(props){ super(props); } componentWillMount(){ this.btnClicked = this.props.onBtnClicked; } onButtonClicked(evt){ evt.preventDefault(); this.btnClicked(evt.target.innerHTML); } createRow(id, ...labels){ const items = labels.map((value, index) => { return <li key={${id}_${index}
} style={value === "AC"? acStyle : btnStyle} onClick={::this.onButtonClicked}> {value} ; }) return(
- {items}
模型视图视图-模型
Model View View-Model (MVVM) is an architecture created by Microsoft in 2005 for handling the GUI management with Windows Presentation Foundation (WPF) . It sticks with a true separation between the view and the model like we have seen in the MVP architecture, but MVVM encapsulates few differences compared to other architecture (Figure 2-5). Figure 2-5MVVM diagram, similar to MVP but the view is not a passive one anymore
MVVM 是如何运作的
The first difference is that we have a View-Model instead of a presenter; this specific object will be the bridge between the data stored inside a model and the view. In a nutshell the view-model is responsible for exposing the data present inside the model to the view and it is also responsible for preparing the data in a way that the view expects, so you can immediately understand that the logic of the view-model is tightly coupled with what the view should render. For instance, let’s imagine that we have a stock value stored in the model with dollars currency but the view needs it to be shown in euro. In MVVM the model stores the raw data, that is, dollars value, and the view-model would be responsible for converting the value to a given currency, in this case euro. Another important aspect of the View-Model is the fact that it has a relationship with the views that is 1 to many: therefore we can have one view-model that is handling multiple views or components. The communication of the View-Model and the view usually happen via bindings; therefore every time a value is updated on the view or in the View-Model, this value is communicated to the other object in order to keep it in sync. It’s very important to understand that with MVP we enforced the concept of complete separation between the model and the view and in MVVM this strong concept doesn’t change at all. The model is very simplistic; we store data in a raw format without any particular change, and in this case we should even avoid keeping the state in the model and moving this information to the view-model or even the view if it’s a small component. Let’s see now how our calculator would work with an MVVM architecture. The App.js file is instantiating all the objects for an MVVM architecture: model, view-model, and view; in the view-model constructor we are going to inject the model and view instances so it will be able to retrieve data from the model and serve the view with the correct value to display. See Listing 2-12. export default class App{ constructor(){ const model = new CalculatorModel(); const emitter = new LiteEventEmitter(); const vm = new CalculatorViewModel(model, emitter); const cont = document.getElementById("app"); ReactDOM.render(
- 1.他们正在更新模型。
- 2.他们正在向视图分派要显示的新值。
It’s now clear that the majority of application business logic is present in the view-model; therefore if we want to reuse a specific component or part of the application. we need to maintain the event or the binding contract between the view and the view-model as it is. So we can say that these two objects become slightly more tightly coupled compared to their relation in the MVP architecture. Last but not least, it’s time to discuss the model. As you can see, the model just exposes a few methods in order to update the value to display; it doesn’t have any data manipulation or any state, and in this case the model is just a data representation of the main application so if we need to use it in combination with another view-model, the model will provide the data expected and nothing more. See Listing 2-16. export default class CalculatorModel{ constructor(){ this.totalOperation = 0; } calculate(operation){ this.totalOperation = math.eval(this.totalOperation); } add(value){ this.totalOperation = value; } reset(){ this.totalOperation = 0; } get total(){ return this.totalOperation; } } Listing 2-16 CalculatorModel.js
JavaScript 框架
Now that we have explored the basics of MV* architectures, understanding the benefits and the issues related to each of them, it’s time to review what the JavaScript ecosystem is proposing and how these architectures are implemented. I’m sure you will realize very soon that understanding the three architectures mentioned before will help you to capture the technical approaches provided by well-known frameworks like Angular, Ember, or Redux. In this section we are going to re-create our calculator application three more times: the first one with Angular, then with Ember, and we will conclude this journey with the combination of React and Redux.
有角的
Angular is an open source framework created and maintained by Google. It’s been around since 2010 and has reached recently version 5, but from now on Google explained that we should just call it Angular. This framework passed through different stages in the past years, from a huge hype when launched with version 1; then, after the announcement of version 2 not being retro compatible with the previous version, many developers decided to move away from it and embrace other frameworks – in particular, React and Flux or Redux or similar combinations. Recently, as in 2017, Google released a new version that should enhance the experience of the JavaScript developers with Angular, providing a complete ecosystem of tools and patterns in order to work easily with this framework without the need to scrap the Web for assembling multiple libraries inside the same project. Since version 2, Angular embraces TypeScript as its main language; JavaScript ES6 and Dart are supported too, but the vast majority of the resources in the Angular website are prepared with TypeScript. Angular as a framework is providing a full architecture and utilities out of the box with also an interesting CLI tool for speeding up a developer’s productivity. TypeScript TypeScript is a typed superset of JavaScript, and it enhances the language adding Enum, Generics, Abstract, and Interfaces very familiar with strictly typed languages like Java, for instance. More information on the official website: www.typescriptlang.org
角度是如何工作的
The architecture we will evaluate in this section is related to Angular 2 and onward (Figure 2-6), so we won’t take into consideration the previous one because it works in a different manner. Figure 2-6Angular architecture example Angular architecture introduces four interesting concepts :
- 依赖注入
- 使用 NgModules 和组件的模块化
- 模板和组件之间的数据绑定
- 大量使用 decorators 来定义对象,如组件或模块
We will now go more in depth of this architecture, analyzing the different parts that compose an Angular project. Any Angular application has at least an NgModule called the root module ; an NgModule is not like a normal JavaScript Module. The peculiarity of the NgModule is the metadata; in each NgModule (root module included), we will describe how the module is composed, and in particular we will define which components belong to a module, if a module has dependencies from other modules, directives, or components; and we also describe the services used by the components present in the module. As described before, any Angular application contains at least one module called the root module. For instance, this is the root module created for our Calculator project in Angular: import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } As you can see in this code snippet, Angular is largely made up of decorators (@NgModule) for wrapping our code inside the Angular framework. Decorators The decorators in JavaScript are actually in proposal (stage 2) for becoming part of the language (more information on the proposal at this link: github.com/tc39/proposal-decorators
). They are based on a simple concept: a decorator wraps a function, augmenting its functionalities without manipulating the function decorated. Usually, in a decorator, we add methods or properties that should be common in our project at runtime; that’s why Angular is using a large number of them. It’s so we can write our logic without inheriting from some base class, thereby creating a tight coupled connection between and decorating it with the built-in functionalities available in the framework. The next topic to introduce is the Angular components. Since Angular 2 we have the possibility to create components, and we can think of them like a bridge between the template system present in Angular and the data we can retrieve from REST API or specific endpoints. Their role is mainly retrieving the data and populating the template with new data via a binding system that is present out of the box in Angular 2. The Angular component is a mix of properties we can find in the presenter and the view-model. In fact, the following is true:
- 组件通过绑定更新视图,就像在视图模型对象中一样。
- 组件和模板之间的关系始终是一对一的,就像演示者一样。
- 该组件处理模板中发生的所有用户交互,就像对演示者一样。
In order to define a component in Angular we need to specify another decorator, @Component. For our calculator example, we have defined just one component considering how simple the application is; but potentially we could have split them in multiple parts as shown in Listing 2-17. const ON_SCREEN_KEYBOARD = [ ['7', '8', '9', '/'], ['4', '5', '6', ''], ['1', '2', '3', '-'], ['0', '.', '=', '+'] ]; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], providers: [UpdateTotalService, CalculatorModel] }) export class AppComponent implements OnInit { onScreenKeyboard = ON_SCREEN_KEYBOARD; total:string; constructor(private updateTotalService:UpdateTotalService){} ngOnInit(): void { this.total = this.updateTotalService.reset(); } updateTotal(value: string){ switch (value) { case 'AC': this.total = this.updateTotalService.reset(); break; case '=': this.total = this.updateTotalService.calculate(); break; case '+': case '-': case '/': case '': case '.': this.total = this.updateTotalService.addSymbol(value); break; default: this.total = this.updateTotalService.addValue(value); break; } } } Listing 2-17 App.component.ts As you can see in the decorator (@Component) we are specifying the HTML template and the CSS associated with the component. The objects to inject will be accessible inside the component but they will be instantiated by Angular and a selector that is just an ID to identify the component. The last thing to mention about Angular components is that we can use life-cycle hooks like ngOnInit method that is triggered when the component is initialized and in our case we use it to set the first value in our calculator display. Now it’s time to see how the components interact with templates. Angular has its own way to handle HTML markup; we can use some directives in order to iterate through a data structure for displaying multiple HTML elements or for adding specific attributes if a certain condition happens in our code. Directives Directives in Angular are instructions for the HTML template on how the template should handle the DOM. In our example we have associated an app.component.html template to the app.component.ts described above and this is the code used for the template:
余烬
Ember is a framework oriented to web applications. It’s well-known in the JavaScript community and used by large organizations such as Netflix or Groupon. Ember has an interesting ecosystem composed by EmberCLI, EmberData, and Ember as a JavaScript framework. The paradigm behind Ember is slightly different from other frameworks but the productivity is high if the application fits this paradigm. Ember favors convention over configuration; a key thing to remember is embracing the EmberCLI tool because it will facilitate your life and boost your productivity, the CLI takes care to generate all the files needed for a specific view (template, router, unit testing files, and so on), model, route, or even controller. The Ember framework shines when a project has an architecture “fat server – thin client” where the majority of the logic is handled on the server; the client should be as dumb as possible and it should be a multipage application over a single page application (SPA) .
Ember 是如何工作的
Ember architecture (Figure 2-7) is based upon MVVM architecture, and there are some key elements that composed this framework:
- 路线
- 模板
- 成分
- 控制器
- 模型
Figure 2-7Ember architecture example In Ember everything starts from a route module and each view is tightly coupled with the endpoint associated. By default any Ember application has a main route system where we define all the application routes; for instance, in our calculator application we have defined this route shown in Listing 2-20. Router.map(function() { this.route('calculator', {path: "/"}); }); Listing 2-20 Router.js That means the root of our web application will be routed to the calculator route. Because Ember works with conventions, we need to save the modules in the right folders in order to be picked up. But luckily the Ember CLI comes to the rescue by providing some useful commands that prepare all the files needed out of the box: Ember generate route
- {{#each row as |value|}} <li class="btnStyle" {{action "updateTotal" value on="mouseDown"}}>{{value}} {{/each}}
反应+还原
React and Redux is a combination of libraries that together can resemble a minimal framework with a large ecosystem that is not imposed at the beginning like for Ember or Angular but is more oriented to a plug-in system where we can use what we really need without the need to import everything up front. React is a library useful for manipulating the DOM, based on components as first citizen; it takes care of the view part of an architecture only, implementing smart algorithm like the reconciliation one – a diff algorithm used for rendering only the part of the DOM that should change. React introduced a very powerful concept embraced nowadays by several other libraries: the Virtual DOM . The Virtual DOM is a DOM representation where the diffing algorithm operates at first glance and where React understands what should change and when, minimizing the changes to the real DOM and improving the performances of our web applications. Reconciliation Reconciliation is a key concept for React. If you want to know more, I suggest reading the official documentation regarding this topic: react-cn.github.io/react/docs/reconciliation.html
On the other hand, Redux is a state container not tightly coupled with React because we can find examples of Redux used in combination with other frameworks like Angular. Redux is solving a well-known problem of how to manage the state inside an application. The most interesting part of it is that it leverages a concept introduced in 2015 from another library called Flux, created by Facebook, of unidirectional flow . Unidirectional flow is a powerful but simple concept: the objects communication inside an application should be unidirectional, and this, in combination with good encapsulation, will allow any application to be easier to debug, to be picked by any team because also complex applications are easy to understand and debug, thereby improving the code quality and the possibility of extending them. The Redux paradigm is straightforward and is composed by only three key elements :
- 行动
- 还原剂
- 商店
The action is just a plain JavaScript object containing the information of what happened inside the application. The reducer is retrieving from an action that the interaction happened in the application and knows how the state should change based on the action dispatched. Finally, the store is the object that brings all together; the store is passing to the reducer the current state tree and the action, and it waits until the reducer provides back the new state, then the store will append to the state tree and all the objects that are listening for a change will be notified. Redux was created on top of three core concepts :
- 真实的单一来源:应用程序状态在一个名为 store 的单一对象中定义的树中表示。
- 状态是只读的:用 Redux 改变状态的唯一方法是通过一个动作。
- 只对纯函数进行更改:缩减器是接收当前状态树和动作的纯函数,它们知道应用程序将如何更改到下一个状态。如果我们总是传递相同的参数,我们就知道一个纯函数的输出;在这种情况下,减速器总是相同的。
Let’s see React and Redux in action with our calculator example written for the last time with a different architecture.
Redux 如何工作
The starting point of any Redux project (Figure 2-8) is usually a main application where we create the store object and we wrap our main view inside a provider object from the Redux library. See Listing 2-23. export default class App{ constructor(){ const store = createStore(CalculatorReducer); const cont = document.getElementById("app"); ReactDOM.render( Figure 2-8Redux project architecture diagram As we mentioned before, after creating the store and associating it to a specific reducer (Calculator Reducer), we are wrapping our main view (Calculator Container) inside a Provider object from the redux library. Think about the Provider as an object that receives as input the store and it propagates it to all the container components inside an application in order to have complete access to it. Considering we have mentioned the container components, it’s time to move to the view part, where we need to distinguish between presentational components and container components. The creator of Redux, Dan Abramov, wrote a post on Medium.com regarding this topic where is explaining the difference between the two types of components. To summarize Dan’s thoughts, in Redux we distinguish the presentational components as component with the only scope of managing the look and feel of the view, more or less like the Passive View described in this chapter. Meanwhile we identify the container components as the ones that can handle the presentational component logic like user interactions, having access to the store, and mapping the store values to the React component via a props object, similar to the Supervising controller of the MVP architecture. This approach will lead to a better separation of concern and reusability of our components across different projects. Presentational vs. Containers components In Redux this is a very important topic. I strongly suggest having a look at this link to Dan Abramov’s Medium post on the presentational and containers components explanation: http://bit.ly/1N83Oov Based on what we have just described, it’s time to see what a presentational component looks like and how we handle the communication with the Redux framework. In the calculator example, our presentational component code looks like that shown in Listing 2-24. export default class Calculator extends React.Component{ constructor(props){ super(props); this.onBtnClicked = this.props.onButtonClicked; } createRow(id, ...labels){ let items = labels.map((value, index) => { return <li key={
${id}_${index}
} style={value === "AC"? acStyle : btnStyle} onClick={::this.onBtnClicked}> {value} ; }) return(
- {items}
- 表示组件是在调用 connect 方法后创建的。connect 方法用于将存储“连接”到组件,它返回一个更高阶的 React 组件类,该类将状态和动作传递到从所提供的参数派生的组件中。
- mapStateToProps 是我们用来将存储传递的新状态转换成将在表示组件中呈现的属性的方法。
- mapDispatchToProps 是我们用来触发容器组件内部发生的所有调度操作的方法。dispatch 方法接受一个动作,该动作将被触发并被列在存储中,然后被传递给 reducer。
Before we move to the reducer, it’s good to understand what an action is. Basically an action is just a plain JavaScript object. Inspecting the CalculatorAction module, we can see it clearly in Listing 2-26. export function calculate(){ return { type: CALCULATE } } export function reset(){ return { type: INIT, result: 0 } } export function appendValue(value){ return { type: APPEND, toAppend: value } } Listing 2-26 CalculatorActions.js The actions are similar to commands where they bring with them the information needed to the reducers in order to change the current state to a new one. Usually they have a property type where we define the type of action called from the store.dispatch method. Finally, it is the turn of the reducer. The reducers are used when we need to change the application state, because the action is notifying us that something happened inside the application but it doesn’t have the knowledge of how the application should change – that’s the reducer’s duty. See Listing 2-27. const calculation = (state = reset(), action) => { switch (action.type) { case CALCULATE: return { type: action.type, result: math.eval(state.result) } break; case APPEND: return { type: action.type, result: resultHandler(state, action.toAppend) } break; case INIT: return { type: action.type, result: resultHandler(state, action.toAppend) } break; default: return state; break; } } Listing 2-27 CalculatorReducer.js In our reducer, we set as the default state the reset action that starts the application with the initial state (INIT in our switch statement). Every time an action is dispatched, the store calls the reducer passing the current state and the action dispatched, and then the reducer is mapping the action to the next application state. Redux is a very simple but effective state management library, and it’s interesting that there are many similarities with the MVP architecture – in particular for the relation view presenter like we have in Redux with the presentational component and its container. Also in the redux ecosystem there are other libraries that we can use in combination with Rx.js, for instance, or other reactive libraries.
总结
In this chapter, we have evaluated different architectures from the oldest one like MVC to the most recent one like Redux. We saw a clear evolution of them but with many references to the past principles. Often we spend a lot of time with a framework without asking us why the authors picked one decision over another. I hope that this journey through the most famous architectures/frameworks available for the front-end development provided you some benefit in your future projects because I truly believe that it is very important to have a good knowledge of them for choosing the right one for a specific project. Too often I have seen developers and architects always using the same architecture fitting any project inside it instead of using “the right tool for the right job”. From the next chapter on, we are beginning the Reactive journey that will lead us to learn how reactive architectures work from different point of views and we will discover more about Cycle.js, MobX and SAM.
三、反应式编程
所有的生活都是一场实验。你做的实验越多越好。—拉尔夫·瓦尔多·爱默生
After an overview of the most famous front-end architectures and their implementations, it’s time to dig into the main topic of this book: Reactive Programming with JavaScript. I watched many videos and read many articles and books on the argument, and I have to admit that often I had the impression that something was missing, like we were just scratching the surface instead of going in depth inside the Reactive ecosystem. The main aim of this chapter is to provide all the tools needed for understanding Reactive Programming more than focusing on a single library, embracing the concepts behind this programming paradigm. This should allow us to embrace any reactive library without the need to invest too much time for switching our mindset. I often saw developers embracing a specific library or framework and being real ninjas with them, but once moved away, they struggled with approaching different implementations – mainly because they didn’t understand what there was behind the implementation. Understanding the core principles of the reactive paradigm will allow you to master any library or framework independently from the different implementations without treating just as a “black box .” There are many reactive libraries available in the open source ecosystem and, we are going to discuss the most famous like Rx.JS and XStream. After that we are going to understand the key principles of Reactive programming such as what is a stream, the difference between hot and cold observables, and back pressure. Unfortunately, reactive programming has a high learning curve mainly due to the concepts it is leveraging, but once you get used to them, you will start reasoning in a reactive way for everything without any issue.
反应式编程 101
First and foremost, implementing reactive programming means working mainly with events streams , but in order to understand what an event stream is, we should start with a simple diagram for better understanding how a stream works. Figure 3-1 is called a marble diagram and it is used to represent an event stream. Figure 3-1Marble diagram representation example It’s very important to understand how it works because often in the documentation you can understand how the APIs work by just looking at the diagram without the need to read the full explanation. Also, marble diagrams are useful during the testing phase because when we need to simulate an event stream, we will use them against our event stream implementation. But let’s see what they are so we can understand better the concept of event stream. In a marble diagram, we can spot a horizontal line representing the time (our stream), and the value inside the colorful circles are events that are happening at a certain point in time in our application (events). The vertical line, instead, represents the end of our stream, after that point the stream is completed. So if we want to describe this diagram we can easily do it in this way :
- 应用程序启动后,流发出值 4。
- 一定时间后,流发出值 1,紧接着 3。
- 在所有这些事件之后,溪流完成了。
The stream could also end after an error, and in that case instead of seeing a vertical line, the marble diagram (Figure 3-2) will show an X symbol notifying when the error happened inside the stream. Figure 3-2Marble diagram with error The last bit to mention is when we apply transformation to the values inside a stream via operators. An operator is a method that allows us to manipulate the data in a stream or the stream itself. In this case the marble diagrams can show how the values are transformed like in this example with the map operator where we are adding 1 to any number emitted by the stream on the top and creating a second stream with the values manipulated (Figure 3-3).
Figure 3-3Marble diagram with map operator applied to an initial stream that returns a new stream with data manipulated Until now, we understood that an event stream is a sequence of values (events) passed in a certain period (stream). We can then assume that everything inside an application could be a stream, and in a certain way it’s exactly what the reactive paradigm is leveraging: we can transform listeners with callbacks in event streams, we can wrap an http request in a stream and handle the response by manipulating the data before updating a view, and we can use streams as communication channels for passing data between two or more objects, and many other use cases. In the reactive panorama we need to introduce a particular type of stream: the observable. A great definition of observable is available on the Rx.JS 5 documentation:
可观察值是一种延迟求值的计算,从它被调用时起,可以同步或异步地返回零到(可能)无穷大的值。(
bit.ly/2sWVEAf
In order to consume data emitted by an observable, we need to create an observer (consumer); this subscribes to the observable and reacts every time a value is emitted by the observable (producer). If we want to summarize in technical words what an observable is, we could say that an observable is an object that wrap some data consumed by an observer (an object with a specific contract) and once instantiated provides a cancellation function. The observer is an object that allows us to retrieve the value emitted by an observable and has a specific contract exposing three methods: next, error, and complete functions. When you deal with observables and observers bear in mind two well-known design patterns for fully understand their mechanisms: the observer and the iterator patterns.
观察者模式
The Observer Pattern is a behavioral pattern where an object called Subject maintains a list of other objects (observers) that want to be notified when a change happens inside the program. The observers are usually subscribing to a change and then every time they receive a notification, they verify internally if they need to handle the notification or not. Usually, in typed languages, the Observer Pattern is composed by a subject and one or multiple observers. The subject is handling the subscription, unsubscription, and the notification to an observer; then each observer implements a specific interface that contains a publish method (update) for reacting to a notification originated by another object or a user interaction and shared through the Subject (Figure 3-4). Figure 3-4Observer Pattern UML
迭代器模式
The Iterator Pattern is a behavioral pattern used for traversing a data container like an array or an object, for instance. As JavaScript developers, we should be familiar with this pattern, considering it was added in ECMAScript 2015. This pattern allows an object to be traversed calling a method (next) for retrieving a subsequent value from the current one if it exists. The iterator pattern usually exposes the next method and the hasNext method that returns a Boolean used for checking if the object traversed contains more values or not. In MDN we can find more information about the official JavaScript implementation in ECMAScript 2015: mzl.la/2sfrMxJ
(Figure 3-5). Figure 3-5Iterator Pattern UML
将守则付诸实践
Let’s create an example by walking through the code for a better understanding of how an observable/observer relation works in practice: const observable = Observable.range(1, 10); const observer = observable.subscribe(onData, onError, onComplete); function onData(value){ console.log(value); } function onError(err){ console.error(err); } function onComplete(){ console.log("stream complete!"); } As you can see from this first example in the first line we have created an observable that contains a range of values from 1 to 10; that means we have these values ready to be emitted to an observer when the observer will subscribe to it. On the second line we subscribe to the observable; the subscribe method in many libraries has three parameters that correspond to callbacks called once the observable is emitting a value, an error, or a complete signal. In the rest of the code we are just reacting to the information emitted by the observable printing to the console an output that could be either an event from the stream or an error or the complete signal. When an observable receives events can also allow us to manipulate the data for providing a different output from them like we have seen previously in the marble diagram where we were mapping each single value emitted, increasing by 1 each of them. Different Reactive libraries are providing different sets of utilities, but the most complete one when the book was written is without any doubt Rx.js. This library provides a set of operators for manipulating not only the events emitted by the observable but also with other observables; it is not unusual to transform observable of observables in flat observables: think about them as an array of arrays. These operators will allow us to flat the object nesting accessing directly to the values in the observables without the iteration complexity. We are going to see the possibilities offered by operators in the next section when we explore the different Reactive libraries so we can understand in practice how to use it and what we can do with them. Obviously, we will review only the most used ones because there are literally hundreds and our focus is on understanding the mechanism behind them more than exploring the entire library.
流实现
Now that we understood what streams are, it’s time to get a better understanding of what is available in the front-end reactive ecosystem. The first library we are going to take into consideration is Rx.JS 5.
Rx。射流研究…
Rx.JS is the most famous and used reactive library at the moment; it’s used almost everywhere, from Angular 2 where the library is integrated inside the framework to many other smaller or larger frameworks that are embracing this library and leveraging its power. It’s one of the most complete reactive libraries with many operators and a great documentation, Rx.JS is part of Reactive Extensions ( reactivex.io
). Learning it will mean being able to switch from one language to another using the same paradigm and often the same APIs. Rx.JS can be used on front-end applications as well on back-end ones with Node.js ; obviously its asynchronous nature will help out on both sides of an application’s development. There are two main versions available. Version 4 is the first JavaScript porting of the Reactive Extension, and then recently another implementation started to rise up that is version 5. The two libraries have several differences in our examples and so we will use version 5. In this section, we won’t be able to play with all the operators available on Rx.JS because this library is broad enough for having its own book (in fact, there are many books available that I strongly suggest you give a go). Our main aim is to grasp a few key concepts, considering we are going to use it in many other examples in this and the next chapters. Therefore we are going to see Rx.JS in action in three different scenarios :
- 当我们需要减少数据来获取最终值时。
- 当我们从 HTTP 端点获取数据时。
- 当我们需要通过可观察物与多个物体交流时。
Considering how large Rx.JS is, we introduce another important concept such as the difference between hot and cold observables, to help to understand better how the other libraries work. Let’s start with a simple example on reducing an array of data and retrieve the sum of values filtering the duplicates present in the initial array . import Rx from "rxjs" const data = [1,2,10,1,3,9,6,13,11,10,10,3,19,18,17,15,4,8,4]; const onData = (value) => console.log(current sum is ${value}
); const onError = _ => console.log("stream error"); const onComplete = _ => console.log("stream completed"); const obs = Rx.Observable.from(data) .filter(value => value % 2 === 0) .distinct() .reduce((acc, value) => acc + value); obs.subscribe(onData, onError, onComplete); As you can see in the example above, we are converting an array of numbers to an observable (Rx.Observable.from(data)), then we start to transform the values inside the array step by step, applying multiple transformations. In fact, first we are filtering the values creating a new array containing only the even numbers, then we remove all the duplicates inside the array with the distinct operator provided by Rx.JS; and finally we sum the values inside the array with the reduce operator. Figure 3-6 shows the final output in the browser console. Figure 3-6 Final result returned by the previous snippet with Rx.JS Every time we are applying a transformation via an operator, we are creating a new observable that is returned at the end of the operation. This means we can concatenate multiple operators by applying several transformations to the same data source. In order to output the final result of our observable, we are subscribing to the observable with the subscribe method, and this method accepts three functions. The first one is triggered every time the observable emits data, the second one if the observable emits an error, and the last one is triggered once the observable receives a complete signal from the producer (in our case, the end of the array). Remember that these callbacks are not all mandatory; we can potentially skip to declare the error and complete callbacks if we don’t need to react to these events. Imagine for a moment how you would implement the same logic in imperative programming… done it? Ok, now you probably understood how functional and reactive paradigms can help to express complex operations in few lines of code, having a clear idea of what’s happening, without storing temporary values anywhere, and without generating any side effects. Everything is contained inside the observables and cannot be modified from external operations. The next example, instead, aims to retrieve the response from an API on the Web and then propagate the result to a hypothetical view. In this case we won’t use any specific architecture but we are going to work well with the single responsibility principle and good encapsulation . import Rx from "rxjs"; const URL = "https://jsonplaceholder.typicode.com/users"; const simplifyUserData = (user) => { return { name: user.name, email: user.email, website: user.website } } const intervalObs = Rx.Observable.interval(1000) .take(2) .mergeMap(_ => fetch(URL)) .mergeMap(data => data.json()) .mergeAll() .map(simplifyUserData) intervalObs.subscribe(user => { console.log(
user name is ${user.name}
); console.log(user email is ${user.email}
); console.log(user website is ${user.website}
); console.log('------------------------------'); }, error => console.error("error", error), complete => console.log("completed")) After creating the constant with the URL to consume, we are creating a method (simplifyUserData) for filtering the data we want to use in our application, by just returning a subset of the information instead of the entire record retrieved from that URL. The endpoint used is a public endpoint usually used for mocking data, but in our case we are going to receive an array of objects that looks like this: { id: 1, name: "Leanne Graham", username: "Bret", email: "Sincere@april.biz", address: { street: "Kulas Light", suite: "Apt. 556", city: "Gwenborough", zipcode: "92998-3874" [....] } We want to consume this endpoint every second but only twice during the application life cycle. In order to do that we create an observable with an interval of a second (Rx.Observable.interval), specifying how many times we want to perform this operation (take operator) and then we want to fetch the data from a URL (first mergeMap) and then return the data fetched as JSON, splitting the object retrieved (mergeAll operator, we could have use other operators like concatAll, for instance, obtaining the same result) in order to emit a value per time instead of the entire array in one go. Finally we simplify the data with the method we created at the beginning of the script (map operator). The final output of this example should look like Figure 3-7. Figure 3-7 Output of previous example with Rx.JS Now we need to understand in depth why we have done all these operations for manipulating the data to get the result displayed in the screenshot above. After creating the interval observable, we perform at each tick a fetch operation. Fetch is not an observable but, as you can see from the example, we are not transforming it via an operator for translating it to an observable, but we are using the operator mergeMap and inside there we are performing the fetch operation .
- 这是因为 fetch 是一个承诺 A+,这些承诺被 Rx 识别。所以不需要像标准承诺一样使用 fromPromise 操作符,我们可以在 mergeMap 操作符创建的可观察对象中直接使用它们。
- MergeMap 是一个做两件事的操作符。第一种方法是将两个流合并成一个唯一的流,然后用流内部发出的值进行迭代:在这种情况下,为了返回一个带有服务响应的流,它将承诺变平。
- 第二个 mergeMap 操作符与另一个 promise 一起使用,我们指定该流中发出的返回值应该是从端点获取的数据的 JSON 表示。
- 第二个承诺是由于 fetch API 契约,正如你在 MDN 规范中看到的:
bit.ly/2sWVEAf
。 - 最后一个操作符是 mergeAll 操作符,它通常用于合并一个可观测量中的所有可观测量,但在这种情况下,它将包含数组的最后一个承诺与检索到的数据合并,并以迭代的方式发出数组的每个单个值,而不是将整个数组作为唯一值发出,从而允许我们使用最后一个操作符(map)来简化发出的数据。
- 很容易理解 Rx 是多么的多才多艺和强大。JS 可能就是这种情况。显然,在这个阶段,我们知道需要做一些工作来熟悉 Rx 提供的所有不同运营商。但不要太难过,因为这是我们在掌握反应范式之前都做过的一步。
- 在回顾另一段代码之前,我们需要解释可观测量的另一个关键概念:当一个可观测量是热的还是冷的时,这意味着什么。
热和冷的可观测量
We can have two different types of observables: hot and cold. A cold observable is lazy and unicast by nature; it starts to emit values only when someone subscribes to it. Instead, the hot observables could emit events also before someone is listening without the need to wait for a trigger for starting its actions; also they are multicast by design. Another important characteristic that we should understand for recognizing hot and cold observables is to understand how the producer is handled in both scenarios. In cold observables, the producer lives inside the observable itself; therefore every time we subscribe to a cold observable, the producer is created again and again. Instead, in the hot observable the producer is unique and shared across multiple observers; therefore we will receive fresh values every time we are subscribing to it without receiving all of them since the first value emitted. Obviously, there are ways in hot observables to create a buffer of values to emit every time an observer is subscribing to it, but we are not going to evaluate each single scenario right now. Let’s see a hot and a cold observable in action for having a better understanding of how to use these two types of objects using Rx.JS.
冷可观察
The best way to understand a cold observable is seeing it in action: import Rx from "rxjs"; const source = Rx.Observable.interval(2000).startWith(123) source.subscribe(value => console.log("first observer", value)) setTimeout(_ =>{ source.subscribe(value => console.log("second observer", value)) }, 5000); setTimeout(_ =>{ source.subscribe(value => console.log("third observer", value)) }, 8000) The output of this small example is shown in Figure 3-8. Figure 3-8Output of cold observable with Rx.JS In this example we are creating an observable that is emitting a sequential value every 2 seconds: it will start from 0 until infinite because we didn’t specify how many values we want to emit before the event streams is completed. We use the startWith operator when we want to show an initial value in our user interface or to start a sequence of events without waiting for the values passed asynchronously. In a cold observable we have the producer, in this case the observable emitting sequential numbers, which is instantiated three times – basically any time a consumer is subscribing to it. In the image above you can clearly see the sequence of numbers is starting every time a consumer is subscribing to the producer. We can conclude that the cold observable re-instantiates the producer any time a consumer is subscribing and it is unicast so the values produced are listened by a consumer per time; also we can have multiple consumers subscribing to the same producer. By default all the observables we create in Rx.JS are cold but we have different ways for transforming them into hot observables.
可观察到的热
Let’s see what a hot observable looks like: import Rx from "rxjs"; const source = Rx.Observable.interval(2000) .startWith(123) .publish() .refCount(); source.subscribe(value => console.log("first observer", value)) setTimeout(_ =>{ source.subscribe(value => console.log("second observer", value)) }, 5000); setTimeout(_ =>{ source.subscribe(value => console.log("third observer", value)) }, 8000) The output is shown in Figure 3-9. The example is very similar to the cold observable one, but in this case we are using other two operators: publish and refCount . Figure 3-9 Output of hot observable with Rx.JS Publish operator is useful to transform our cold observables to hot observables because it returns a ConnectableObservable instead and observable object. A ConnectableObservable starts emitting values only when a connect method is called or, like in our example, when we use refCount operator. RefCount operator is used when we don’t need to control the start and stop of the ConnectableObservable but instead we automate this process; when a consumer subscribes to a ConnectableObservable, it is the logic provided by refCount that will trigger the connect method for emitting the values to the subscribers. Also the refCount logic will unsubscribe once there are any subscribers ready for receiving new values. When we have a hot observable the producer becomes multicast and the values emitted are shared across all the subscribers but at the same time we need to remember that by default, it’s not waiting for any consumer to subscribe. Instead it is emitting values immediately after the connect method is called. There are operators that will allow you to control when a hot observable starts to emit values like multicast and connect instead of refCount, which is automating these steps. Just keep this in mind when you work with Rx.JS because there are many opportunities available with this great library so keep an eye on the documentation and the implementation will become very smooth. Understanding what we are subscribing to and what are the characteristics and benefits of a hot or a cold observable could save a lot of debugging time and many headaches once our code hits the production stage. I think it is clear that Rx.JS is not just that – it’s way more than that but with these simple examples we are trying to memorize a few useful key concepts that will facilitate the integration of this library in our existing or new applications. Now it’s time to see another reactive library flavor with XStream. It’s important to understand that the concepts showed in the Rx.JS sections are very similar in other libraries; therefore owning them will allow you to pick the right library for the a project.
X 流
XStream is one of the most recent reactive libraries created by André Staltz for providing a simple way to approach reactive programming tailored specifically for Cycle.js. XStream leverages a few concepts that we have already seen in action with Rx.JS like the observables but by simplifying the core concepts behind streams. All the streams are hot observables and there isn’t a way to create cold observables with this library. I personally think the author took into consideration the reactive programming learning curve when he worked on this library, and he tried smoothing out the entry level in order to be more approachable by newbies and experts as well. On top, XStream is very fast, second only to Most.js (another reactive library), and very light too, around 30kb; and with less than 30 operators available, it represents a super basic library for dealing with observables, perfect for any prototype or project that requires the use of observables but without all the “commodities ” offered by other libraries with plenty of operators. XStream is using instead of observables the streams concepts , which are event emitters with multiple listeners. A listener is an object with a specific contract, and it has three public methods: next, error, and complete; as the name suggests, a listener object is listening to events emitted by a stream. Comparing streams and listeners to Rx.JS observables and observers, we can say that a stream is acting like a hot observable and the listener like an observer mimicking the same implementation. Last but not least, in XStream we can use producers for generating events broadcasted via a stream to multiple objects. The producer controls the life cycle of the stream emitting the values at its convenience. A producer has a specific signature, and it exposes two main methods: start and stop. We will see later an example that will introduce the producer concept. Now it’s time to see XStream in action, porting the examples we have approached previously during the Rx.JS section. The first example is related to reducing an array of values that extracts just the even numbers, removing the duplicates and calculating their sum: import xs from 'xstream'; const data = [1,2,10,1,3,9,6,13,11,10,10,3,19,18,17,15,4,8,4]; const filterEven = (value) => value % 2 === 0; const removeDuplicates = (inputStream) => { const buffer = []; const outputStream = xs.fromArray(buffer); inputStream.addListener({ next(value){ if(buffer.length === 0 || buffer.indexOf(value) < 0) buffer.push(value); } }) return outputStream; } const sumValues = (acc, value) => acc + value; const listener = { next(value){ console.log(current sum is ${value}
); }, error(){ console.error("stream error"); }, complete(){ console.log("stream completed"); } } const stream = xs.fromArray(data) .filter(filterEven) .compose(removeDuplicates) .fold(sumValues, 0); stream.addListener(listener); The data object contains an array of unordered integers with duplicates values and even and odd numbers. Our aim for this exercise is to filter the array retrieving only the even numbers, removing the duplicates after the first transformation, and finally calculating the final sum. After instantiating the array source, we are defining the transformation we are going to apply; the first one is filtering the even numbers from the initial array with the method filterEven . In this method we are checking that each value we are going to receive is divisible by 2 or not. If so it means the value is an even number so we want to keep it, otherwise we will skip it (remember that a stream will emit 1 value per time). The second method is removeDuplicates , but in XStream there isn’t an operator for doing it automatically like for Rx.JS. As we said at the beginning, XStream is meant for learning how to handle streams more than having a complete library with many operators. Therefore we are going to use the compose operator that returns a stream and expects a new stream as input. OutputStream will be our new stream used by the next operator emitting the array generated inside the removeDuplicates method. Inside removeDuplicates we create an array for storing the unique values and we push them only if are not present inside the array. The last transformation for this exercise is calculating the sum of the values filtered in the previous steps. We are going to use the fold operator that requests a function with two arguments : an accumulator and the current value to evaluate for calculating the final result, very similar to the reduce method used when you wanted to calculate the values inside an array. Finally, we can create the stream using xs.fromArray passing the initial array, and this will produce an initial stream that will emit the array values. After that we apply all the different transformations via XStream operators like filter, compose, and fold. Bear in mind that the second parameter of the fold operator is the accumulator initial value, and in our case we want to start from the value zero and sum all the others. The last step is to listen to the stream creating a listener. As we said at the beginning of this section, a listener is just an object with a specific signature (next, error, complete), and in our case we output to the console the transformation made inside our stream. Opening the dev tools of your favorite browser, you should see something like Figure 3-10. Figure 3-10 Output of XStream example, slightly different from the Rx.JS example due to the fold operator As we can see, this example made with XStream resembles the one we did previously with Rx.JS. The key takeaway here is the fact that understanding how the observables work are helping us to switch from a reactive library to another one without investing time on learning new concepts but just applying a few basic concepts with different syntax and operators. Our second example is based on retrieving some data from a REST endpoint every few seconds. In this case we can leverage the power of the producer objects available in XStream for fetching the remote data and emitting the result to a stream. Considering we want to retrieve the data every few seconds for a certain amount of times, we can use an interval stream instead of a set interval like we would do in plain JavaScript. This is our code example: import xs from "xstream"; import fetch from "fetch"; const URL = "https://jsonplaceholder.typicode.com/users"; const producer = { emitter(){ const that = this; return { next(){ emitUserData(listener); }, complete(){ that.stop() } } }, start(listener){ xs.periodic(5000).take(2).addListener(this.emitter()) }, stop(){ listener.complete(); } } const emitUserData = (listener) => { fetch.fetchUrl(URL, (error, meta, body) => { if(error) return; const data = JSON.parse(body.toString()); data.forEach(user => { listener.next(user) }, this); }) } const simplifyUserData = (user) => { return { name: user.name, email: user.email, website: user.website } } const listener = { next(user){ console.log(
user name is ${user.name}
); console.log(user email is ${user.email}
); console.log(user website is ${user.website}
); console.log('------------------------------'); }, error(){ console.error("stream error"); }, complete(){ console.log("stream completed"); } } const userStream = xs.create(producer).map(simplifyUserData); userStream.addListener(listener); As you can see in the example above, we start defining our producer object; remember that a producer is just an object that requires two methods: start and stop . The start method will be called once the first consumer subscribes to the stream. Keep in mind that a producer can have just one listener per time; therefore, in order to broadcast the results to multiple listeners, we have to create a stream that uses the producer to emit the values. Our producer contains also another method called emitter that returns a listener object that we are going to use inside the interval stream created in the start method. The start method uses the xs.periodic operator that accepts as an argument an interval of time when an event is emitted by the stream; so in our case, every 5 seconds a new event will be emitted. We also used the operator take that is used for retrieving a certain amount of values from that stream, ignoring all the others emitted. The last thing to do is to subscribe to that stream with a listener and every tick (next method) fetches the data from the endpoint. The endpoint is the same one as the previous example in Rx.JS, so we need to expect the same JSON object fetched from the endpoint. The main goal of this example is outputting a simplified version of this data that could be used in a hypothetical view of our application. When we create simplifyUserData method for extracting only the information we need from the value emitted in the stream; this function is returning a filtered object containing only a few fields instead of the entire collection. After that, we create our listener object with the typical signature next, error, and complete methods where we are handling the values emitted by the stream. Finally, we create the glue between the stream and the listener object by creating a stream with xs.create passing as the argument our producer. Then we iterate through all the values emitted, filtering the user data and in the last line of our script we associate the listener to the stream that will trigger the producer to start emitting the values. In this case there are some differences compared to Rx.JS example but again the key concepts are still there. The last example for the XStream library is focused on how we broadcast values to multiple listener objects; in this case XStream is helping us because all the streams are hot, therefore multicast by nature. We don’t need to perform any action or understand what kind of stream we are dealing with. That’s also why I recommend always starting with a simple library like XStream that contains everything we need for getting our hands dirty with streams and then moving to a more complete toolbox library like Rx.JS or Most.js . import xs from "xstream"; const periodicStream = xs.periodic(1000) .map(_ => Math.floor(Math.random()*1000) + 100); periodicStream.addListener({ next(value){ console.log("first listener", value); } }) setTimeout(_ =>{ periodicStream.addListener({ next(value){ console.log("second listener", value); } }) }, 3000); setTimeout(_ =>{ periodicStream.addListener({ next(value){ console.log("third listener", value); } }) }, 6000); The first thing is to create a stream (periodicStream constant) that emits every second a random number. Then every time we are adding a new listener to the main stream, each listener receives the value emitted by the periodicStream. Checking the output on the browser’s console we can see how the stream works; remember that we said any stream in XStream is a hot one and multicast. Therefore we will have just one producer that won’t be re-created but every time a new listener is subscribing, we receive the value emitted from that moment onward (Figure 3-11). Figure 3-11Output XStream example with hot streams Now we can see we have the expected behavior, when the second and the third listener subscribe to the stream they receive the value emitted without any information of the previous values like we have seen in the cold observable example in Rx.JS. From this last example it is clear that if you need to manage with a certain granularity the streams type in a project, Rx.JS would be the right choice for that. Otherwise XStream could simplify and speed up your development, considering that it is pretty straightforward.
背压
Another important Reactive Programming concept is backpressure and how we can use it for improving our reactive applications. When we work with multiple streams, they could emit a large amount of events in a short period of time. Therefore we need a way for alleviating the amount of data consumed by the observers if we don’t really need all of them or if the process to elaborate them is too computationally intense, and the consumer is not able to keep up. Usually we have two possibilities to handle back pressure in our application: first, we can queue the value, creating a buffer and elaborate all the values received, so in this case we don’t miss the values emitted. This strategy is called loss-less strategy . Another strategy could be skipping some events and reacting only after a certain amount of time, filtering what we receive because maybe this information is not critical for what the consumer needs to do; in this case we call this strategy lossy strategy . Imagine, for example, that we are merging two observables with a zip operator . The first observable is providing some capital case letters every second, and the second observable is emitting lowercase letters every 200 milliseconds. The zip operator in this case will create a new observable with the values of the two streams coupled together, but because it needs to couple the letters from different streams that are emitting values with different speed, inside the zip operator we have a buffer for storing the values of the second observable until the first one is going to emit the following value. Figure 3-12 should shed some light on our final goal . Figure 3-12Example of back pressure As you can see from the marble diagram above, the second stream is producing 5 times more values than the first one in the same amount of time, so the new observable will need to maintain the data in a buffer to match the values before emitting them to the consumer. Unfortunately, these kinds of scenarios in Reactive Programming are not rare and in this occasion, back pressure operators come to the rescue. These operators allow us to alleviate the pressure from the observer by simply stopping a reaction from values emitted by an observable, pausing the reception of values until we define when to resume receiving the values. Let’s write a concrete example with Rx.JS for understanding better the concept described. What we are going to create with Rx.JS and React is a simple box with a simulation of a stock that receives data in real time and it needs to display the stock value inside the component (Figure 3-13).
Figure 3-13Outcome of our next exercise on back pressure This small example is composed by two files: the main application and the React component . The main application will generate the observable that will produce random values in a specific range ready to be displayed inside the component. import React from "react"; import ReactDOM from "react-dom"; import Rx from "rxjs"; import Stock from "./Stock.jsx"; export default class App{ constructor(){ const cont = document.getElementById("app"); const observable = this.generateProducer(); const AAPL = "AAPL - Apple Inc."; ReactDOM.render(<Stock producer\(={observable} title={AAPL} />, cont); } generateProducer(){ const stockValuesProducer = Rx.Observable.interval(50) .map(value => { return (Math.random() * 50 + 100).toFixed(2); }) return stockValuesProducer; } } let app = new App(); What is happening in the main application file is that we are generating a producer (generateProducer method) that should simulate a constant interaction with a source of data, and every 50 milliseconds it is emitting a value between 100 and 150 with 2 decimals. This is a typical example where the back pressure operators could help out; we really don’t need to update the UI every 50 milliseconds because more than a useful experience, we are going to create a constant refresh that won’t be well received by our users, and it will be very intensive, in particular, on low-end machines. So what can we do to alleviate the pressure on the observer that is going to receive these values? If in generateProducer method , instead of returning the observable as it is, we could add a back pressure operator like this one: generateProducer(){ const stockValuesProducer = Rx.Observable.interval(50) .map(value => { return (Math.random() * 50 + 100).toFixed(2); }) return stockValuesProducer.sampleTime(500); } In this case, the sampleTime operator will emit a value only every 500 milliseconds, ignoring the other values received in the meantime. Just to fix this concept even better, Figure 3-14 shows how this works inside a marble diagram. Figure 3-14Marble diagram showing sampleTIme operator with half-second debounce In the component code , we are going to subscribe to the observable received from the producer property and display the value in our UI: import React from "react"; export default class Stock extends React.Component{ constructor(){ super(); this.state = {stockValue: 0}; } componentDidMount(){ this.props.producer\).subscribe(this.setStockValue.bind(this)); } setStockValue(value){ this.setState({stockValue: value}); } render(){ return (
{this.props.title}
${this.state.stockValue}
总结
In this chapter, we explained how to work with a few key reactive concepts; in particular, we introduced the concepts of streams, observables and observers on Rx.JS and streams and listeners in XStream. We also reviewed the most famous libraries implementations and how we can use them in order to manipulate our streams. We have discovered what and hot and cold observables are and how to handle the back pressure when we receive a large amount of data that we don’t need to share all of inside our stream. Now we have all the tools we need to see concrete implementation and how different frameworks/libraries introduced reactive programming in the front-end world. Obviously, the aim of this book is not going in depth on each single operator or a specific library but providing the knowledge to jump from a library to another one without headaches. There are also other libraries not mentioned in this chapter that could replace Rx.JS and XStream like, for instance, Most.js ( github.com/cujojs/most
) or IxJS ( github.com/ReactiveX/IxJS
) ) or Kefir ( rpominov.github.io/kefir/
) ) or FlyD ( github.com/paldepind/flyd
) or again Bacon.js ( baconjs.github.io/
). All of them have their pros and cons and I strongly suggest taking a look at each single one as well as picking the right library for your projects. In the next few chapters we are going to see Reactive programming in action. In particular we will explore Cycle.js and how this simple library is able to handle function reactive programming with its innovative architecture called MVI (Model-View-Intent). We then approach MobX, a library very famous in the reactive front-end world that is becoming more popular day by day and is solving the application state management in an easy and reactive way. We will finish this book with SAM Pattern, an architectural pattern that provides a reactive structure that is totally framework agnostic, giving us the possibility to integrate it in any existing or greenfield project.
四、Cycle.js 和 MVI
我们不能用创造问题时的思维来解决问题。
- —Albert Einstein
In this chapter, we are introducing Cycle.js , a functional and reactive framework that is very interesting in the front-end panorama for the number of concepts ported from different languages and wired together from the knowledgeable mind of the creator and the Cycle community. Cycle.js (cycle.js.org) was created by André Staltz, one of the most famous and active advocate of the front-end reactive panorama; in fact, André is a contributor of Rx.JS and the mind behind XStream library. As we are going to see in the chapter, Cycle.js can be used in several ways. Its modularity guarantees great results in any project, providing a great level of encapsulation and enhancing the components’ reusability across multiple projects without the need of “copying and pasting” part of a specific flow from different files like in other framework implementations.
Cycle.js 简介
The Cycle.js focus is on the interaction between computer and user, taking into consideration research on human-computer interaction and studies focused on the interfaces between human beings and computers. This interaction is a circular one between the inputs and outputs as shown in Figure 4-1. Figure 4-1Interaction schema between human and computer As we can see, a person can interact with a computer via its input methods (mouse, keyboard, microphone, touchpad…); for doing this, the person will use a hand or the voice as output. When the computer receives the input from the user, it will elaborate a new output related to the input received, providing it on the screen. The user then will be able to recognize and elaborate on the computer’s output via the user’s input sense (eyes or ears, for instance), understanding how his interactions affect the computer’s output, creating de facto, a circular interaction between computer and user: from here, the name of Cycle.js. We could summarize this interaction saying that between the human and the computer there is a dialogue where the two main actors are interacting with each other, reacting to the inputs and providing a new output. As we understand, this framework uses a different approach from the others we used to work with where the GUI is at the center; instead Cycle.js privileges more the interactions aspect over the graphical representation. In order to do that, Cycle.js introduces a message passing architecture where we send a message to an object and this one knows which part of the code to run based on the input received. This highly decoupled architecture stands on the opposite edge of the spectrum compared to more traditional architectures where dependency injection is heavily used and we favor objects interactions over reactivity. Cycle is not the only framework leveraging a message passing architecture; other examples could be retrieved in Akka with the actor-model architecture or in CSP (Communicating Sequential Processes) with channels. During the chapter, we will discover that using this approach will lead Cycle.js projects to a better separation of concerns and encapsulation, in favor of better components reusability across projects. But it’s time to see Cycle in action though, so let’s move to some concrete examples to truly appreciate the ideas behind this reactive framework.
构建简单的 Cycle.js 应用程序
The first key concept we need to stick in our mind when we work with Cycle.js is the fact that this framework is clearly separating the application logic from the side effects. For example, let’s assume we want to load some data from an endpoint after the user clicks on a button in our interface. Cycle.js separates completely the DOM rendering and the remote data fetching from the logic of preparing the HTTP request and the data manipulation to be presented in the DOM; in this way we can focus on what really matters inside our application delegating de facto the real action of manipulating the DOM or fetching remote data with a HTTPRequest object. Before examining a simple Cycle application, we need to explore how this framework is composed. In Cycle.js there are three key concepts to remember:
- 纯函数
- 流
- 司机
A pure function is a functional concept where a function can be defined as pure if it doesn’t create side effects inside the program, so when we pass a specific parameter it always returns the same output. We could summarize the pure function concept as a function where its output is determined only by the input values, and an example could be: function add(a, b){ return a + b; } In this case when I call the function add I receive a result based on the function’s arguments (a and b), and there is no possibility that external states or variables could affect the final result considering that our pure function is totally independent from the application where it is running. Another key concept is the stream , but considering we extensively talked about them in the previous chapters we can move to the next concept: the drivers. For better understanding the drivers , we need to analyze how Cycle.js is composed; otherwise we will struggle to catch why the drivers are used in this framework. Cycle.js is a modular architecture composed of multiple libraries . The core library is really small and it exposes just one method used for creating the glue between the application logic and the side effects like a DOM state rendering or remote data connection. By default, Cycle.js uses XStream as a main stream library but it allows us to use other libraries like Rx.JS, Most.js, or even a custom one created by us. We have already discussed how Cycle.js separates the application logic from the side effects : this is the key part for understanding the drivers. All the side effects in a Cycle application are handled by drivers, for instance, one of the most used ones is the DOM driver that performs the DOM manipulation received by the application logic that instead prepares a virtual DOM representation instead of interacting directly with the DOM. The communication between the drivers and the application is always made via observables; a driver can be a read and write or a read-only driver. The rule of thumb here is that a driver has always as input an observable but may or may not return an output . If we want to draw the anatomy of a Cycle application we could use this example as a skeleton: import xs from 'xstream'; import {run} from '@cycle/run'; import {makeDOMDriver, p} from '@cycle/dom' const main = sources => { const sinks = { DOM: xs.periodic(1000).map(v => p(seconds: ${v}
)) } return sinks; } const drivers = { DOM: makeDOMDriver('#app') }; run(main, drivers); Let’s analyze what we have in this basic example. After importing XStream, Cycle run function, and the DOMDriver, all the Cycle.js applications have a run function; a pure function, composed by a sources object; and a sink object as output that contains the logic for the side effects to be applied after our application finishes the elaboration. For instance, in this example we have a stream that every second is incrementing a variable and returning a virtual DOM, in this case a virtual paragraph object. Taking a look at the DOM driver , we can see that as a parameter of the DOMDriver method we need to pass the HTML element to use for appending our future DOM elements created dynamically by our Cycle.js application. The last, but essential, thing to do is calling the method run provided by the framework for creating the glue between the main function and the drivers. What the run method is doing is simple to explain; this method is creating a circular dependency between the main function and the drivers retrieving the output of each function and returning as a source of the other as we explained at the beginning of this chapter. Obviously the input and output are always object-containing observables and it’s where Cycle is really shining with its architecture. An important piece of information that we didn’t mention before is related to the virtual DOM library used in Cycle.js. Cycle uses Snabbdom ( bit.ly/2gtpUKP
) out of the box, a JavaScript library leveraging similar concepts expressed in React.js like the Virtual DOM and good diffing algorithm, on top Snabbdom offers a modular and minimal implementation compared to React (only 200 lines of code). Performance wise, Snabbdom results faster and more performant than React.js version 15 and below; it’s heavily based on function and could be used with JSX syntax with Babel if we are familiar with React.js. Snabbdom would require another chapter for explaining in depth the beauty of this Virtual DOM library, but because it’s not the main topic of this chapter we can get more information with these resources: Snabbdom on github: bit.ly/2gtpUKP
Snabbdom JSX: bit.ly/2wxqDSE
React-less Virtual DOM with Snabbdom: bit.ly/1QFVayF
Now let’s try to see a more complete example where we are going to create a simple weather forecast application that allows the user to search for a specific city and retrieve the weather forecasts for the next five days, and the UI looks like Figure 4-2. Figure 4-2Cycle.js Project UI This example will follow us for the entire chapter and we will refine it with two different approaches in order to explore properly the different possibilities offered by Cycle.js. Let’s start to list what we need to do in order to create the weather application with Cycle.js:
- 我们需要使用两个驱动程序:一个用于 DOM 操作,另一个用于从远程 REST 服务获取数据。
- 我们需要创建一个带有按钮的输入字段,允许用户搜索特定的城市。
- 我们需要请求将数据呈现给天气预报服务(在本例中是第三方服务)。
- 我们需要创建一个 UI,顶部有一个标题,突出显示当天的预测,以及接下来几天的列表。
The first thing to create is the typical Cycle skeleton application with a run method , the drivers. and the main function : const main = sources => { // here there will be our application logic } const drivers = { DOM: makeDOMDriver('#app'), HTTP: makeHTTPDriver() }; run(main, drivers); As planned, we have a DOM driver (makeDOMDriver) that will manipulate the DOM inside the div with id app and the HTTP driver that instead will perform the request to the weather forecast’s endpoint. That means in our main function we are going to return an object with two observables: one for the endpoint request providing which city the user is interested on, and one with the virtual DOM of our page. Then the drivers will take care to perform the actions for us. Let’s go ahead creating our application view, for instance, if we want to create the input field with the button shown in the application picture presented before, we need to create a function called getForm that will return to us the virtual DOM version of our elements: const getForm = () => div(".form", [ input("#location-input"), button("#location-btn", "get forecasts") ]) Now we can observe for changes happening in both interactive elements in order to capture the text inserted by the user in the input field and when the user clicks the button for retrieving the forecast. In order to do that we are going to add these few lines in our main function : const input = sources.DOM.select("#location-btn").events("mousedown"); Remember that everything can be a stream when we work with reactive programming; therefore once the driver will render our interactive elements in the real DOM, it will provide us access to the real DOM available in the DOM object and we are able to observe the user interactions, thanks to the APIs provided by the DOM driver. Every time the user will click the button we will need to retrieve what he typed and prepare the request URL and the query string for allowing the HTTP driver to perform the real request. Because we need to react when the user clicks the button but also to understand what the user wrote in the input field, we are going to combine the two streams in a unique one, and we prepare the URL with the new parameters any time the producer is producing new values, so in the main function we will add: const merged$ = xs.combine(input); const request$ = merged\(.map(([city, mouseEvt]) => getRequest(city)) .startWith(getRequest(INIT_CITY)) And we then create getRequest function that returns the composed URL: const getRequest = city => { return { url: `http://api.apixu.com/v1/forecast.json?key=04ca1fa2705645e4830214415172307&q=\){city}&days=7, category: CATEGORY } } The request$ stream will be the one that we are going to pass to the HTTP driver, and this one will perform the real HTTP request for us, as you can see Cycle is separating the application logic from the side effect, defining what the application should do from how to perform the real effect. In the combined stream, we can spot that there is a startWith method that returns a default city, in our case London, just for providing some information to the user the first time that accesses our weather application and he didn’t interact with our input field yet. It’s time to handle the response once the HTTP driver receives it; inside our main function again we are going to retrieve the HTTP object exposed by the driver, and we are going to prepare the data for a set of functions for generating the virtual DOM based on the data retrieved by the HTTP driver . const response$ = sources.HTTP.select(CATEGORY) .flatten() const vdom$ = response$.map(parseResponse) .map(simplifyData) .map(generateVDOM) .startWith(h1("Loading...")) And outside our main function we then create the functions needed for generating the UI : const parseResponse = response => JSON.parse(response.text); const simplifyData = data => { return { city: data.location.name, current: data.current, forecast: data.forecast.forecastday } } const generateVDOM = data => div(".main-container", [ h1(
Your forecasts for ${data.city}`), getForm(), generateCurrentForecast(data.current), generateNext5Days(data.forecast) ]) As you can see in the main function, once we receive the response we need to select which one we handle, in this case the CATEGORY one described at the beginning of our application. Then we need to flatten the result because the HTTP driver returns always a stream of streams, so if we want to manipulate the data in this complex structure we need to create a flat stream (flatten method) . For creating the virtual DOM that will be passed to the DOM driver , we need now to do the following:
- 1.解析响应并返回一个 JSON 对象(parseResponse 方法)。
- 2.只提取我们的 UI 需要的数据,以便呈现最终结果(simplifyData 方法)。
- 3.生成传递详细数据的虚拟 DOM(generateVDOM 方法)。
These three operations are generating a final stream with the virtual DOM that will be rendered via the DOM driver. The last bit of our main function is what it returns, so a sink object containing a stream for the DOM driver and one for the HTTP driver that represent the output of our Cycle application. This is the final implementation of our first Cycle example: import xs from 'xstream'; import {run} from '@cycle/run'; import {makeDOMDriver, div, h1, h2, h3, img, p, input, button} from '@cycle/dom'; import {makeHTTPDriver} from '@cycle/http'; import debounce from 'xstream/extra/debounce' import moment from 'moment'; const CATEGORY = "forecast"; const INIT_CITY = "London"; const getForm = () => div(".form", [ input("#location-input"), button("#location-btn", "get forecasts") ]) const generateNext5Days = forecasts => { const list = forecasts.map(forecast => { return div(".forecast-box", [ h3(moment(forecast.date).format("dddd Do MMM")), p(min ${forecast.day.mintemp_c}°C - max ${forecast.day.maxtemp_c}°C
), img(".forecast-img", { props: { src: http:${forecast.day.condition.icon}
} }), p(".status", forecast.day.condition.text) ]) }); return div(".forecasts-container", list) } const generateCurrentForecast = forecast => div(".current-forecast-container", [ div(".today-forecast", [ img(".forecast-img", { props: { src: http:${forecast.condition.icon}
} }), p(".status", forecast.condition.text) ]), h3(moment(forecast.last_updated).format("dddd Do MMMM YYYY")), h2(${forecast.temp_c}°C
), p(humidity: ${forecast.humidity}%
) ]) const parseResponse = response => JSON.parse(response.text); const simplifyData = data => { return { city: data.location.name, current: data.current, forecast: data.forecast.forecastday } } const generateVDOM = data => div(".main-container", [ h1(Your forecasts for ${data.city}
), getForm(), generateCurrentForecast(data.current), generateNext5Days(data.forecast) ]) const getRequest = city => { return { url: http://api.apixu.com/v1/forecast.json?key=04ca1fa2705645e4830214415172307&q=${city}&days=7
, category: CATEGORY } } const main = sources => { const input = sources.DOM.select("#location-btn").events("mousedown"); const merged$ = xs.combine(input); const request$ = merged = sources.HTTP.select(CATEGORY) .flatten() const vdom$ = response\(.map(parseResponse) .map(simplifyData) .map(generateVDOM) .startWith(h1("Loading...")) return { DOM: vdom\), HTTP: request$ } } const drivers = { DOM: makeDOMDriver('#app'), HTTP: makeHTTPDriver() }; run(main, drivers); It’s important to highlight a couple of things in this example. First of all, in our main function we are handling the input and the output of our application; we are not operating any real side effects that, instead, are delegated to the drivers. The drivers and the main applications are communicating via streams ; remember that Cycle.js is a message passing architecture, and this approach facilitates the data flow of our applications maintaining a high separation between application logic and side effects and a strong encapsulation. For the first time in this book, we are looking to a reactive implementation where the communication between different parts of our architecture are made by streams; interestingly there isn’t any knowledge in our application on how a driver is going to handle the side effects and we are not calling any specific method exposed by a driver. There is just a circular dependency between our main function and the driver that communicates only via streams. It’s important to iterate again these concepts because they will become very useful from now on considering we are going to discover MVI (model view intent), a reactive architecture heavily based on them.
模型视图意图架构
If you are familiar with ELM language and its architecture, MVI won’t surprise you at all, but we need to admit that this is definitely a great improvement from the architecture we used in the past and in other famous frameworks like Redux or Angular. But first, let’s see what the Model View Intent is and how it differs from the other frameworks. The first characteristic of this architecture is that it follows the unidirectional flow like the Flux pattern introduced by Facebook right after React.js, unidirectional flow is becoming a constant in many front-end reactive architectures. What it means is that the data flow is always going in a unique direction and it never changes; this helps the debugging of your application and the possibility of adding new team members without a long induction period for explaining how the architecture of your applications work or how the system works. Flux Pattern Flux is a pattern introduced by Facebook that forces a monodirectional communication flow in web applications (Figure 4-3). Figure 4-3Flux data flow diagram As you can see from the schema above, every time the user interacts with an element in the view, this one dispatches an action that is caught by a global dispatcher. The main aim of the dispatcher is triggering the callbacks the stores have registered in order to listen for the actions they are interested in. Once the store receives the data from the action performed, the changes needed for the view emit a change event to the view that will retrieve the data from the store and then they will render the changes updating the components’ stated. For more information visit:
bit.ly/2rHN8F0
. Another characteristic we mentioned previously is the fact that the communication between Models Views and Intents happens via streams only; therefore there isn’t any direct control between different modules but just a stream as input and one as output, like we have seen in the communication between Cycle.js application logic and drivers. MVI is composed of three main modules :
- 我们精心设计用户交互并保存应用程序状态的模型。
- 我们将 UI 与模型提供的状态连接起来的视图。
- 我们订阅用户交互或输入,并将它们提供给模型以将状态更改为新状态的意图。
Figure 4-4 shows this architecture . Figure 4-4MVI data flow diagram The renderer part represents the DOM driver in this case. Let’s see now how we can change our simple Cycle.js example using model view intent architecture. The code we are going to explore is very similar to the previous example, so we will highlight only the key parts without investing too much time on how we have parsed the data retrieved from the HTTP driver or how we compose the virtual DOM elements rendered later on by the DOM driver. The first thing to do is to identify in our previous example the part in our main function that should be allocated to different parts of an MVI architecture. Originally our main function was implemented in the following way: const main = sources => { const input = sources.DOM.select("#location-btn").events("mousedown"); const merged$ = xs.combine(input); const request$ = merged = sources.HTTP.select(CATEGORY) .flatten() const vdom$ = response\(.map(parseResponse) .map(simplifyData) .map(generateVDOM) .startWith(h1("Loading...")) return { DOM: vdom\), HTTP: request$ } } We can immediately identify the intent part in the first few lines of our implementation. As we said with the intent we are capturing the user intentions, therefore all the DOM interactions . In fact, the intent receives as input the real DOM after being rendered and as output the user intentions as streams: const intent = DOM => { const input = DOM.select("#location-btn").events("mousedown"); return xs.combine(input) .map(([city, mouseEvt])=> getRequest(city)) .startWith(getRequest(INIT_CITY)) } The stream with the request will be passed to the model and to the HTTP driver for executing the request to the remote endpoint. Then we need to handle the response received from the HTTP driver , in this case the model will take care of it by preparing the data for the view: const model = (actions => { return state\(.map(generateVDOM) .startWith(h1("Loading...")) } So our main function now will look like: const main = sources => { const actions\) = intent(sources.DOM); const state$ = model(actions = view(state\(); return { DOM: vdom\), HTTP: actions$ } } Then we can take a look at the full example with MVI applied: import xs from 'xstream'; import {run} from '@cycle/run'; import {makeDOMDriver, div, h1, h2, h3, img, p, input, button} from '@cycle/dom'; import {makeHTTPDriver} from '@cycle/http'; import moment from 'moment'; const CATEGORY = "forecast"; const INIT_CITY = "London"; const getForm = () => div(".form", [ input("#location-input"), button("#location-btn", "get forecasts") ]) const generateNext5Days = forecasts => { const list = forecasts.map(forecast => { return div(".forecast-box", [ h3(moment(forecast.date).format("dddd Do MMM")), p(
min ${forecast.day.mintemp_c}°C - max ${forecast.day.maxtemp_c}°C
), img(".forecast-img", { props: { src: http:${forecast.day.condition.icon}
} }), p(".status", forecast.day.condition.text) ]) }); return div(".forecasts-container", list) } const generateCurrentForecast = forecast => div(".current-forecast-container", [ div(".today-forecast", [ img(".forecast-img", { props: { src: http:${forecast.condition.icon}
} }), p(".status", forecast.condition.text) ]), h3(moment(forecast.last_updated).format("dddd Do MMMM YYYY")), h2(${forecast.temp_c}°C
), p(humidity: ${forecast.humidity}%
) ]) const generateVDOM = data => div(".main-container", [ h1(Your forecasts in ${data.city}
), getForm(), generateCurrentForecast(data.current), generateNext5Days(data.forecast) ]) const parseResponse = response => JSON.parse(response.text); const simplifyData = data => { return { city: data.location.name, current: data.current, forecast: data.forecast.forecastday } } const getRequest = city => { return { url: http://api.apixu.com/v1/forecast.json?key=04ca1fa2705645e4830214415172307&q=${city}&days=7
, category: CATEGORY } } const model = (actions\(, HTTP) => { return HTTP.select(CATEGORY) .flatten() .map(parseResponse) .map(simplifyData) } const intent = DOM => { const input\) = DOM.select("#location-input").events("focusout") .map(evt => evt.target.value); const btn$ = DOM.select("#location-btn").events("mousedown"); return xs.combine(input) .map(([city, mouseEvt])=> getRequest(city)) .startWith(getRequest(INIT_CITY)) } const view = state$ => { return state\(.map(generateVDOM) .startWith(h1("Loading...")) } const main = sources => { const actions\) = intent(sources.DOM); const state$ = model(actions = view(state\(); return { DOM: vdom\), HTTP: actions$ } } const drivers = { DOM: makeDOMDriver('#app'), HTTP: makeHTTPDriver() }; run(main, drivers); MVI is not as complicated as it looks like; we just need to get used to it. I’d like to highlight a few key concepts that we need to bear in mind when we integrate this architecture in a Cycle.js application:
- 首先,我们将我们的 Cycle.js 项目转换成一个结构化的项目,其中的每个部分都可以被重用和单独测试。
- 这种架构甚至允许我们进一步将 MVI 架构应用于每个组件:一个 MVI 架构应用于表单,一个应用于当前日期,一个应用于工作日列表(由于其性质)。
- 使用流进行通信允许整个架构更加灵活,并增强了关注点的分离。
Before concluding the chapter with an overview of what Cycle.js brings to the reactive programming world, we need to enhance this example once again, introducing the official Cycle.js state management called Onionify.
Cycle.js 和状态管理
After seeing MVI in action, our journey continues with Onionify, a library created for managing the application state in Cycle.js . As we know, handling the state is the key part of any web application. Cycle.js provides a unique approach to that, slightly different from what we are used to seeing with Angular or Redux. Onionify is a tiny library (2kb only) with only one purpose: managing the application state in Cycle applications. This library doesn’t provide a driver, as we have seen in other occasions, but instead Onionify is wrapping the entire Cycle.js application with a unique state that is injected across multiple components. The application state is a stream managed internally by Onionify, and each component can manipulate its own state and the parent components one via reducers. The components need to be “isolated” with the homonymous library called isolate . Isolate is a utility provided by Cycle.js that allows us to literally isolate a component, sharing only the sources provided by the main application; or a parent component, and returning a sink object that could be shared with the main application and/or other components. Let’s stop here for a moment and try to gather what we have learned until now about Cycle.js :
- 我们知道我们可以创建一个 MVI 架构的应用程序。
- MVI 不仅可以应用于整个循环应用,还可以应用于组件。
- 这导致相同的架构应用于我们架构的所有级别,其中接收器和源是对象之间通信的唯一方式。
Considering all these facts, we can say that with Cycle.js, applying MVI, we can create a Fractal architecture that will allow us to use always the same “piece” (model view intent) for generating a bigger composition made by identical pieces applied several times. Fractal architecture Fractal architecture is not a new concept in the software development. This architecture with identical subsystems structures allows a high separation of concerns in order to shape a large project where modifying, deleting, or creating new parts won’t affect the entire application considering the isolation in which the subsystems live. Onionify applied in conjunction with MVI architecture helps by creating a solid and reusable architecture with strong separation of concerns and good encapsulation. Therefore we should be able to reuse part of our Cycle.js application in others just respecting the contract our components need, so using the correct sources (drivers and streams) and interacting with the sinks returned by them. In order to see Onionify in action, we are going to modify our weather application once again, splitting our MVI application in multiple components and using Onionify for changing the state. Figure 4-5 shows what our Cycle.js application will look like after introducing Onionify. Figure 4-5Cycle Onionify project diagram Let’s analyze what we have here compared to the previous application:
- 我们有三个组成部分:城市形态、今日预测和未来预测。
- 我们有一个状态,由主应用程序传递给由状态 observable 组成的 Onionify 包装器。
- 我们仍然以和以前一样的方式和车手交流;因此,我们仍然有 HTTP 和 DOM 驱动程序。
CityForm is the component with the main logic, and it is responsible for retrieving what the user is typing inside the input field and also to prepare the request that will perform then by the HTTP driver. import onionify from 'cycle-onionify'; import xs from 'xstream'; import {div, input, button, h1} from '@cycle/dom'; const INIT_CITY = "London"; const CITY_SEARCH = "citySearchAction"; const CATEGORY = "forecast"; const getRequest = city => ({ type: CITY_SEARCH, city: city, url: http://api.apixu.com/v1/forecast.json?key=04ca1fa2705645e4830 214415172307&q=${city}&days=7
, category: CATEGORY }) const getForm = location => div(".form", [ h1(Your forecasts in ${location.city}
), input("#location-input", {props: {value: ${location.city}
}}), button("#location-btn", "get forecasts") ]) const parseResponse = response => JSON.parse(response.text); const simplifyData = data => function changeState(prevState) { return { city: data.location.name, current: data.current, forecasts: data.forecast.forecastday } } const model = (actions\(, HTTP) => { const reducer\) = HTTP.select(CATEGORY) .flatten() .map(parseResponse) .map(simplifyData) return reducer$ } const intent = DOM => { const input = DOM.select("#location-btn").events("mousedown"); return xs.combine(input) .map(([city, mouseEvt]) => getRequest(city)) .startWith(getRequest(INIT_CITY)) } const view = state$ => state\(.map(state => getForm(state)) export const CityForm = sources => { const state\) = sources.onion.state = intent(sources.DOM); const reducer$ = model(actions = view(state\(); return { DOM: vdom\), onion: reducer } } As we can immediately recognize, we have a new parameter from the sources that is provided by Onionify wrapper; this library provides an onion object that contains the state stream, based on that we can interact with the parent state stream, reacting to that, or manipulating the internal component state as well. Overall the component is very similar to what we had in the previous application, and the only change is related to the application state that represents the response coming from the weather API, so the CityForm is using the state stream just for retrieving the location chosen by the user. The last thing to mention is to understand what this component is returning as sink and how we can immediately spot the onion property containing the HTTP response as the state of the application. As we can recognize, this component is self-contained, so if we would like to reuse it in another application we would be able to do it without the need for changing anything: that’s the power of working with a fractal architecture where the reusability, separation of concerns, and encapsulation are first citizens in the architecture. Before investigating how we need to modify App.js for handling Onionify library, let’s do a quick tour of the other two passive components: TodayForecast and FutureForecast. These two components are passive because they just need to render some content provided by the state; they don’t have user interactions and they are not going to manipulate any parent state or perform new HTTP requests. This is the TodayForecast component : import {div, h2, h3, img, p} from '@cycle/dom'; import moment from 'moment'; const generateCurrentForecast = forecast => div(".current-forecast-container", [ div(".today-forecast", [ img(".forecast-img", { props: { src: http:${forecast.condition.icon}
} }), p(".status", forecast.condition.text) ]), h3(moment(forecast.last_updated).format("dddd Do MMMM YYYY")), h2(${forecast.temp_c}°C
), p(humidity: ${forecast.humidity}%
) ]) const view = state$ => state\(.map(state => generateCurrentForecast(state.current)) export const TodayForecast = sources => { const state\) = sources.onion.state = view(state\() return { DOM: vdom\) } } In this chunk of code we can spot in the TodayForecast function the state stream for rendering the view that corresponds to the weather data representation for the specific moment when the user is requesting the forecasts. Considering this is a passive component, its only duty is providing the virtual dom to the DOM driver for rendering the view. Obviously, in case of any user interaction that could change the application state, this will be reflected in the sink and it would have been able to share the new state via the onionify property of our sink – the same in case it would need to trigger a new HTTP request to the weather forecast endpoint. Let’s take a look to the FutureForecast component then: import {div, h3, img, p} from '@cycle/dom'; import moment from 'moment'; const generateNext5Days = forecasts => { const list = forecasts.map(forecast => div(".forecast-box", [ h3(moment(forecast.date).format("dddd Do MMM")), p(min ${forecast.day.mintemp_c}°C - max ${forecast.day.maxtemp_c}°C
), img(".forecast-img", { props: { src: http:${forecast.day.condition.icon}
} }), p(".status", forecast.day.condition.text) ]) ); return div(".forecasts-container", list) } const view = state$ => state\(.map(state => generateNext5Days(state.forecasts)) export const FutureForecast = sources => { const state\) = sources.onion.state = view(state\() return { DOM: vdom\) } } Also, this component is very similar to the previous one, and it doesn’t need to share any state update. It’s just consuming the state stream in order to render the new virtual dom to provide to the DOM driver. Finally, it’s the turn of App.js where we can find the glue for our “onionified application ”: import xs from 'xstream'; import {run} from '@cycle/run'; import {makeDOMDriver, div, h1} from '@cycle/dom'; import {makeHTTPDriver} from '@cycle/http'; import isolate from '@cycle/isolate'; import onionify from 'cycle-onionify'; import {CityForm} from './CityForm'; import {TodayForecast} from './TodayForecast'; import {FutureForecast} from './FutureForecast'; const generateVDOM = ([formVNode, todayVNode, futureVNode]) => div(".main-container", [ formVNode, todayVNode, futureVNode ]) const view = (locationDOM, futureForecastDOM\() => { return xs.combine(locationDOM\), todayForecastDOM) .map(combinedStreams => generateVDOM(combinedStreams)) .startWith(h1("Loading...")); } const main = sources => { const cityLens = { get: state => state, set: (state, childState) => childState } const locationSink = isolate(CityForm, {onion: cityLens})(sources); const todayForecastSink = isolate(TodayForecast, {onion: cityLens})(sources); const futureForecastSink = isolate(FutureForecast, {onion: cityLens})(sources); const locationReducer = locationSink.HTTP; const vdom$ = view(locationSink.DOM, todayForecastSink.DOM, futureForecastSink.DOM); return { DOM: vdom, onion: locationReducer$ } } const drivers = { DOM: makeDOMDriver('#app'), HTTP: makeHTTPDriver() }; const mainOnionified = onionify(main); run(mainOnionified, drivers); Here we can find quite a few interesting new concepts, so let’s start to describe what we are doing from the end of our JavaScript file. As we mentioned, Onionify is not a driver but a wrapper around our application; therefore we used it for wrapping our main function and we passed the decorated, or onionified, version of our main function to the run method. This allows us to pass the state through different components via the onionify property in our sources and sinks. Let’s now take a look on what the main function looks like. We start with a lens called cityLens. Lenses are used when a component needs to access the same object of its parent like in our case but also when we need to manipulate the state before it lands into a specific component. Technically speaking a lens is an object with a getter and a setter, nothing really complicated, but they are very useful in functional programming specifically when we use them for composing objects. Lenses are also well known in the Haskell language, in the JavaScript world they are used in conjunction with Immutable.js , and definitely they are present in Ramda (lensProp method for instance), a JavaScript functional library. After the lens definition , which will allow us to share the data present in the component with the others, we have the instantiation of our three custom components with the isolate utility offered by Cycle.js. We are passing the lens to each component and the sources, and this allows us to get interaction with the parent component inside each component . In this case it’s the parent component in the application itself but it’s clear by the approach, working in this way, we can wrap our component defining a standard input/output contract and reuse it easily in different parts of the same application or even better in other applications. After creating the instances of our components it’s time to pass the manipulated state and the HTTP request to the associated properties in our sink object. In this case we need to highlight a couple of things. In this project the state is handled by the endpoint response, and there isn’t any manipulation from other components that are just used for the rendering phase. Obviously, if we check other examples present in the Onionify repository ( bit.ly/2eQXh9S
), we can see that instead of passing just a stream as an application state like we are doing, we can combine multiple streams from different components in order to store a more complex application state. The last bit is retrieving all the streams containing the virtual dom prepared by the different components and combine them all together for producing the look and feel of our Cycle application. For doing that, we create a view method that collects all the streams and combines them in a unique one that will generate the final stream with the virtual dom that will be rendered by the DOM driver. I’d like to get your attention on the way we are doing that because as you can see, it’s a little bit verbose, in particular when the application grows and we need to merge multiple components. In these cases we should prepare some utils methods for handling these situations (wrapping in a single or multiple functions would make the deal). Cycle.js doesn’t come with anything useful out of the box, but there are some libraries that are aiming for that. At the moment, however, there is nothing official for achieving a more readable and flexible approach.
总结
In this chapter, we evaluated our first fully reactive framework, understanding what Cycle.js brings to the table, and the different architectural approaches used for achieving that. Cycle is maintained by a small but very talented community that is porting many reactive and functional concepts from other languages in the JavaScript community. I believe that it is one of the most innovative approaches we can find in the front-end reactive architecture ecosystem at the moment. MVI is, in my opinion, merging the best from older architectures like MVP, for instance, and the unidirectional flow that is characterizing the latest front-end architectures like Redux. There is still a lot to do in our reactive journey, so it’s time to move on and talk about our next functional reactive state management library: MobX.
五、MobX:简单状态管理
做,或者不做。没有‘尝试’。—尤达
After a deep dive into the first reactive framework, we can continue our journey discovering others’ reactive architectures, reviewing a flexible and easy-to-use state management system like MobX . MobX is simple but very effective state management system that we can integrate in many projects independently from the stack we are using. Obviously being simple doesn’t mean incomplete; we can really structure complex applications with the help of MobX state tree: an opinionated, transactional and MobX powered state container heavily based on the tree data structure for handling the application state. During this chapter, we are going to review a couple of examples that will give us an idea of how to structure a MobX application and what are the benefits of using it in our projects.
MobX 简介
The philosophy behind MobX is very simple: Anything that can be derived from the application state, should be derived. Automatically. Usually when we have a non-reactive application and we want to update its state, we create methods for manipulating manually specific parts of the model and for updating the views, potentially causing inconsistency between the two, sometimes increasing the coupling between them. With MobX we are trying to minimize these situations having an automated update, via subscription (observer) , to reactions that happened at the application state level (observables). MobX has a few key concepts to remember in order to properly embrace the library:
- 可观测量用于导出我们的应用程序的状态;这里的经验法则是,一个可观察对象应该存储一个定义我们的应用程序中的状态的值。
- 计算值是用于以同步方式使用函数从应用程序状态自动导出复杂值的属性;这种机制允许更好地预测和调试我们的计算值。计算值延迟更新。
- 反应类似于计算值,因为从应用程序状态开始生成新值,但在这种情况下,它们用于生成副作用。
- 动作是用于改变应用程序状态的方法;如果我们指定在严格模式下使用 MobX,动作是更新应用程序状态的唯一方式。
- 观察器在视图中用于对任何状态变化做出反应。
With just these concepts we can start to build a simple MobX application to get familiar with it. MobX finds the perfect fit with Virtual DOM libraries like React.js; actually, React and MobX are a great combination for creating reactive applications, but obviously this won’t mean other frameworks are out of scope, but React is combined perfectly with the MobX philosophy. Before going ahead, let’s see MobX in practice with a simple counter : import React from 'react'; import ReactDOM from 'react-dom'; import {observable} from 'mobx'; import {observer} from 'mobx-react'; const counterState = observable({ value: 0, inc: function(){ counter.value++ }, dec: function(){ counter.value-- } }); @observer export class Counter extends React.Component{ render(){ return (
bit.ly/2xHYifx
In-depth explanation of MobX: bit.ly/2g2fYIW
Fundamental principles behind MobX: bit.ly/2x7YcJL
After this introduction on the key elements behind MobX, it’s time to see a MobX application in the real world, so in this case we are going to build a MobX and React application based on an image gallery and the same one with MobX state tree. In this way we can compare the two approaches, understanding the pros and cons of both and describing the different architectural approach.
我们的第一个 MobX 和 React 应用
In this chapter’s projects, we are exploring four key architectural areas to cover in the majority of front-end applications like these:
- 如何在应用程序中管理数据流。
- 如何处理用户交互?
- 如何使用远程端点。
- 如何管理应用程序的状态?
The application we are going to build is an image gallery similar to Figure 5-1. Figure 5-1The project UI It could be interpreted as a component of a larger application but it’s important to cover the concepts listed above and then we will be able to replicate them in projects at scale. The service we are going to consume for searching images is the Flickr APIs. Flickrs allow us to perform a search on their catalogue based on a specific search term. Figure 5-2 shows how our MobX application is structured.
Figure 5-2MobX project architecture diagram On the left of the diagram we have all the views, and in our case are passive views that are used only for rendering the user interface and gathering the interactions handled by the stores. Each view has a 1 to 1 relation with a store; this is very similar to what we have done in the second chapter when we analyzed the MVP architecture. Doing it will guarantee a great separation of concerns (view representation and application behaviours are separated) and components reusability (we could change completely the views without affecting the application behaviours and vice-versa) – two important principles to take into consideration in any software architecture. On the right we have instead all the stores instantiated from the stores object. In each store we are going to inject an application model called GalleryModel . This is not a concept requested by MobX, but we want to create solid architectures with great flexibility and high code standards, creating a model that will allow us to gather all the application states, or component states, inside a unique object that will be visible to all the stores. In this way, it will be easier to maintain separation of the different stores that are not aware of the existence of others and on top will facilitate the application debugging because the application state is present in a unique place instead of being spread in different stores. Considering MobX uses observables for storing the application state, this means that inside the model we are going to have observables and potentially some actions for changing the observable, but all the computed values will be closer to the view at the store level. Let’s now take a look at the folder structure used for this project.
检查文件夹结构
Obviously, the project is also available on the Github repository of this book, so feel free to download it to better help you to follow the next steps. As you can see in Figure 5-3 the folder structure resembles our architecture with a separation per file type (stores, models, components. and so on) instead of a domain approach (a search folder, am image gallery folder…). Figure 5-3MobX project structure In large applications I usually prefer gathering the files in domain folders, and this could really help to understand better the application, in particular, for new developers that are approaching the application for the first time. This way it’s easier to find a correlation of what we see on the screen and the folder name when the names are meaningful enough. It is time to write our MobX application, but the first thing we do is to compose the general structure of our application, defining it in the App.js file. We need to create three main React components: a text input for performing a search, a pictures grid, and a selected image component for better viewing an image. ... @observer export class App extends React.Component{ render(){ return ( <Provider {...stores}>
total images: ${this.props.photoGallery.totalPics}
} 
- 1.用户与调用商店中的动作的 UI 进行交互。
- 2.该操作更新了主模型中的一个可观察对象。
- 3.订阅该可观察对象的所有计算值都在更新该值。
- 4.视图中存在的计算值或可观察值导致呈现具有新状态的组件。
This is a simple MobX example that provides the idea of how this framework is using the reactive programming paradigm via observables. As we can see, the single responsibility principle is respected and in a certain way enforced by the observables, computed values, and actions. At the same time we can spot a certain freedom that probably won’t help much in larger teams where any developer could possibly interpret this flexibility as a way to take shortcuts if needed. Not only for this reason but also for providing a more structured way for writing large applications, we can possibly use MobX state tree, an opinionated library that is creating clear boundaries around the application structure, and it helps to create a solid and scalable architecture enforcing the concepts of automatic derivation and immutable state in an effective way.
MobX 状态树
As the name suggests, MobX state tree is based on immutable trees, in particular the benefits provided by this library are the following:
- 编写隐藏了反应式实现的“命令式”代码的可能性,因此接触反应式范例的新开发人员的学习曲线应该更容易。
- 可变数据结构,其中在幕后由库本身创建和维护不可变的树快照,还提供了与同构应用程序很好地工作的可能性,其中我们可以为特定状态注入树,应用程序将对此作出反应。
- 商店类型系统提供开箱即用的可能性,指定原始和自定义值。
- 商店级别的生命周期挂钩,类似于 React 组件中的挂钩。
- 明确如何构建存储以及如何更新应用程序状态的界限。
These are just a few of the key features provided by MobX state tree. The interesting point of MobX state tree is the fact we are dealing with a tree structure, similar to what we are used to when we work with the DOM on the UI. At the same time, this means we have a strong hierarchy to deal with. Therefore when we structure our state trees we need to pay more attention to how we encapsulate our data and how we handle the dependencies between different branches of the tree. The best way to understand the problems we might encounter using MobX state tree is trying to implement it in our previous project in order to find possible solutions and new approaches. Refactoring the previous project won’t require many changes; in fact the views will remain almost the same. Refer to Figure 5-5 for an eagle eye view of our architecture before deep diving inside the code. Figure 5-5MobX state tree application architecture Comparing this architecture with the previous one we immediately notice a few differences :
- 在组件内部,我们将注入整个树,而不像前面的实现那样只是一个特定的商店。
- GalleryModel 不是在商店内创建的对象,而是由应用程序树组成的。
- 商店之间有一个层次结构,有一个根和多个节点(PhotoGalleryStore、SearchStore 和 SelectedPictureStore ),因此我们可以在商店之间双向遍历数据。
In a certain way, this last feature could be the most dangerous to deal with, because if we are not able to structure the stores in a smart way respecting our domain, we could end up in a large application with a lot of dependencies between stores, thus introducing coupling where it is not needed and complicating the evolution of our project or the refactor of specific design implementations. A suggestion I feel that is useful to share is that we should structure our large applications with multiple trees, as this will help us to maintain and potentially reuse our code in a better way. The trick is always dividing by domain our application; when we are able to identify clearly the boundaries of a specific domain, structuring a state tree becomes easier. After this brief excursus of MobX state tree architecture, it is time to see how we could apply this to our image gallery project, so we are going to analyze the differences compared with the previous project where we used just MobX with a custom architecture. App.js introduces a few new concepts strictly related to the MobX state tree library: ... const augmentedStore = types.compose(GalleryModel, Stores); const PicsGalleryStore = augmentedStore.create({ picsList: [], searchTerm: "", selectedPictureURL: "" }, {config: Config}); @observer export class App extends React.Component{ render(){ return (
- 模型需要一个描述模型状态的对象(类似于 MobX 中的 observables)。
- 视图需要一个对象,我们在其中指定链接到应用程序状态的计算值,它们对应用程序状态做出反应。
- 动作是 UI 用来与应用程序状态交互的方法。
With this in mind we can move forward analyzing the rest of the stores. In the SearchStore there are a couple of other important things to highlight, so let’s check the implementation of the computed value inside the views object: get url(){ return getEnv(self).config.getSearchURL(getParent(self).searchTerm); } getEnv and getParent are the other two methods provided by MobX state tree. The first one is used for getting access to the objects injected inside the main tree, in our case the configuration util object used for creating the final URL to use when we consume the Flickr APIs. getParent, instead, allows us to retrieve the parent node of the tree, in our case the root that we composed with the main GalleryModel; searchTerm is an observable described inside the main model. As we can see, this approach could result in difficult maintenance, particularly when the tree has many nested nodes. That’s why we need to think twice when we work in this way: the secret here is keeping the trees with not many nested levels and to work more encapsulating the components of our application. The functionalities in the search store are exactly the same as the previous example but expressed in the MobX state tree way. Moving forward we can briefly take a look at the other two stores; the PhotoGalleryStore is implemented like this: import { types, getParent } from 'mobx-state-tree' const photoGalleryStore = types.model("Gallery", { }).views(self => ({ get totalPics(){ return getParent(self).picsList.length; } })).actions(self =>({ setPictures(arr){ getParent(self).setPicsList(arr); } })) export default photoGalleryStore; Once again, we are communicating with the GalleryModel via the getParent method, considering the main model is composed inside the root of our state tree. For the SelectedPictureStore , instead, the implementation is this one: import { types, getParent } from "mobx-state-tree"; const selectedPictureStore = types.model({ }).actions(self => ({ setPictureURL(url){ getParent(self).setSelectedPictureURL(url); } })) export default selectedPictureStore; And also in this case we are just setting the final URL to display and automatically the components will react to the change happening to the selectedPictureURL observable in the main model. Overall, the porting from a MobX project to a state tree one doesn’t request much effort but provides a lot of value due to its opinionated nature of doing things and enforcing rules for a better application structure.
总结
In this chapter, we have evaluated how MobX can be used in the real world as a reactive state management providing automated and optimized derivations for improving our application performances and providing predictability. We discovered that MobX can be used in conjunction with React, but there are also several implementations for other virtual DOM libraries like Vue.js or Snabbdom. Last but not least we have evaluated two examples: one with MobX only; and another one with MobX state tree, an opinionated library, which provides a tree structure to our application state, mirroring the concepts behind a DOM tree and providing several utilities for composing and decorating our application state. Architecturally speaking, MobX provides a level of flexibility that is hard to find with other frameworks, and it’s the perfect companion for moving existing applications to a reactive paradigm improving their predictability and for anyone that wants to embrace the reactive paradigm quickly. Using a MobX state tree for new applications or proof of concepts provides a quick and well-designed structure improving our productivity without compromising on quality. And the last important point is that in both examples, we extracted the application state from the stores guaranteeing a better separation of concerns and less coupling between stores that are unaware of the existence of others maintaining their bounded context appropriately.
六、SAM:函数式反应式模式
因为,你知道,弹性——如果你从淘金热的角度来考虑,那么你现在会非常沮丧,因为最后一块金子会消失。但好的一面是,有了创新,就不会有最后的掘金。每一个新事物都会产生两个新问题和两个新机会。—杰夫·贝索斯
In this chapter we are going to explore the SAM Pattern, less used inside the community but still part of the functional/reactive family. SAM stands for State-Action- Model , three actors that we have already encountered in other frameworks, but with the SAM pattern we are going to discover a different use for them. Let’s start with saying that SAM is following the unidirectional flow pattern, also is heavily based on functional concepts; in fact functions are first-class citizens for the correct implementation of the final architecture. Because SAM is a pattern, it’s totally framework agnostic, and it can be used with MobX, Cycle.js, Angular, or any other framework we are using in a specific project, also in our Vanilla JavaScript projects if we want. During this chapter, we explore a “vanilla” implementation of SAM in order to understand how this pattern uses the reactive paradigm for managing the data flow between objects.
SAM 简介
SAM was created with clear goals in mind, and we could summarize them in this way:
- 组合:在 SAM 中,我们大量使用组合和纯函数;这允许我们减少对象之间的耦合,并且容易地在隔离模式的不同部分中进行测试。
- 单向数据流:正如我们在 Cycle.js 中看到的,SAM 也通过高阶函数利用单向数据流的概念,在我们应用程序的不同部分之间创建一个反应循环。
- 框架无关性:SAM 是框架无关性的,因此不仅可以在客户机上使用,如果需要的话也可以在服务器上使用。
- 被动视图:使用 SAM 允许我们拥有被动视图,它们与行为完全分离,允许我们单独测试特定视图,并在运行时或传输时潜在地更改它们,保持相同的行为。
- 分形架构:在我们的旅程中,我们已经遇到了包含分形架构范例的 Cycle.js。SAM 也在做同样的事情:提供使用封装良好的组件的灵活性,这些组件可以按照相同的架构原则组合在一起。
Figure 6-1 provides a schematic to understand this pattern. Figure 6-1SAM pattern schematic As mentioned before, SAM is composed by the state, action, and model , and the flow for a SAM project is the following one:
- 一个动作由用户交互触发,主要职责是将用户意图转化为要在模型中验证的数据。
- 模型需要评估动作接收到的数据,它可以接受也可以不接受。如果数据不符合预期,模型可以决定触发错误或无声地失败。模型还负责维护应用程序状态和触发状态表示。以这种模式实现模型的最佳方式是使用单个状态树。
- 顾名思义,状态是一个从模型中计算状态表示并通知视图需要呈现新内容的函数。状态还负责调用下一个动作谓词,这个名为 nap 的函数将调用任何需要调用的自动动作。
The reactivity in this pattern is composed by the loop of the main actors, which are changing the state representation after every action invoked. SAM pattern could be summarized with a mathematical formula : View = State( viewmodel(Model.present(Action(data))), nextActionPredicate(Model) ) SAM takes inspiration from React components implementation where React introduced a strong decoupling between the data to represent and the view itself; therefore any virtual DOM library fits very well in the SAM pattern. In fact, the state representation is just providing an output that will be computed by a virtual DOM library like React, Vue.js, or Snabbdom. Obviously, we can also think to use template libraries but the Virtual DOM ones fit better for the pattern purposes. The state representation is not meant to keep the state but to merge the data with the virtual dom or template, retrieving the state from the model and combining it with a specific interface. The model, as mentioned above, has to evaluate values provided by an action; it exposes only the present function, and it can decide to accept or refuse the data received, triggering an error or silently stopping the loop for a specific scenario. The actions are very similar to what an intent is doing in Cycle.js: they are preparing the data to be proposed to the model. The actions can be invoked by user interactions on the UI or via the nap method, and this method is called after the state representation for automatically changing the model and triggering another render of the view.
SAM 模式数据流
If we were to summarize with code the SAM pattern data flow , we could do it with a skeleton like this: const model = { status: STATUS.INIT, present: data => { if(model.status === STATUS.INIT){ model.status = STATUS.STATE1; } if(Array.isArray(data)){ model.status = STATUS.INIT; } state.render(model) } }; const actions = { state1: (value, present) => { present({data: value}); } } const nap = model => { if (model.status === STATUS.INIT) { actions.state1(value, model.present); } } const view = { init: model => { return
SAM 的基本实现
This time we want to build an interface similar to Figure 6-2. Figure 6-2Our new interface This interface has some peculiarities, so the first thing to do would be to load the countries data provided by an open API. Then we will need to generate the list on the left of the map above where every time a user is clicking on a country name, we want to display few information on the right side of our UI, like the country name, the flag, the population size, the capital, and an interactive map showing the country coordinates. Considering we start the application without any user interaction we are going immediately to set a default country, so we can fill up the space and provide some information on how to use our application to the final user. In this case the next-action-predicate will help us to achieve this task. We are going now to create the application based on the skeleton we have explored before; remember that the SAM pattern embraces the simplicity of a clean and robust architecture with the power of two programming paradigms like functional and reactive ones.
回顾示例
Let’s start to analyze our example from the beginning. The first thing to do is to wait until the first action for consuming a remote endpoint, retrieving the specific data requested for displaying the selected country details : document.addEventListener("DOMContentLoaded", function(event) { actions.getCountries(REGION, model.present) }); We can immediately see that we are passing to the getCountries action a default country and the present method of the model that will be invoked once the promise inside the action will be fulfilled: getCountries: (region, present) => { fetch(URL + region) .then(response => response.json()) .then(data => { const countries = normaliseData(data); present(countries); }) } Once we receive the response and we normalize the data filtering with only what the view needs, we call the present method injected as argument, and this method is responsible for accepting or not the data prepared by the action. Therefore we can say that these actions have the responsibility of preparing data that the model consumes and uses for then rendering a new state. Let’s see what our model looks like: const model = { status: STATUS.INIT, selectedCountryID: "", countries: [], present: data => { if(data.selectedID !== "" || model.status === STATUS.INIT){ model.status = STATUS.SELECT_COUNTRY; model.selectedCountryID = data.selectedID; } if(Array.isArray(data)){ model.status = STATUS.INIT; model.countries = data; } state.render(model) } }; The model is an object with a status property where we store the current application state, a few parameters used for rendering the new state like countries and selectedCountryID, and finally the method present that is invoked every time by actions only. Inside the present method we can see the checks for each single possible value we are going to receive from an action. This could become a long list of checks, so in large applications we would need to wrap these statements in external files for making the code more readable and easy to manage. Once the checks are passed the present function invokes the render method of the state object: const state = { init: model => (model.status === STATUS.INIT && model.countries.length > 0 && model.selectedCountryID == ""), selectCountry: model => (model.status === STATUS.SELECT_COUNTRY && model.selectedCountryID !== ""), render: model => { stateRepresentation(model); nap(model); } } In the state object, we have some methods used for understanding in which state the application is, like the init and selectCountry methods, and also the render method that is split in preparing the state representation and invoking the nap function. The state representation uses the application state for merging the data stored in the model with the UI to display: const stateRepresentation = model => { let representation =
- 一旦用户与 countries 列表中的元素进行交互,就会触发一个动作,向模型的 present 方法提供国家 ID。
- 该模型检查从操作接收的数据类型,并将操作提供的 ID 存储在 selectedCountryID 中。
- 然后,模型触发状态表示,我们根据模型数据组装新视图。
- 视图被推送到 display 方法,该方法会将新的视图状态附加到 DOM。
- 最后,我们调用 nap 函数,检查在特定的应用程序状态下,我们是否需要触发任何自动操作。
SAM is an interesting pattern because it combines S.O.L.I.D. principles with different programming paradigms. When we want to implement it in a real project we need to be aware of the freedom provided by this pattern, considering at the moment there aren’t many frameworks that are enforcing SAM rules. At the same time, SAM provides us a great structure, a strong encapsulation, and applies the single responsibility principle very well for each main part of the pattern. This allows us to test quickly and in total isolation our projects, and considering it’s heavily based on pure functions, we are not going to have any side effects from them. Therefore the outcome predictability of this pattern is very high. As I said at the beginning of the chapter, we can implement SAM in our existing architectures or we could use an architecture that bases its foundations into SAM providing a solid structure for handling elegantly the data flow of our applications.
总结
In this chapter, we looked at another approach to reactive architectures. As we have learned, there are many interpretations and all of them are providing pros and cons to take in consideration. SAM in particular has solid foundations with great principles to be inspired by, and the simplicity of its implementation grants us the ability to apply the SAM pattern in any project, existing or greenfield, without the need to use specific frameworks for using it. Now it’s time to look at the future of reactive paradigm and in particular how we will envision this paradigm in the front-end panorama for the next years. But this time I won’t be alone, so let us move on to the last chapter of this book.
七、反应式的未来
未来属于今天为它做准备的人。—马尔孔十世
Is this the end of our journey of Reactive architectures? I have to say that’s not the end at all, and probably it’s just the beginning! In this book I tried to collect some information to be really productive with Reactive Programming. Often you can find resources that explain how to use Reactive Programming with Rx.JS, for instance, but none of them clearly present concrete examples on how to implement Reactive Architectures in practice. Reactive Architectures as well as Reactive Programming are just at the beginning of their life cycle, and there are plenty of other things to discover: viewing Reactive concepts from different angles and probably learning from the mistakes made at the beginning of this journey. Embracing Reactive architectures right now could provide you a competitive advantage for the future of your projects and/or career. I personally think Reactive Programming is going to impact the way we are thinking about software programming, and this paradigm has a learning curve that is far from being the easiest one, but once we understand a few key concepts, every line of code we write will become better and better. Another important value provided by Reactive Programming is the fact we can “mix and match” with existing architectures, enhancing our projects and moving them to the next step forward, enhancing objects encapsulation and the single responsibility principles. I don’t want to spend an entire chapter talking about how I embraced the Reactive paradigm because I think it’s clear that I really get into it; however I thought it would be beneficial hearing also some stories from the most active developers in the Reactive ecosystem. Therefore, I decided to interview them and share how they envision the future of this paradigm on front-end development. Following, you will be able to read the answers to a questionnaire I prepared for the following:
- 本·莱什-Rx。JS 5 创建者
- Michel west west-mobx creator
I hope you will enjoy what these gurus have to say on the topic and that you have enjoyed this journey of Reactive Front-End Architectures.
本·莱什
Can you give us a brief introduction of yourself? My name is Ben Lesh, and I’m an engineer at Google, formerly at Netflix, and I’m the development lead for RxJS. What are you doing for the reactive community? I’m working on making RxJS smaller, faster, and more ergonomic. Why should a company or a developer embrace Reactive Programming? Reactive programming enables a lot of agility in code by being able to quickly, declaratively define almost any behavior with very little code. In your opinion, what are the Pros and the Cons of Reactive Programming? The biggest pro is that you have a single type that can represent pretty much anything. Values that are either synchronous or asynchronous. A single value, many values, or no values at all. And because this type represents a set, there are a great number of transformations, joins, and queries you can perform on them. The biggest con is that in order to do all of the above, you have to learn a vernacular that is particular to reactive programming. This means learning terms like mergeMap or switchMap, etc. Which is the most important benefit provided by Reactive Programming? The biggest benefit of reactive programming is being able to break applications down into smaller pieces of work that are truly independent from one another. Talking about Reactive architectures, do you think there will be an evolution from where we are now? If so, which one? I think we’re about to see a lot of reactive languages start popping up. I know of two off the top of my head right now. One is a server-side reactive programming language that Facebook is working on. Another is a client-side reactive programming language being worked on by a colleague of mine. Which framework or architecture do you usually use for your Reactive projects and why? Over the last three years I have used Ember, React, and Angular. Angular has first-class handling for rxjs observables. Since I work on an end of the project at Google, I use Angular, but I have no favorite. However the Angular Team itself is one of my favorite groups of people so I guess I give them a little favoritism. What is missing in the Reactive panorama? We need more tooling for debugging in the browser. You can only do so much within a library, and it would be nice to have better tools built into Chrome to divide some of the more hairy scenarios in rxjs. What would you change in the Reactive ecosystem? I wish we had more contributors. I particularly wish we had a more diverse set of contributors. When I say contributors I mean pretty much anything from engineering talent to community organization. The reactive programming Community is very small even though RxJS is widely used. Which is the best approach to start working with Reactive Programming? I think it’s best just to pull it into something like jsbin and start using it and playing with it. Try a few operators and start with things like filter, map, and scan, then move on to something like mergeMap. Talking about the library you created, can you share with us your vision for its future? Well in the not-too-distant future we’re going to be moving away from prototype patching and using more functional-based operators. So basically we’re going to have higher-order functions that return a function you can use in the let operator. There will also be more utility around that. This will allow better tree shaking, which means smaller bundles for people using rxjs with something like webpack. I’m also working very hard on reducing the size of the library. If you could change one thing right now in your library, what would it be? I’m already working on it. Mostly I just wish it was less work so I could be done already. Haha. Can you share one trick of your library that few developers understand or are using in this moment? I think I talked about this in my talk at AngularConnect last year. But it’s important for people to realize what they’re doing is setting up a chain of Observers. And that observers all have the guarantee that if error or complete is called on them they can no longer next. So if you want to handle an error and keep an observable alive, you need to isolate that Observer chain in a merge map and punctuated with a catch or a retry. Reactive programming in 10 years, what is your prevision? I predict that it’ll be a little bit more mainstream than it is now. It’s one of those infectious ideas that’s hard to convey to people but once they get it they can’t help but tell other people. Anything else to add? I’d really like to thank Eric Meijer, Matt Podwysocki for creating RxJS, and the rest of the Rx community for supporting RxJS these last few years.
米歇尔·韦斯特
Can you give us a brief introduction of yourself? I have been programming for almost 20 years, and 10 years professionally. Started with Pascal, Object Pascal as a kid, then went to C# and Java and finally ended up doing mainly JavaScript / TypeScript. The pace of innovation in the JavaScript and TypeScript world makes it a very interesting environment. What are you doing for the reactive community? Over a year ago I open sourced the MobX reactive programming library. It was initially developed for Mendix, but it solves many of the problems present in the React community (although it is generically applicable and used without the React community as well). Why should a company or a developer embrace Reactive Programming? If reactive programming is well applied, it increases decoupling and increases the declarativity of the code. This is the result of the fact that Reactive Programming separates the description of behavior from the enforcement of that behavior; with Reactive Programming, behavior of a value is declared once, while the consistent execution of that behavior is enforced by the Reactive Programming library. I often compare it with civil laws versus laws of nature. The former state a wish, but they need law enforcement to ensure the behavior is applied in the majority of the cases. Laws of nature, on the other hand, cannot be violated intentionally or accidentally. The system simply guarantees consistency. As an example, MobX guarantees that any React component that renders a piece of data will update as soon as that data is changed. It doesn’t matter how the component got the data. It will update. Like towing a skateboard with a cart; if the car starts moving, so will the skateboard. Simply because you introduced a rope tying the two things together. You don’t bother enforcing that relation. The relation is no longer the idea of a relation, it is an actual one. And nature, or MobX, will always do its job. In your opinion, what are the Pros and the Cons of Reactive Programming? Pro: More declarative in nature. You just specify what should happen. Then it is up to the system to figure out when it should happen. Pro: Inversion of control: You don’t care about who is listening to the event you are emitting, or the value you are changing. This increases decoupling significantly. Eases refactoring, etc. Pro: Better scalable architecture: Because producer and consumer are strictly separated, you are more flexible in changing either of them. It avoids the spaghetti code related to all the effects that need to happen when updating a value, which is so typical for large imperative projects. Con: Inversion of control also means an additional level of indirection. So when doing using Reactive Programming, between the event emitted or the value changed, there is always some library internals you have to debug through, before you arrive at the effect they cause. (Some libraries will even run the effects on a different stack, making this significantly harder.) Con: Reactive Programming code is often harder to grasp. Especially when using observable streams, you need to know the meaning of a whole range of stream-specific operators before you are able to read what was intended. Which is the most important benefit provided by Reactive Programming? Decoupling, then declarativity. Talking about Reactive architectures, do you think there will be an evolution from where we are now? If so, which one? I think reactivity across system boundaries is an area that will evolve significantly in the future. Meteor did some groundbreaking work here in the past, and we see this idea still in horizonDB, firebase, and GraphQL subscriptions. I think that in the future we will consume and react to events in the database just as easily as we react to events in the client-side state. Another dream I have is to introduce a reactive (virtual?) filesystem and build our build tools on top of that. If each file is just a reactive stream of data, we will able to combine the efficiency of Makefiles with real-time updates we have started to appreciate so much in file watchers. With proper dependency analysis, we will remove all the quirks where a chained watcher will build too much, too early, or in the wrong order. Which framework or architecture do you usually use for your Reactive projects and why? There are two mainstream categories of reactivity: Reactive streams and Reactive values (or cells). Reactive streams are very powerful if the derivation of a value requires knowledge of the past or time. Examples of this are debouncing events, network requests etc., producing values from a sliding window of past values etc. In these cases the Rx* family provides very powerful tools. However, when deriving values, it only requires knowledge of the present, reactive data cells are much more convenient to work with. This is the mechanism that made spreadsheets so popular. With reactive data cells, one does often not need to set up explicit subscriptions, and one can use all the first-class operators of the language, rather than having stream-specific libraries. This mechanism makes libraries like Knockout, Meteor, Vuejs, and MobX so approachable. What is missing in the Reactive panorama? Reactive file system interactions. What would you change in the Reactive ecosystem? There are two important mainstream categories of reactive systems: Reactive streams, and (transparent) reactive cells (or values). They both solve quite different problems, but failing to recognize the distinction leads to quite some confusion. Which is the best approach to start working with Reactive Programming? Build a web application with React and MobX to manage the client-side state. Then add RxJS to debounce search queries and network requests. Talking about the library you created, can you share with us your vision for its future? MobX is barely visible in your code; it is mostly there at the background. I want to make it even more invisible when Proxies are standardized in browsers. If you could change one thing right now in your library, what would it be? Separate the reactivity algorithms and core api from the observable data collections. That would make it easier to target different environments (e.g., ES3 or Proxy-supporting environments) and make MobX more accessible for those who don’t like the property wrapping it like it does on objects. Can you share one trick of your library that few developers understand or are using in this moment? Keep side effects really small. But derive as much data as possible. It’s often surprising how much one can derive from the state. It keeps imperative code to a minimum. Reactive programming in 10 years, what is your prevision? More closely connected front-end / back-end architectures where the notion of client-side state largely disappears for domain data. Anything else to add? Can you send me a copy of the book 😃
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!