📜 ⬆️ ⬇️

MaskJS - HMV * framework


Developing MaskJS for more than half a year, we managed to turn the DOM template into a very powerful, but at the same time productive web framework. The article will acquaint you with perhaps interesting approaches to development. I'm sure it will be interesting to read about using signals and slots instead of DOM events. And how components make our lives easier. The mask can be easily integrated into an already finished project, and can even be used with any other framework. The main difference is probably the render flow , where in the process a Document Fragment / controllers / "binders" are created in stages. Actually all the flexibility is even difficult to convey, but I will try, and invite under the cat.


MaskJS @ GitHub
A small todo example for warming up. mask-fiddle works best on webkit.

View


')

Markup


In case someone does not know, the templates use syntax similar to css / less / sass. Recently, various bugs have been fixed, so the work of the engine should now be stable.

 p { div#info.dark > 'Single Child' button data-user='123' style='cursor:pointer;' > span > 'Submit' input type=hidden value=x; } 

As you can see, we removed the "<>" from the tags and removed the closing tags, but instead blocks are allocated with the usual "{}" brackets. (For simplicity, a block with one child does not look like a selector with a ">" transition, but without children at all - closed with a semicolon). The text is placed in literals, as in javascript. This is not a tricky transformation, we easily concentrated the markup on the structure - which is actually more in demand in the application architecture than html redundancy, which is aimed at text markup. This is surprising to many html supporters, but let's face it - we, and probably as well as you, are developing applications with multiple localization. We don’t have text in the views - only the keys to json with localization, then why is the hypertext markup syntax being asked?
And unlike templates based on indents, the mask is easily minified and takes up minimal space.

Speed


This is the main priority in the development of MaskJS - to offer maximum performance on mobile devices. It was possible to achieve a speed more or less comparable to html, and in the case of the webkit engine - even to increase, this especially applies to mobile platforms. And not because html parsing in webkit is slow, but because str.charCodeAt and document.createElement are very fast). And most importantly, the overhead of controller / interpolation / dom events / data bindings in the MaskJS architecture is minimal. As a result, we no longer need to compile the templates, and this is already a big plus to the enjoyment of the development. If interested, several links to jsperf.com can be found in the readme on the github .

Flexibility


MaskJS is a fairly extensible system. We can define controllers for any tags and create new ones; in fact, the entire hierarchy of controllers ( H MVC) is built on this feature. We can define handlers for any attributes. We can also define utilities that, when interpolating a model in a template, will transform or redefine data. And let me remind you that the mask on the client is rendered directly in the DocumentFragment, so we always work with DOM elements.
All controllers are created through the similarity of IoC containers, and if you are “in the subject line”, then you yourself understand how easy it will be to redefine or imitate them (“mock”) .
Do you have a jQuery widget (or equivalent) and are you tired of initializing it every time after inserting it into a house? and initialized widgets . With MaskJS, you create a Tag wrapper on your widget, and the mask will do everything for you:
 mask.registerHandler(':timer', Compo({ //     template: '.cotainer > .someInnerPanel', slots: { domInsert: function(){ //        " ", //  ,    -  } } onRenderEnd: function(){ this.$.mySuperTimer({ timespan: this.attr.timespan << 0 }); /**   -    ,         / ,       ,     .            (int) ,  undefined  . */ } }); // =  // ... :timer timespan="5000"; 


Now, you can use this tag in the template as much as you like, but you no longer need to initialize it. A small example is the creation of timers:

 $.getJSON(url).done(function(collection){ jmask("ul > % each=timers > li > :timer timespan='~[timespan]'; ", collection).appendTo(document.body); }); 


Design Patterns



There are many different architectural solutions, but everyone has a common goal - to reduce connections and dependencies. In MaskJS, the main focus is on V ( View ) from MVC, and we are trying to abstract from the Model. The mask doesn't care what your Business Layer looks like and where it comes from. And this means that all classes, data and any business logic are independent of the view and controllers - and not only architecturally, but also from the MaskJS library as a whole. A model can be either a Data Centric (note - json service response) or a complex Domain Model. But in any case, it is separate and thus easy to develop and test.
Next I will give small examples of different MVC scenarios, something will be exaggerated - so do not judge strictly, I do it only for the sake of better clarity.

But the main thing in all these MV * - not their names, especially here everything is far-fetched (I hope no one offended?). And the essence itself, how we create controllers for different purposes. And as you can see, we point the dependencies directly from the view — by unloading the controllers themselves, and leave them to deal only with their immediate tasks.

Component / (Controller) / (Widget)


AST

MaskDOM

Parser transforms the View into a node tree. A “builder” is then traversed along it and interpolates the model - creates HTMLElements and Controllers (arbitrary tags). The standard assembly MaskJS includes another good library - jmask @ github . It helps to work with the maskDOM tree, it uses the jQuery syntax and it is convenient to use it wherever you need to dynamically create a maskdom tree or modify it, for example, in onRenderStart components:
 //.. onRenderStart: function() { jmask(this).tag('div').addClass('pixel').wrappAll('.container data-id=dialog'); // eq. == jmask(this).wrappAll('.pixel > .container data-id=dialog'); } 

If somewhere you use jQuery to create a DOM, then the mask will cope with this in the same way, and much more quickly , a small example
 $('<div><span></span></div>').addClass('container').data('foo','bar').children('span').text('2013').appendTo('body') //    jmask jmask('div > span').addClass('container').data('foo','bar').children('span').text('2013').appendTo(document.body) jmask('.container foo=bar > span > "~[text]"').appendTo(document.body, { year: 2013 }) 


Controllers Tree

The builder also creates a tree from components, so you can find other controllers through selectors.
 mask.registerHandler(':page', Compo({ // ... foo: function(){ // ... //  find     this.find(':scroller').scroller.refresh(); this.closest(':item[data-id=5]]').bar(); //  jmask     jmask(this).find(':listItem').each(function(x) { x.bar() }) } }); 



Signals / Slots


mask-compo @ github

A component can have a hash object with a list of all the events it wants to process -
 var _myCompo = Compo({ constructor: function(){ this.name = 'C'; }, events: { 'touchstart: .pane': function(event){ this instanceof _myCompo // -> true }, //... } }); 

But in this way, we bind to the markup (css classes) - which means binding to the implementation of the view, which makes it difficult for us to replace the View. And this is not good. In many frameworks, you can call controller methods directly from the view, but this is also not the case, although MaskJS supports expressions in templates - div > '~[: controllerMethod("test") ]' (I note that the mask has its expression parser and evaluator implemented without with / new Function / eval ). It is much better when the view sends signals up the tree of controllers, starting with the "owner" of the element - and there already, who wants, implements the logic.
 mask.registerHandler(':myCompo', Compo({ constructor: function(){ this.name = 'C'; }, slots : { greet: function(sender){ // sender   dom    event object,     alert(this.name); // "C" // return false -  ,          . }, //... } })); mask.render(" :myCompo > .panel x-signal='click: greet'; "); 

Notice the declarative declaration of slots in the slots object — by this we clearly share the logic of the controller, and the mask itself will call these handlers when the corresponding signals are triggered.
Additionally, we can deactivate a slot or a signal at any time, and at the same time, all elements that send this signal in a given “scope” of controllers will receive the status : disabled .

Pipes

Normal signals walk only up or down the component tree, and in order to connect two components that lie outside the hierarchy, you need to use tubes.
 mask.registerHandler(':userInfo', Compo({ pipes: { // pipe name user: { logout: function(){ this.model.authenticated = false; } } } })); // = template.mask = menu { #logout x-pipe-signal='click: user.logout' > button > 'Sign out' // .... } section #content { //   "user"       % use="user" > :userInfo type='brief' ; // .. } section #footer { // bind to "user.authenticated" prop %% if="user.authenticated" > % each="user.keys" > "~[.]" } 

In this example, I tried to complicate the presentation a little.

All signals can also be sent from the controllers themselves:
  this.emitIn('name', args...); //  this.emitOut('name', args...); //  Compo.pipe('user').emit('logout'); //       "". 


Bindings


mask-binding @ github
How is a web framework without “bindings”? Here everything is in the best traditions of the genre: One- / Two-way Bindings, Custom Binding Providers, Array Mutators, Validators.
An example can be viewed at mask-try | bindings . Bindings are by their nature very productive, since in render time they only save the links to the house elements, and are attached to the model via defineProperty/ __defineSetter__ . And yes - you rightly noticed, old browsers are not supported - but by redefining the standard provider, you can bind to functions like setX/getX , or other templates like .get("x")/.set("x") . Actually, if necessary, you can bypass restrictions.
Interesting moments:


Development

It is important not only to write large and productive applications, but also to get maximum pleasure from it. Dividing the development into components, ranging from the smallest ( :customCheckBox ) to the largest ( :inbox ), we always concentrate on what is necessary. To catch bugs, there are debugger and log attributes in the system controller:
 .user { % debugger; .user-status > '~[bind:info.status]' %% log="info.status"; } 


Hot Reload Plugin

... for IncludeJS
A “hot” update of resources without page reload will not surprise anyone now - and the implementation is rather trivial, but not everything is so simple with scripts. This should include closures, dom events, and so on. We use the IncludeJS library to load all modules, and each script file can export the reload method. Also, IncludeJS.Builder includes a server that monitors changes to the requested files and notifies IncludeJS via IncludeJS . Actually the script is quite simple. But MaskJS, in turn, in the reload plugin redefines mask.registerHandler - in it it records all the components that are registered and relates them to the path of the current script. The plugin also subscribes to the controller creation event, and saves the current model and a link to the controller. Thus, when through socket.io we receive a notification about a file change, we have a list of the names of the components that this file creates, as well as a list of instances (instances) . And then the matter of technology is to call remove/dispose each controller, and initialize the updated components into their places. Using signals and slots, parents do not need to subscribe to the dom events of the updated components - the signal will come. If the component loads mask markup separately and we have changed something in it, then IncludeJS will regard this as a change in the customCompo.js file itself. The topic IncludeJS is quite capacious and about all its possibilities some other time. But the fact that the MaskJS architecture allows you to replace components on the fly greatly simplifies development, especially if the component is hidden behind N clicks (note somewhere in the dialog).

Node.js and TODO


Currently, MaskJS also works in node.js. The principles of operation are the same, only after creating mask dom, the html dom is created, which in turn turns into a string buffer . And on the client, all components will be initialized and to complete, they will receive the necessary DOM elements in the onRenderEnd method.
Routing is tied not to controllers, as in many frameworks, but to views. Remember? The view itself initializes the necessary controllers. Here you can use Master Pages technology and so on.
Work in this area is not finished yet, it will be necessary to deal with some nuances. But the goal is that the components / controllers / widgets work as on the client (first of all), but also with the ability to render on the backend with termination on the client.
I also want to create a wrapper from a component over some kind of css framework. The mask will simplify the markup at times - hide the css classes and div wrappers ). And simplify the creation of menus / calendars / dialogs, etc.

FIN

This is probably all the highlights. Much has not yet been told, but I hope it was clear at least what I was talking about, because the narrator is not from me, but there is a lot of material, so it is difficult to concentrate.
In the comments, please do not write - “look at the X-framework!” - we follow the mainstream, but I will be very happy for deeper comments.


I would also like to thank rma4ok habrauyazar for many good advice. Tried to take into account much. Also I will be glad to any other advice or suggestions. If you know interesting techniques from other languages ​​/ frameworks - please share your knowledge). And you can also join the development.

Good luck.

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


All Articles