二、架构比较
如果你觉得好的建筑很贵,那就试试差的建筑。—布莱恩·富特
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: https://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(, cont); } Listing 2-2CalculatorController.js Then we will use the React Component life-cycle methods to store these two objects locally and listen for any change from the model; when a change happens, we are going to update a state property inside the React component to display the correct value. See Listing 2-3. componentWillMount(){ this.model = this.props.model; this.emitter = this.props.emitter; this.emitter.on("TotalChanged", _ => this.setState({displayValue: this.model.total})); this.setState({displayValue: this.model.total}) } Listing 2-3Calculator.jsx So, every time the displayValue property is updated, this will trigger the render function; therefore the view will be updated with a new result as shown in Listing 2-4. render(){ return(
模型视图演示者
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
- {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(, cont); } } Listing 2-12App.js The Calculator view is a React component that has two key concepts: dispatching the data from the view to the view-model and retrieving the data from the view-model. In order to do that we can use a data binding library or simply events as both are accepted from MVVM architecture, therefore we are using an event emitter, injected when the view and the view-model were instantiated, for handling the communication between these two objects, as shown in Listing 2-13. componentWillMount(){ this.setState({displayValue: this.props.initValue}) this.emitter = this.props.emitter; this.emitter.on("UpdateDisplayValue", value => this.setState({displayValue: value})); } manageDisplayState(value){ switch (value) { case "AC": this.emitter.emit("ResetEvent") break; case "=": this.emitter.emit("CalculateEvent") break; case "+": case "-": case "/": case "*": case ".": this.emitter.emit("AddValueEvent", {value: value, type: SYMBOL}) break; default: this.emitter.emit("AddValueEvent", {value: value, type: NUMERIC}) break; } } Listing 2-13 Calculator.jsx In manageDisplayState we have all the events we will communicate to the view-model, but in the componentWillMount we define only the event we need to listen for updating the calculator’s display. The rest of the view is very similar to the other views we have discussed in the previous examples. Now it’s the turn of our view-model. Here we need to do exactly the opposite of how we handled the events in the view; therefore we are going to listen for all the events emitted by the view and dispatch the event for updating the view’s display every time the value changes. See Listing 2-14. initialize(){ this.emitter.on("CalculateEvent", _ => { this.calculate() }); this.emitter.on("AddValueEvent", content => { this.addValue(content.value, content.type); }); this.emitter.on("ResetEvent", _ => { this.reset(); }) } updateDisplayAndState(state){ this.state = state; this.emitter.emit("UpdateDisplayValue", this.model.total); } Listing 2-14 CalculatorViewModel.js The view-model is responsible for retrieving and updating the data stored in the model, so every time the user clicks a button in our view we are going to update the value in the model and then call the updateDisplayAndState method for dispatching this change to the view. We keep a state internal to the view-model to understand which method we need to call during the calculation; therefore we need to update it when the state is changed. Let’s see, for instance, how we handle the changes in the three methods we have created for handling these operations, as shown in Listing 2-15. addValue(value, type){ let valueToAdd; if(type === NUMERIC){ valueToAdd = this.getValue(value); } else if(type === SYMBOL){ valueToAdd = this.checkSymbol(value); } this.model.add(valueToAdd) this.updateDisplayAndState(IN_PROGRESS_OPERATION) } reset(){ this.model.reset(); this.updateDisplayAndState(FIRST_OPERATION) } calculate(value){ this.model.calculate(value); this.updateDisplayAndState(FIRST_OPERATION) } Listing 2-15 CalculatorViewModel.js All of them are doing two main operations :
- 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: https://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: https://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:
- {{value}}
余烬
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 And automatically the CLI will generate a route file, the associated test file, and the template, and then it will also insert the code for the new route to the application route system. The route we created for our calculator exposes the model to the template; in Ember only the routes and the controllers have access to the model. Therefore, there is a strong separation of concerns between the view and how the model should be in an MVVM architecture . export default Ember.Route.extend({ model(){ this.store.push({ data:[{ id: 1, type: "calculator", attributes: { total: "0", state: AppState.FIRST_OPERATION }, relationships: {} }] }); return this.store.peekRecord('calculator', 1); } }); The concept of Model in Ember is slightly different from what we are used to; the Model class defines that the value will be present in the store (the concrete model) facilitating the data validation when they hit the client side of our Ember application. If we check our model class, present inside the models folder, we can see that we are defining two properties” both of type string, one called total and the other state. See Listing 2-21. import DS from 'ember-data'; export default DS.Model.extend({ total: DS.attr("string"), state: DS.attr("string") }); Listing 2-21 models/calculator.js As you can see from the route code above, the store is a data structure centralized for the entire application (think of it as a Singleton), accessible by the routes and controllers, so independently from the amount of templates, routers, or controllers our application is composed of, we have a unique source of truth to fetch or cache data. The store object is a very interesting one because it allows data to be automated and fetched from an endpoint, and then it will store the response directly inside the store object without handling all the code for defining these kinds of operations. The store works with records; a record is a concrete model that contains the data fetched from a REST endpoint or cached inside the application like in our case. It’s very important to notice the structure of an object stored in a record, as shown in Listing 2-22. { id: 1, type: "calculator", attributes: { total: "0", state: AppState.FIRST_OPERATION }, relationships: {} } Listing 2-22 routes/calculator.js Inside the store we need to define the data object with a unique ID; a type value, used for retrieving it later on; and the attributes we need to store inside it, for our application will be the value total and the application state. Because we are not fetching the data from any remote endpoint we are using peek Record method, which will skip the server request and retrieve the data directly from the concrete model. Instead if we would need to retrieve data from a specific REST endpoint, we could use find Record that will perform a request to the endpoint specified, and it will store the response inside the store object. Our application logic sits in the controller where we are providing public methods for the views to be called and we are handling the setting and getting of data to and from the model. When we extend the base controller from the Ember framework we have an object called actions where we can expose all the methods to the template; in our case we just have one method called updateTotal : export default Ember.Controller.extend({ onScreenKeyboard: [ ["7", "8", "9", "/"], ["4", "5", "6", ""], ["1", "2", "3", "-"], ["0", ".", "=", "+"] ], actions:{ updateTotal(value){ let result; let model = this.store.peekRecord('calculator', 1); switch (value) { case "AC": model.set("total", 0); model.set("state", AppState.FIRST_OPERATION); break; case "=": result = math.eval(model.get("total")); model.set("total", result); model.set("state", AppState.FIRST_OPERATION); break; case "+": case "-": case "/": case "": case ".": result = checkSymbol(model.get("total"), value, model.get("state")); model.set("total", result); model.set("state", AppState.CALCULATING); break; default: result = getValue(model.get("total"), value, model.get("state")); model.set("total", result); model.set("state", AppState.CALCULATING); break; } } } }); Here in each case we are retrieving the current value from the store and manipulating it, but the annoying part is that Ember works a lot with strings in order to identify a specific object or value. Therefore we won’t have code completion provided by our editor and it could be prone to error if it’s not properly wrapped in a constant statement. The last bit to discuss is the template. Ember is using handlebars out of the box; therefore if we are familiar with this famous template library we will be productive in no time. Handlebars has some specific markup, like Angular, for identifying specific behaviors like filtering, creating similar tags populated with data retrieved from the controller or the route and so on. This is the handlebars code needed in order to render our calculator:
- {{#each row as |value|}}
- {{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: https://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( , cont); } } let app = new App(); Listing 2-23 App.js 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
- {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.
版权属于:月萌API www.moonapi.com,转载请注明出处