⬆️ ⬇️

Creating a client MVC application using RequireJS

As a web developer, you probably often wrote JavaScript code in one file, and when the amount of code gets bigger and bigger, it is difficult to maintain. To solve this problem, you can divide your code into several files, add additional script tags and use global variables to access functions declared in other files. But it pollutes the global namespace and for each file, an additional HTTP request reduces bandwidth, which increases page load time.



If this is familiar to you, you probably realized the need to reorganize your frontend code, especially if you are creating a large-scale web application with thousands of lines of JavaScript code. We need to reorganize all this confusion in order to make the code easier to maintain. A new method is to use script loaders. Many implementations can be found on the Internet, but we will take one of the best ones, called RequireJS.



In this step-by-step instruction, you will learn how to build a simple MVC (Model - View - Controller) application using RequireJS. You will not need any prior knowledge in loading scripts, we will look at the basics in this article.



Introduction



What is RequireJS and how cool it is


RequireJS is an implementation of AMD (Asynchronous Module Definition), an API for declaring modules and their asynchronous loading on the fly when they are needed. This is the development of James Burke (James Burke) and it reached version 1.0 after two years of development. RequireJS helps you organize your code using modules and will manage asynchronous and parallel loading of your files for you. Since the scripts are loaded only when necessary and in parallel, this reduces the page load time, which is very cool!

')

MVC on the frontend?


MVC is a well-known pattern for organizing code on the server side, making it modular and supported. What can be said about its use on the front end? Can we apply this pattern on javascript? If you only use JavaScript to animate, test several forms or a few simple methods that do not require many lines of code (say, less than 100 lines), there is no need to structure files using MVC, and probably there is no need to use RequireJS. However, if you are creating a large web application with many different views, definitely, yes!



Create an application


To show how to organize MVC code using RequireJS, we will create a very simple application with 2 views:





Here is what it will look like:







The business logic will be very simple, so you can focus on understanding what really matters: code structuring. And since it is so simple, I highly recommend that you try to run this example in parallel with reading the article. It won't take long, and if you have never resorted to modular programming or using RequireJS before, this example will help you become a more professional programmer. Seriously, it's worth it.



HTML and CSS


This is the text of the HTML file that we will use in the example:



<!doctype html> <html> <head> <meta charset="utf-8"> <title>A simple MVC structure</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <div id="container"> <h1>My users</h1> <nav><a href="#list">List</a> - <a href="#add">Add</a></nav> <div id="app"></div> </div> <script data-main="js/main" src="js/require.js"></script> </body> </html> 




Navigation in our application will be the nav menu links, which will be present on each page of the application, and all the magic of the MVC application will occur in the element <div id = "app"> </ div> . We also included RequireJS (which you can download here ) at the bottom of the body . You may notice a special attribute on the script tag: data-main = "js / main" . The value assigned to this attribute is used by RequireJS as the entry point of the entire application.



Let's also add some styles:



 #container{ font-family:Calibri, Helvetica, serif; color:#444; width:200px; margin:100px auto 0; padding:30px; border:1px solid #ddd; background:#f6f6f6; -webkit-border-radius:4px; -moz-border-radius:4px; border-radius:4px; } h1, nav{ text-align:center; margin:0 0 20px; } 




Remembering OOP: What is a module?


In object-oriented JavaScript programming, there is a very common pattern called Module Template. It is used to encapsulate methods and attributes in objects (which are “modules”) to avoid contamination of the global namespace. It is also used to partially imitate classes from other OOP languages, such as Java or PHP. An example of how you can define a simple MyMath module in our main.js file:



 var MyMath = (function(){ //       return { //     add:function(a, b){ return a + b; } }; })(); console.log(MyMath.add(1, 2)); 




Public methods are declared using the object in literal notation, which is not very convenient. Alternatively, you can use the Module Discovery Template, which returns private attributes and methods:



 var MyMath = (function(){ //         : function add(a, b){ return a + b; } return { add:add //        ! }; })(); console.log(MyMath.add(1, 2)); 




I will use the Module Discovery Template for the rest of the article.



RequireJS



Module Description with RequireJS


In the previous section, we defined a module in a variable for its further call. This is just one way to declare a module. We will now look at another method used by RequireJS. The goal of RequireJS is to split our JavaScript files for easier maintenance, so let's create the file MyMath.js to define our module MyMath , in the same folder as main.js :



 define(function(){ function add(a, b){ return a + b; } return { add:add }; }); 




Instead of declaring a variable, we simply put the module as a parameter to the define function. This function is defined in RequireJS and it makes our module accessible from outside.



Module loading in the main file


Let's go back to our main.js file. RequireJS provides another function called require , which we use to call our module MyMath . This is how our main.js looks like now:



 require(['MyMath'], function(MyMath){ console.log(MyMath.add(1, 2)); }); 




The call to the MyMath module is now wrapped in a require function that takes two parameters:





Now you can reload the page. Just like that you call a method from another file! Yes, it was very simple and now you are ready for the big and awesome MVC architecture (which works just like the definition of the module you just made, so be sure you can handle it perfectly!).



MVC structure



Important note : In this tutorial we will simulate the MVC structure, often used on the server side, in which one controller corresponds to the representation. In front-end development, it is common to use several views with one controller. In this case, the representations are visual components, such as buttons or input fields. MVC JavaScript frameworks such as Backbone use a different approach, which is not the purpose of this article. My goal is not to create a real MVC framework, but simply to show how RequireJS can be used in a structure that many of you are familiar with.



Let's start by creating folders and files for our project. We will use models to present data, business logic will be concentrated in the controllers, and these controllers will invoke certain views to display the pages. What do you think? We need 3 folders: Models, Controllers and Views. Given the simplicity of our application, we will have 2 controllers, 2 views and 1 model. Our javascript folder now looks like this:







So the structure is ready. Let's start with the implementation of the simplest part: this is a model.



Model: User.js



In this example, user is a simple class with one name attribute:



 define(function(){ function User(name){ this.name = name || 'Default name'; } return User; }); 




If we now return to our main.js file, we can now declare a dependency on User in the require method, and manually create a set of users for this example:



 require(['Models/User'], function(User){ var users = [new User('Barney'), new User('Cartman'), new User('Sheldon')]; for (var i = 0, len = users.length; i < len; i++){ console.log(users[i].name); } localStorage.users = JSON.stringify(users); }); 




Then we serialize an array of users in JSON and store it in the HTML5 local storage to make them available as in a database:







Note : to serialize JSON with the stringify method and deserialize with the parse method, you need any polyfill library, so that the example would work in IE7 and earlier. For this you can use the Douglas Crockford json2.js library from the Github repository .



We display the list of users


It's time to show the list of users in the interface of our application. For this we will use ListController.js and ListView.js . Obviously, these two components are related. There are many ways to do this, but to keep our example simple, I suggest: The ListView will have a render method and our ListController will simply get users from the local repository and call the render method of the ListView , passing users as a parameter. So obviously, ListController needs a dependency on ListView .



Just as in require , you can pass an array of dependencies to define if the module depends on other modules. Let's also create a start method (or with some other name that seems meaningful to you, such as run or main ) to describe the main controller behavior in it:



 define(['Views/ListView'], function(ListView){ function start(){ var users = JSON.parse(localStorage.users); ListView.render({users:users}); } return { start:start }; }); 




Here we deserialize the list of users from the local repository and pass it to the render method as an object. Now, all we need to do is implement the render method in ListView.js :



 define(function(){ function render(parameters){ var appDiv = document.getElementById('app'); var users = parameters.users; var html = '<ul>'; for (var i = 0, len = users.length; i < len; i++){ html += '<li>' + users[i].name + '</li>'; } html += '</ul>'; appDiv.innerHTML = html; } return { render:render }; }); 




This method simply loops through the users and combines their names into an HTML string that we insert into the element.



Important : Using HTML in a JavaScript file, as in this example, is not the best solution, because it is very difficult to maintain. Instead, you should look at the templates. Templates are an elegant way to insert data into HTML. There are many good template engines on the Internet. For example, you can use jQuery-tmpl or mustache.js . But this is beyond the scope of this article, and would add complexity to the current architecture, I preferred to make it as simple as possible.



Now, all we need to do is “run” our ListController module. To do this, let's declare it as a dependency in our main.js file, and call the ListController.start () method:



 require(['Models/User', 'Controllers/ListController'], function(User, ListController){ var users = [new User('Barney'), new User('Cartman'), new User('Sheldon')]; localStorage.users = JSON.stringify(users); ListController.start(); }); 




Now you can refresh the page to see this wonderful list of users:







Yes it works! Congratulations, if you followed an example in parallel and got the same result!



Note : At the moment, we can only manually declare the controller that we want to run, since we do not have any routing system yet. But we will soon create one very simple.



Add user


Now we need the ability to add users to the list. We will display a simple input field and a button, with an event handler when you click on the button in which we will add the user to the local storage. Let's start with AddController , as in the previous section. This file will be very simple, since we do not have parameters to pass them to the view. This is AddController.js :



 define(['Views/AddView'], function(AddView){ function start(){ AddView.render(); } return { start:start }; }); 




And the corresponding representation:



 define(function(){ function render(parameters){ var appDiv = document.getElementById('app'); appDiv.innerHTML = '<input id="user-name" /><button id="add">Add this user</button>'; } return { render:render }; }); 




Now you can declare AddController as a dependency in the main file and call its start method to see the expected representation:







But since we do not yet have a link to button events, this presentation is not very useful ... Let's work on it. I have a question for you: Where should we place the logic to handle this event? In the view or in the controller? If we post in a view, this will be the right place to add a subscription to an event, but placing business logic in a view will be a very bad practice. Placing the logic in the controller seems to be the best idea, even if it is not perfect, because we don’t want to see here any mention of html elements that relate to the view.



Note : the best way would be to place event handlers in the view that would call business logic methods located in the controller or in a dedicated event module. This is easy to do, but it would complicate the example, and I do not want you to get confused. Try it for practice!



As I said, let's put all the logic of the event in the controller. We need to create the bindEvents function in AddController and call it after the view has finished displaying the HTML:



 define(['Views/AddView', 'Models/User'], function(AddView, User){ function start(){ AddView.render(); bindEvents(); } function bindEvents(){ document.getElementById('add').addEventListener('click', function(){ var users = JSON.parse(localStorage.users); var userName = document.getElementById('user-name').value; users.push(new User(userName)); localStorage.users = JSON.stringify(users); require(['Controllers/ListController'], function(ListController){ ListController.start(); }); }, false); } return { start:start }; }); 




In bindEvents , we simply add an event listener for the click on the #add button (feel free to use your own function if you are working with attachEvent in IE - or just using jQuery). When the button is clicked, we get a row of users from the local repository, deserialize it to get an array, add a new user with the name contained in the input field # user-name , and put the updated array of users into the local repository. After that, we finally load the ListController to execute its start method, and we can see the result:







Wonderful! It's time for you to get some rest; you did a good job if you followed the example with me.



Navigating between views using routes



Our small application is cool, but it's really bad that we still cannot navigate between views to add new users. Not enough routing system. If you have previously worked with server MVC frameworks, you are probably familiar with such a system. Each URL leads to a presentation. However, now we are on the client side, and the situation is slightly different. Single-page JavaScript applications like this use a hash to navigate between different parts of the application. In our case, we want to display two different views when these URLs open:





This will allow you to bookmark each page of the application.



Note : Firefox, Chrome and Opera have good HTML5 history management support (PushState, popState, replaceState), which avoids working with hashes.



Browser functionality and compatibility


History management and hash navigation can be a weak point if you require good compatibility with older browsers. Depending on the browsers that you want to support, you can consider various ways to track routes:





In our case, we use simple manual monitoring, which is fairly easy to implement. All we have to do is check the hash change every n milliseconds, and run some functions in case a change is detected.



Note : A jQuery plugin is also available to control this process.



Routes and Main Routing Loop


Let's create a Router.js file next to main.js to control the routing logic. At the beginning of this file, we need to declare our routes and set the default route if it is not listed in the URL. We can, for example, use a simple array of route objects that contain hashes and their corresponding controllers that need to be loaded. We also need defaultRoute if the hash is not in the URL:



 define(function(){ var routes = [{hash:'#list', controller:'ListController'}, {hash:'#add', controller:'AddController'}]; var defaultRoute = '#list'; var currentHash = ''; function startRouting(){ window.location.hash = window.location.hash || defaultRoute; setInterval(hashCheck, 100); } return { startRouting:startRouting }; }); 




When the startRouting method is called , it sets the default value of the hash to the URL and starts the repeated call of the hashCheck method, which we have not yet implemented. The currentHash variable will be used to store the current hash value if a change was detected.



Check hash changes


This is the hashCheck function, which is called every 100 milliseconds:



 function hashCheck(){ if (window.location.hash != currentHash){ for (var i = 0, currentRoute; currentRoute = routes[i++];){ if (window.location.hash == currentRoute.hash) loadController(currentRoute.controller); } currentHash = window.location.hash; } } 




hashCheck simply checks if the hash has changed from currentHash , and if it matches one of the routes, it calls loadController with the corresponding controller name.



Loading the desired controller


Finally, loadController simply calls the require function to load the controller module and calls its start function:



 function loadController(controllerName){ require(['Controllers/' + controllerName], function(controller){ controller.start(); }); } 




So, the final version of the Router.js file looks like this:



 define(function(){ var routes = [{hash:'#list', controller:'ListController'}, {hash:'#add', controller:'AddController'}]; var defaultRoute = '#list'; var currentHash = ''; function startRouting(){ window.location.hash = window.location.hash || defaultRoute; setInterval(hashCheck, 100); } function hashCheck(){ if (window.location.hash != currentHash){ for (var i = 0, currentRoute; currentRoute = routes[i++];){ if (window.location.hash == currentRoute.hash) loadController(currentRoute.controller); } currentHash = window.location.hash; } } function loadController(controllerName){ require(['Controllers/' + controllerName], function(controller){ controller.start(); }); } return { startRouting:startRouting }; }); 




Using a new routing system


All we have to do is load this module in our main file and call the startRouting method:



 require(['Models/User', 'Router'], function(User, Router){ var users = [new User('Barney'), new User('Cartman'), new User('Sheldon')]; localStorage.users = JSON.stringify(users); Router.startRouting(); }); 




If we want to move in our application from one controller to another, we can simply replace the current window.hash with the hash of the route of another controller. In our case, we still manually load the ListController from AddController instead of using our new routing system:



 require(['Controllers/ListController'], function(ListController){ ListController.start(); }); 




Let's replace these three lines with a simple hash update:



 window.location.hash = '#list'; 




That's all! Our application now has a functional routing system! You can move from one view to another, go back, do whatever you want with the hash in the URL, and the correct controller will load if the corresponding routes are announced in the router.



Here is a link to the online demo of this application.



findings



You can be proud of yourself, you have written the MVC application without using any frameworks! We only used RequireJS to merge our files, this is the only essential tool to build a modular structure. So what are the next steps? If you like the minimalist approach in the DIY style, as in this example, you can enrich our little framework with new features as your application grows and new technical needs appear. Here are some options for future steps:







This example is great for learning, but our framework is not really ready for use in real projects. If you are too lazy to add the functionality listed above (and this list is not exhaustive!), You can start exploring one of the existing MVC frameworks. Here are some of the most popular:







Original article in English .

Source: https://habr.com/ru/post/209228/



All Articles