Core points
Design patterns are often integrated into popular frameworks. For example, the Model-View-Controller (MVC) design pattern is everywhere. In JavaScript, it is difficult to separate frameworks from design patterns. Often, a specific framework will come with its own interpretation of this design pattern. Frames come with perspectives, and each frame forces you to think in some way.
Modern frameworks determine the specific implementation method of the MVC model. When all explanations are different, this can be confusing, thereby adding noise and confusion. Frustrating confusion occurs when any code base adopts multiple frameworks. The question in my mind is, is there a better way?
MVC pattern is suitable for client frameworks, but modern frameworks will change. Today’s modernization will die over time. In this case, I want to explore alternatives and see where a little discipline can take us.
Read modern JavaScript and keep abreast of the ever-changing world of JavaScript! Read this book The MVC model itself can be traced back decades. This makes it a design pattern worth your investment in programming skills. MVC mode is a mode that can exist independently. The question is, how far can this take us?
First of all, I want to eliminate this common misunderstanding: design patterns are not frameworks. Design patterns are a rigorous way to solve code problems. This requires a certain skill level and puts the responsibility on the programmer. Design patterns separate concerns and facilitate writing clean code.
The frame is different because it does not have to follow any design pattern. One way to distinguish between frameworks and patterns is to look for Hollywood principles. The Hollywood principle is: "Don't call us, we'll call you." There is a dependency at any time to decide when you use it, and it's a framework. The framework is much like Hollywood, and you can't decide what to do or how to do it. In fact, developers are like actors, following scripts when asked to act.
There are many good reasons to avoid client frameworks:
MVC design pattern originated from Xerox Smalltalk research project in the 1970s and 1980s. This is a time-tested mode for the front-end graphical user interface. This pattern comes from desktop applications, but has been proven to work well for web applications as well.
Its core is that the MVC design pattern is about a clear separation of concerns. The purpose is to make the solution clear and understandable. Any programmer who wants to make a specific change can easily find the right place.
Penguin! Cute and furry, one of the cutest creatures on earth. In fact, they are very cute, with 17 different penguins, not all of them live in Antarctica's environment.
It's time to make a Penguin demo! I'll show a deck showing several species on a page. For this, I want to use MVC design patterns and a little discipline. I will use extreme programming methods to use unit testing and simple methods to solve the problem at hand. Finally, you should be able to browse through several penguins, each with its own data and profile picture.
By the end of this example, you should have learned enough to use the MVC design pattern in pure JavaScript. The pattern itself is very easy to test, so good unit testing is expected.
I will stick with ES5 in this demo for cross-browser compatibility reasons. It makes sense to use proven language features combined with this permanent design pattern.
Are you ready? Let's wait and see.
The demo will consist of three main parts: the controller, the view, and the model. Each section has its own concerns and issues that need to be solved.
The following is a visualization of its appearance:
PenguinController handles events and is the intermediary between the view and the model. It calculates what happens when the user performs an action (for example, clicking a button or pressing a key). Client-specific logic can be placed in the controller. In a larger system with a lot of things to do, you can break it down into modules. The controller is the entry point for events and the only mediator between the view and the data.
PenguinView cares about DOM. DOM is the browser API you use to perform HTML operations. In MVC, no part of the change DOM except the view. The view can attach user events, but leave the event handling issue to the controller. The main command of the view is to change the status the user sees on the screen. For this demonstration, the view will use pure JavaScript for DOM operations.
PenguinModel cares about data. In client JavaScript, this means Ajax. One advantage of MVC pattern is that you now have a single location for server-side Ajax calls. This makes it easier for other programmers who are not familiar with the solution to get started. The model in this design pattern only cares about JSON or objects from the server.
One anti-pattern is against this inherent separation of concerns. For example, the model must not care about HTML. Views must not care about Ajax. The controller must act as a mediator without worrying about implementation details.
I find that when using this pattern, developers initially have good intentions, but they will leak their concerns. It's tempting to turn everything into a web component and end up in a mess. Focus on features and user-oriented concerns. However, functional concerns are different from functional concerns.
What I like in programming is to make a clear separation of functional concerns. Each individual programming problem will be solved consistently. This makes it easier to understand when you read the code. The purpose is to write easy-to-understand code so that others can also make positive contributions.
It's not a good demonstration without a real example that you can see and touch. So, needless to say, here is a CodePen showing the Penguin's demonstration:
View SitePoint (@SitePoint) Pen A Demo of Penguins on CodePen.
I have said enough, it’s time to write code.
View and model are two components used by the controller. The controller contains all the components needed to do the job in its constructor:
<code>var PenguinController = function PenguinController(penguinView, penguinModel) { this.penguinView = penguinView; this.penguinModel = penguinModel; }; </code>
The constructor uses control inversion and injects the module in this way. This mode allows you to inject any component that satisfies the advanced contract. Think of it as a great way to abstract code and implement details. This pattern allows you to write clean code in pure JavaScript.
The user events are then connected and handled in this way:
<code>PenguinController.prototype.initialize = function initialize() { this.penguinView.onClickGetPenguin = this.onClickGetPenguin.bind(this); }; PenguinController.prototype.onClickGetPenguin = function onClickGetPenguin(e) { var target = e.currentTarget; var index = parseInt(target.dataset.penguinIndex, 10); this.penguinModel.getPenguin(index, this.showPenguin.bind(this)); }; </code>
Note that this event uses the current target to get the state stored in the DOM. In this case, the DOM will tell you all about its current status. The current status of the DOM is what the user sees on the browser. You can store the status data in the DOM itself, as long as the controller does not change the status.
After the event is triggered, the controller will get the data and explain what will happen next. This.showPenguin() callback is interesting:
<code>PenguinController.prototype.showPenguin = function showPenguin(penguinModelData) { var penguinViewModel = { name: penguinModelData.name, imageUrl: penguinModelData.imageUrl, size: penguinModelData.size, favoriteFood: penguinModelData.favoriteFood }; penguinViewModel.previousIndex = penguinModelData.index - 1; penguinViewModel.nextIndex = penguinModelData.index + 1; if (penguinModelData.index === 0) { penguinViewModel.previousIndex = penguinModelData.count - 1; } if (penguinModelData.index === penguinModelData.count - 1) { penguinViewModel.nextIndex = 0; } this.penguinView.render(penguinViewModel); }; </code>
The controller calculates the index of each penguin and tells the view to render it. It takes data from the model and converts it into objects that the view understands and cares about.
The following is a unit test showing the happy path when penguin:
<code>var PenguinController = function PenguinController(penguinView, penguinModel) { this.penguinView = penguinView; this.penguinModel = penguinModel; }; </code>
PenguinViewMock has the same contract as real implementation. This makes it possible to write unit tests and make assertions. Assertions come from Node assertions and also exist in Chai assertions. This allows you to write tests that can run on Node and on your browser.
Please note that the controller does not care about implementation details. It uses the contract provided by the view, such as this.render(). That's the discipline required to write clean code. The controller can trust that every component can do what it says. This increases transparency and makes the code easier to read.
View only cares about DOM elements and connection events, for example:
<code>PenguinController.prototype.initialize = function initialize() { this.penguinView.onClickGetPenguin = this.onClickGetPenguin.bind(this); }; PenguinController.prototype.onClickGetPenguin = function onClickGetPenguin(e) { var target = e.currentTarget; var index = parseInt(target.dataset.penguinIndex, 10); this.penguinModel.getPenguin(index, this.showPenguin.bind(this)); }; </code>
When it changes the status seen by the user, the implementation is as follows:
<code>PenguinController.prototype.showPenguin = function showPenguin(penguinModelData) { var penguinViewModel = { name: penguinModelData.name, imageUrl: penguinModelData.imageUrl, size: penguinModelData.size, favoriteFood: penguinModelData.favoriteFood }; penguinViewModel.previousIndex = penguinModelData.index - 1; penguinViewModel.nextIndex = penguinModelData.index + 1; if (penguinModelData.index === 0) { penguinViewModel.previousIndex = penguinModelData.count - 1; } if (penguinModelData.index === penguinModelData.count - 1) { penguinViewModel.nextIndex = 0; } this.penguinView.render(penguinViewModel); }; </code>
Note that its main concern is converting view model data to HTML and changing the state. The second is to connect the click event and let the controller act as an entry point. After the status changes, the event handler is attached to the DOM. This technology handles event management at one time.
To test this, we can verify that the element has been updated and changed the status:
<code>var PenguinViewMock = function PenguinViewMock() { this.calledRenderWith = null; }; PenguinViewMock.prototype.render = function render(penguinViewModel) { this.calledRenderWith = penguinViewModel; }; // Arrange var penguinViewMock = new PenguinViewMock(); var controller = new PenguinController(penguinViewMock, null); var penguinModelData = { name: 'Chinstrap', imageUrl: 'http://chinstrapl.jpg', size: '5.0kg (m), 4.8kg (f)', favoriteFood: 'krill', index: 2, count: 5 }; // Act controller.showPenguin(penguinModelData); // Assert assert.strictEqual(penguinViewMock.calledRenderWith.name, 'Chinstrap'); assert.strictEqual(penguinViewMock.calledRenderWith.imageUrl, 'http://chinstrapl.jpg'); assert.strictEqual(penguinViewMock.calledRenderWith.size, '5.0kg (m), 4.8kg (f)'); assert.strictEqual(penguinViewMock.calledRenderWith.favoriteFood, 'krill'); assert.strictEqual(penguinViewMock.calledRenderWith.previousIndex, 1); assert.strictEqual(penguinViewMock.calledRenderWith.nextIndex, 3); </code>
This solves all major issues, state changes and connection events. But where does the data come from?
In MVC, all models care about Ajax. For example:
<code>var PenguinView = function PenguinView(element) { this.element = element; this.onClickGetPenguin = null; }; </code>
Note that the module XMLHttpRequest is injected into the constructor. This is a way to let other programmers know what components this model needs. If the model needs more than simple Ajax, you can use more modules to represent this. Also, using unit testing, I can inject mocks that have exactly the same contract as the original module.
It's time to get the penguin based on the index:
<code>PenguinView.prototype.render = function render(viewModel) { this.element.innerHTML = '<h3>' + viewModel.name + '</h3>' + '<img alt="' + viewModel.name + '" src="'%20+%20viewModel.imageUrl%20+%0A%20%20%20%20%20%20'">' + '<p><b>Size:</b> ' + viewModel.size + '</p>' + '<p><b>Favorite food:</b> ' + viewModel.favoriteFood + '</p>' + '<a href="https://www.php.cn/link/f0b875eb6cff6fd5f491e6b6521c7510"> ' data-penguin-index="' + viewModel.previousIndex + '">Previous</a> ' + '<a href="https://www.php.cn/link/f0b875eb6cff6fd5f491e6b6521c7510"> ' data-penguin-index="' + viewModel.nextIndex + '">Next</a>'; this.previousIndex = viewModel.previousIndex; this.nextIndex = viewModel.nextIndex; // Wire up click events, and let the controller handle events var previousPenguin = this.element.querySelector('#previousPenguin'); previousPenguin.addEventListener('click', this.onClickGetPenguin); var nextPenguin = this.element.querySelector('#nextPenguin'); nextPenguin.addEventListener('click', this.onClickGetPenguin); nextPenguin.focus(); }; </code>
This points to an endpoint and gets data from the server. We can test this by mocking data with unit tests:
<code>var ElementMock = function ElementMock() { this.innerHTML = null; }; // Stub functions, so we can pass the test ElementMock.prototype.querySelector = function querySelector() { }; ElementMock.prototype.addEventListener = function addEventListener() { }; ElementMock.prototype.focus = function focus() { }; // Arrange var elementMock = new ElementMock(); var view = new PenguinView(elementMock); var viewModel = { name: 'Chinstrap', imageUrl: 'http://chinstrap1.jpg', size: '5.0kg (m), 4.8kg (f)', favoriteFood: 'krill', previousIndex: 1, nextIndex: 2 }; // Act view.render(viewModel); // Assert assert(elementMock.innerHTML.indexOf(viewModel.name) > 0); assert(elementMock.innerHTML.indexOf(viewModel.imageUrl) > 0); assert(elementMock.innerHTML.indexOf(viewModel.size) > 0); assert(elementMock.innerHTML.indexOf(viewModel.favoriteFood) > 0); assert(elementMock.innerHTML.indexOf(viewModel.previousIndex) > 0); assert(elementMock.innerHTML.indexOf(viewModel.nextIndex) > 0); </code>
As you can see, the model cares only about the original data. This means using Ajax and JavaScript objects. If you are not aware of Ajax in pure JavaScript, there is an article with more information.
For any discipline, it is important to obtain guaranteed work. The MVC design pattern does not stipulate how to solve the problem. Design patterns provide you with a broad set of boundaries that enable you to write clean code. This saves you from dependence. For me, this means providing a complete set of unit tests for each use case. Testing provides guidance on how the code can be useful. This makes it open and tempting to any programmer who wants to make a specific change.
Feel free to view the entire set of unit tests. I think this will help you understand this design pattern. Each test is targeted at a specific use case; consider it as a fine-grained focus. Unit testing helps you independently consider each coding problem and solve it. This separation of functional concerns in MVC is reflected in every unit test.
Looking forward
Of course, my readers, you can further improve this demo. These are just some ideas that you can showcase the power of this design pattern.
I hope you can see where MVC design patterns and a little discipline can take you. A good design pattern will facilitate writing clean code without getting in the way. It will keep you focused on the task when solving the problem at hand. It will make you a better and more efficient programmer.
In programming, the purpose is to focus closely on the issues at hand while eliminating redundancy. The art of programming is to solve one problem at a time. In MVC, this means solving a functional problem one at a time.
As a developer, it's easy to believe that you are logical and don't deal with emotions. The truth is, you get frustrated when you have too many problems at once. This is the normal human reaction we all have to deal with. In fact, frustration can negatively affect code quality. When this feeling captures you and leads your work, it’s no longer about logic. This can be frustrating as solutions take more risks and complex dependencies.
What I like is focusing on a single focus. Solve one problem at a time and get positive feedback. This way, you can stay focused, efficient and avoid meaningless things.
This article was peer-reviewed by Vildan Softic. Thanks to all SitePoint peer reviewers for getting SitePoint content to its best!
Model-View-Controller (MVC) design pattern in JavaScript is crucial because it helps organize code in a concise and systematic way. It divides the application's focus into three interrelated components. The model processes data and business logic, the view manages the display of data, and the controller processes user input. This separation allows for efficient code management, easier debugging and improved scalability.
MVC mode enhances the readability and maintainability of the code by isolating responsibilities. Each component of the MVC pattern has a different function. This separation means that developers can handle individual components without affecting others. It also makes it easier to find and fix errors, update features, or refactor code, because changes in one component do not affect others.
The model in the MVC pattern is responsible for managing data and business logic. It retrieves data from the database, operates on data and updates data. The model is independent of the user interface and does not interact directly with the view or controller. Instead, it sends notifications to them when its state changes.
Views in MVC mode are responsible for displaying data to users. It receives data from the model and presents the data in a user-friendly format. The view does not interact directly with the model. Instead, it receives updates from the controller.
The controller in MVC mode acts as a mediator between the model and the view. It processes user input and updates the model and view accordingly. When the user interacts with the view, the controller interprets the input and makes necessary changes to the model. It also updates the view to reflect these changes.
MVC mode enhances scalability by separating focus points. This separation allows developers to modify or extend one component without affecting others. For example, if you need to change how your data is displayed, you can modify the view without touching the model or the controller. This modularity makes it easier to scale and grow your application over time.
Yes, the MVC pattern can be used with various JavaScript frameworks such as AngularJS, Ember.js, and Backbone.js. These frameworks provide a structured approach to implementing the MVC pattern, making it easier to build complex applications.
Implementing the MVC pattern in JavaScript can be challenging due to the dynamic nature of JavaScript. It requires a good understanding of the language and requires careful planning to ensure that the model, view and controller are properly separated and interact correctly. Additionally, managing updates between these components can be complicated.
MVC mode supports team development by allowing different developers to handle different components simultaneously. For example, one developer can handle the model, while another developer can handle the view. This separation of concerns not only increases productivity, but also reduces the possibility of conflicts or errors due to code overlap.
Yes, MVC mode can be used to develop mobile applications. It provides a structured approach to application development that makes managing complex mobile applications easier. Many popular mobile development frameworks, such as React Native and Ionic, support the MVC mode.
The above is the detailed content of The MVC Design Pattern in Vanilla JavaScript. For more information, please follow other related articles on the PHP Chinese website!