📜 ⬆️ ⬇️

Simple minimalist implementation of complex JavaScript applications

I want to describe a simple minimalist approach to developing complex JavaScript applications. Of the external libraries, only jQuery and my js template will be used, and from jQuery only $.ready() , $.ajax() and $.proxy() - i.e. the essence is not in the libraries (they are trivial to replace them with those you prefer), but in the approach itself.

The approach is based on two ideas:
  1. JavaScript widgets are small modules, each of which “owns” a certain part of a web page (i.e., all control of this part of the page occurs exclusively through the methods of this module, and not through direct modification of DOM — encapsulation ). The widget is solely responsible for the functionality, but not for the appearance; therefore, direct modification of the part of the DOM that the widget “owns” outside the widget is allowed - but only for purely design tasks (for the architecture and the total complexity of the application, there is no fundamental difference between the appearance correction via CSS or jQuery).
  2. Global Event Manager. The interaction between the widgets is done by sending messages to the global dispatcher ( weak connectivity , Mediator pattern), and he already decides what to do with this message - create / delete widgets, pull methods of other widgets, execute designer code, etc. In contrast to the dynamic approach to handling events (when handlers of a specific event are added / removed during work), the static dispatcher greatly simplifies understanding and debugging code. Of course, there are tasks for which dynamic event handlers are needed, but in most cases this is an excessive complication, so all that is possible is done by static handlers.


About bicycles


I usually develop the server part of the application (and more even not so much the website itself as network services), the client part is not my main profile. So when I first had the task of developing a fairly complex one-page web application (about two years ago), I looked for ready-made architectural approaches for such applications, but, unfortunately, I did not find it (although by that time half a year had passed since the report Nicholas Zakas "Scalable JavaScript Application Architecture": video , slides ).

The architecture I developed at that time, I ran these two years on different projects, checked how it works on real tasks. It works well. :) That's why I decided to write this article. In the process of writing, I looked again for other solutions, and found the report by Nicholas Zakas, plus I really liked the article Addy Osmani Patterns For Large-Scale JavaScript Application Architecture from the reading on this topic.
')
For those who are already familiar with the architecture described there, I will briefly describe the differences between my approach:

But, by and large, I then developed a minimalist version of the same architecture.

Start


Create a skeleton page of our application.

index.html
 <!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <title>    JavaScript </title> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script> <script type="text/javascript" src="http://powerman.name/download/js/POWER.js"></script> <!--     --> </head> <body> <script type="text/javascript"> /*    */ </script> <!--   html-   js- --> </body> </html> 


Add a global event dispatcher. Events are the textual name of the event and an optional hash with parameters.

Usually, after the page loads, we need to perform some actions (at least create and add widgets to the page), and for consistency we will also process this event in the main dispatcher.

index.html
 /*    */ notice = (function(){ var w = { }; return function(e,data){ switch (e) { case 'document ready': break; default: alert('notice: unknown event "'+e+'"'); } }; })(); $(document).ready(function(){ notice('document ready') }); 


The hash w will be used to store the created widget objects - you need to store them somewhere after creation - plus that the dispatcher (who will create these objects) always has access to them.

And what are we doing?


The universal part is ready, it's time to decide what our application will do. To demonstrate the complex dynamic interaction between different parts of the page, we will need several active elements, whose actions will be reflected in different parts of the page.

Let's make an application where you can add / delete phrases, and it will show the number of any results for these phrases (for example, the number of errors found by the spelling checker). Plus the total number of results for all phrases.

Architecture


Separate the necessary functionality between widgets. The smaller and simpler the widgets are, the easier the application as a whole will be, so do not be surprised at their small size - this is not a feature of this application, but a recommended approach to the architecture of any complex applications.

Widgets


Although the widget owns its area, it does not control exactly where this area is located on the page - i.e. he does not know whether it is visible on the screen, and indeed whether it is added to the DOM or not. For adding / deleting areas controlled by widgets to a page, an external code is responsible (that is, our global event dispatcher, or an external widget in the case when some widgets are nested in others).

The absolute minimum of external functions is available to it inside the widget class:

With rare exceptions, the widget should somehow be displayed on the screen. The region of the page that he “owns” usually either already exists (and then the widget is transferred to the widget's constructor #id ), or is generated on the fly by the widget from the js-template (and then the #id passed to the constructor).

Everything else is not important. My style:

A small terminological clarification. Often a JavaScript widget is called a bundle of HTML + CSS + JavaScript. My widgets can also be distributed along with their HTML and CSS chunks to facilitate their connection “by default” to other applications, but I prefer to write widgets so that they have an absolute minimum of design information — this simplifies both code and design changes. Therefore, in the widgets described below you will not find a word about design.

Addphrase

The requirements of this widget for HTML - you need a form with one input type = text. Anything else the designer can make out as you like.

w.addphrase.js
 /* * <form id="addphrase"><input type=text></form> * * w = new W.AddPhrase('addphrase'); * * -> 'add phrase', phrase * */ var W; W = W || {}; W.AddPhrase = function(id){ this.$ = $( '#'+id ); this._input = this.$.find('input'); // cache this.$.submit( $.proxy(this,'_onsubmit') ); }; W.AddPhrase.prototype._onsubmit = function(){ notice('add phrase', { phrase: this._input.val() }); this._input.val(''); return false; }; 


Add the creation of the widget when the page loads. And a little code for usability - this code should be right here, and not in the widget, because the widget does not know that in this application it is he who is the main element of the interface and work begins with it.

index.html
  <!--     --> <script type="text/javascript" src="w.addphrase.js"></script> ... <!--   html-   js- --> <form id="addphrase">  : <input type=text> </form> 


index.html
 /*    */ notice = (function(){ var w = { addphrase: null }; return function(e,data){ switch (e) { case 'document ready': w.addphrase = new W.AddPhrase('addphrase'); $('#addphrase input').focus(); break; case 'add phrase': break; 


Actually, that's all. :) We have a universal widget that can be “hung up” on any form, and which regularly generates events as the user enters phrases (while we ignore them in the manager, because there is still no widget that should process them).

At the same time, there is not a single “drop of fat” in the widget - a line that does not relate to its immediate functionality. In what style this functionality would not be realized, all these lines will be anyway. In the dispatcher, of course, there are “extra” lines - if we compare it with a non-modular application in the style of N-year-old, when all the code was written in one piece. But in fact, these “extra” lines are the most valuable in our application, since It is they who visually and simply describe the high-level logic of the application.

Phrase

Using this widget as an example, we will look at working with js templates and nested widgets.

The requirements of this widget for HTML - you need a js-template, in which the variable phrase will be passed, and in which there must be an element with the class handlers - widgets with the results will be added to it.

w.phrase.js
 /* * <script type="text/x-tmpl" id="tmpl_phrase"> * <div> * <%= phrase %> * <span class="handlers"></span> * </div> * </script> * * w = new W.Phrase('tmpl_phrase', 'some phrase'); * w.add_handler( new W.SomeHandler() ); * * -> 'select phrase', phrase * */ var W; W = W || {}; W.Phrase = function(tmpl, phrase){ this.$ = $( POWER.render(tmpl, { 'phrase': phrase }) ); this._phrase= phrase; this._w = []; this._h = this.$.find('.handlers'); // cache this.$.click( $.proxy(this,'_onclick') ); }; W.Phrase.prototype.add_handler = function(w_handler){ this._w.push( w_handler ); this._h.append( w_handler.$ ); }; W.Phrase.prototype._onclick = function(){ notice('select phrase', { phrase: this._phrase }); }; 


I think everything is clear with templates. But working with nested widgets is better explained.

Although the spelling widget is “nested” visually into the Phrase widget and the Phrase widget “owns” the spelling widget (the only link to the spelling widget is in this._w , while all global widgets are held by the links in the hash w ), however As you can see, Phrase does not create a spelling widget object, but receives it as a parameter. This dependency injection for increasing flexibility avoids dependencies between specific widgets and simplifies the creation of widget objects: the fact is that usually only the dispatcher has all the necessary data to call the designer of the embedded widget, and this approach avoids the transparent transmission of these parameters for constructors of nested widgets through constructors of external widgets.

index.html
  <!--     --> ... <script type="text/javascript" src="w.phrase.js"></script> ... <!--   html-   js- --> ... <div id="phrases"> <p> <script type="text/x-tmpl" id="tmpl_phrase"> <div> <%= phrase %> <span class="handlers"></span> </div> </script> </div> 


index.html
 /*    */ notice = (function(){ var w = { ... phrase: {} }; return function(e,data){ ... case 'add phrase': if(data.phrase in w.phrase) break; w.phrase[data.phrase] = new W.Phrase('tmpl_phrase', data.phrase); $('#phrases').append( w.phrase[data.phrase].$ ); break; case 'select phrase': w.phrase[data.phrase].$.remove(); delete w.phrase[data.phrase]; $('#addphrase input').focus(); break; 


A couple of explanations: firstly, we don’t have a W.SpellCheck widget, so we’re not calling w.phrase[phrase].add_handler( new W.SpellCheck() ) ; Secondly, after clicking on the widget, Phrase (to delete a phrase) loses the focus of the phrase input line, and it must be returned.

Spellcheck

Using the example of this widget, we will look at working with ajax.

In addition, we will need to show the status of the ajax request (in the process, a response is received). There are two different approaches: you can change the DOM in the manager (by handling the 'spellcheck: started' and 'spellcheck: success' events), and you can flash all the states inside the js-template and repeatedly execute / replace the template. The second method is a bit more complicated, but because we have an example, it is worth implementing it.

To implement it, you have to use one trick. The fact is that if we save the completed template to the property .$ (as in the Phrase widget), then we will no longer be able to replace it - the fact is that the value of .$ Will be added somewhere in the DOM (we are inside the widget we don’t know where), and if we just replace the value in .$ new template, then the DOM will have a link to the old value .$ and visually nothing will change on the page (and this widget will completely lose access to "its" part of the page). To avoid this, any empty tag container (eg span or div) is saved in .$ , And the result of the template execution is placed in it already. Unfortunately, at the same time, a “unexpected” tag appears for the designer on the page, but I did not think of how to solve this problem more elegantly.

The requirements of this widget for HTML - you need a js-template, to which the status variables (possible values ​​of "started" and "success" ) and spellerrors (if the value of status=="success" ) are spellerrors .

w.spellcheck.js
 /* * <script type="text/x-tmpl" id="tmpl_spellcheck"> * <% if (status == 'started') { %> * … * <% } else { %> *  <%= spellerrors %> . * <% } %> * </script> * * w = new W.SpellCheck('tmpl_spellcheck', 'some phrase'); * * -> 'spellcheck: started', phrase * -> 'spellcheck: success', phrase, spellerrors * */ var W; W = W || {}; W.SpellCheck = function(tmpl, phrase){ this.$ = $('<span>'); this._tmpl = tmpl; this._phrase= phrase; this._load(); }; W.SpellCheck.prototype._load = function(){ $.ajax({ url: 'http://speller.yandex.net/services/spellservice.json/checkText', data: { text: this._phrase }, dataType: 'jsonp', success: $.proxy(this,'_load_success') }); this.$.html( POWER.render(this._tmpl, { status: 'started' }) ); notice('spellcheck: started', { phrase: this._phrase }); } W.SpellCheck.prototype._load_success = function(data){ this.$.html( POWER.render(this._tmpl, { status: 'success', spellerrors: data.length }) ); notice('spellcheck: success', { phrase: this._phrase, spellerrors: data.length }); }; 


index.html
  <!--     --> ... <script type="text/javascript" src="w.spellcheck.js"></script> ... <!--   html-   js- --> ... <div id="phrases"> ... <script type="text/x-tmpl" id="tmpl_spellcheck"> <% if (status == 'started') { %> (…) <% } else { %> ( <%= spellerrors %> ) <% } %> </script> </div> 


index.html
  case 'add phrase': ... w.phrase[data.phrase].add_handler(new W.SpellCheck('tmpl_spellcheck', data.phrase)); ... break; case 'spellcheck: started': break; case 'spellcheck: success': break; 


Sum


w.sum.js
 /* * <span id="sum"></span> * * w = new W.Sum('sum'); * w.add(5); * w.sub(3); * */ var W; W = W || {}; W.Sum = function(id){ this.$ = $( '#'+id ); this.$.html(0); }; W.Sum.prototype.add = function(n){ this.$.html( parseInt(this.$.html()) + n ); }; W.Sum.prototype.sub = function(n){ this.$.html( parseInt(this.$.html()) - n ); }; 


index.html
  <!--     --> ... <script type="text/javascript" src="w.sum.js"></script> ... <!--   html-   js- --> ... <p><b>: <span id="sum"></span> .</b></p> 


index.html
 /*    */ notice = (function(){ var w = { ... sum: null }; var spellerrors = {}; return function(e,data){ ... case 'document ready': ... w.sum = new W.Sum('sum'); break; case 'select phrase': ... if(data.phrase in spellerrors){ w.sum.sub(spellerrors[data.phrase]); delete spellerrors[data.phrase]; } break; case 'spellcheck: success': w.sum.add(data.spellerrors); spellerrors[data.phrase] = data.spellerrors; break; 


Total


In total for all files: 200 lines of HTML + JavaScript, 5.5KB. Of these, almost 50 lines are documentation / comments.

The issues of error handling, testing, logging and debugging are still behind the scenes. Here I can not add anything new, everything is standard (for example, see the report Nicholas Zakas "Enterprise JavaScript Error Handling": slides ).

Additional Information


The sources of the application described in the article are laid out on bitbucket , with step-by-step commit corresponding to the article. You can also look at the application itself .

Those who want to criticize my approach to the implementation of the modules can first get acquainted with my opinion on other approaches . Plus, there was once a little discussion about my js template engine .

Other articles on this topic: Scalable JavaScript applications .

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


All Articles