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:
- 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).
- 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:
- In my opinion, that architecture is necessary for applications of the scale of Yahoo !, GMail, etc. but for the vast majority of modern, rather complex web applications, this is still a bust. The excessive flexibility of this architecture comes at a price — it increases the complexity of the application. Here is an example of features in which I never had a real need:
- the ability to simply replace the base library (for example jQuery with Dojo)
- check and control of “access rights” of modules to the system functionality
- dynamic add / remove modules on the page
- I already wrote about dynamic event handlers above
- This is not explicitly mentioned anywhere, but as far as I understood, in their architecture “modules” are practically whole large and complex applications (like chat on the GMail page or a weather widget on Yahoo). My “modules” implement the minimum possible functionality that can be isolated into a separate isolated entity. (However, Andrew Burgess “Writing Modular JavaScript” screen shows the same small modules as mine.) As an example, you can take a module that is responsible for a form with one input or a module that is responsible for outputting one value inside a single tag. At the same time, these modules implement exactly the business logic of the application, and are not analogous to jQuery plug-ins that make the usual <select> very cool, beautiful and feature-rich <select>.
- As a consequence of the difference in the scale of the functionality of the modules, their modules consist of HTML + CSS + JavaScript, and for me most of the modules are only JavaScript plus documentation on the minimum required for this module HTML structure.
- Although I absolutely agree that it is necessary to clearly restrict what external functionality modules have access to (in fact, I wrote about this in the first paragraph of the article, it remains only to add a dispatcher to that list), but I believe that the most reliable, efficient and the safe code is the one that is not. Therefore, if it is possible to replace the code with, for example, agreements, I usually prefer agreements (although, of course, here I also need to know the measure). In my approach there is no code that would restrict the modules in access, but there is an agreement where modules can climb, and where not.
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> </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.
- AddPhrase - add phrase widget.
- Methods: no.
- Generates events:
'add phrase'
.
- Phrase - a widget that displays the added phrase, allowing it to be selected (for deletion) and add different handlers for this phrase (such as spelling check).
- Methods:
add_handler
. - Generates events:
'select phrase'
.
- SpellCheck - a widget that displays the number of errors in this phrase. Also responsible for the ajax service request.
- Methods: no.
- Generates the events
'spellcheck: started'
and 'spellcheck: success'
.
- Sum - a widget that displays the total number of results for all phrases.
- Methods:
add
and sub
. - Generates events: no.
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:
- global function
notice()
for generating events; - jQuery
$
object; - functions of the template library.
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:
- Widgets are in separate w.widget_name.js files.
- Widgets are implemented as classes in the global namespace
W
, usually one widget is one class: W.WidgetName
. - Classes are implemented in the simplest and most natural for JavaScript form - the usual constructor functions and prototype inheritance.
- Private properties and methods begin with an underscore.
- At the beginning of the widget it is documented what kind of js-template (or HTML block) it needs; constructor, public methods / properties, generated events.
- By convention, the area controlled by the widget is available through the property of the widget object
.$
(Its external code is inserted into the DOM to display the widget on the page).
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 var W; W = W || {}; W.AddPhrase = function(id){ this.$ = $( '#'+id ); this._input = this.$.find('input');
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> ... <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 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');
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> ... ... <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 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> ... ... <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 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> ... ... <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 .