📜 ⬆️ ⬇️

JS Loader + template engine or history of another

I consider the invention of my “unique” bicycle to be very useful if: it does not distract from work (or distracts, but not strongly); gives some new positive experience; The results can be used somewhere; The process itself is high. I was repelled by this, starting to construct my “great” about 3 years ago and, probably, 3-4 times rewriting it by today.

It all started with bootloader and jq



RequireJS is certainly a nice and very effective utility that allows you to organize a modular system on the client side very quickly and naturally. And she suited me all, except for two things:

By “appearance” I mean that links to modules are placed in the argument string of the parent module.

requirejs(["module_0", "module_1"], function(module_0, module_1) { }); 

')
And when the modules became more and more - it turned into a disgrace.

On the one hand, you can call the necessary module somewhere inside the code (if it is used very, very rarely), on the other - I would like to see all the dependencies of the parent module in one place.

In addition, I was a little annoyed by the need to operate paths when declaring dependencies, rather than variables containing the necessary data. No, you can, of course, somewhere, declare a global object with paths and bring everything to something like this (but it's still somehow ugly):

 requirejs([modules.mod_0, modules.mod_1], function(module_0, module_1) { }); 


As for managing the cache, it seemed to me, let's say, not explicit.

Over time, my requirements for my loader began to emerge, and today they are formulated as follows:
  1. All JS and CSS files should be cached, and the caching system should have clear and understandable control.
  2. modules declared throughout the application must be described in one specific place (single register).
  3. The declaration of module dependencies on each other should occur without the use of paths, and with the use of references to a single register (clause 2) or module names.
  4. there should be control over the order of loading modules, as well as the possibility of asynchronous paging of the necessary resources.


And that's what I did. The kernel consists of three files, the names of which completely imply their purpose:


I note that you only need to connect [flex.core.js], and the register of modules and settings will be picked up automatically.

But here is the first unpleasant news. The developer is strictly bound to the file names and their location. [flex.registry.modules.js] and [flex.settings.js] should be in the same place as the base module [flex.core.js], and their names cannot be changed.

But since this is my great and while I am the only developer, this circumstance does not really bother me. In addition, this organization suits me very much. Already there are a dozen projects written using flex, and I always know where I can find the settings and the full list of modules used.

So let's take a look at [flex.registry.modules.js] (the register of modules from a live project).

  flex.libraries = { //Basic binding controller binds : { source: 'KERNEL::flex.binds.js', hash: 'HASHPROPERTY' }, //Basic events controller events : { source: 'KERNEL::flex.events.js', autoHash: false }, //Collection of tools for management of DOM html : { source: 'KERNEL::flex.html.js' }, css : { //Controller CSS animation animation : { source: 'KERNEL::flex.css.animation.js'}, //Controller CSS events events : { source: 'KERNEL::flex.css.events.js' }, }, //Collection of UI elements ui : { //Controller of window (dialog) window : { //Controller of window movement move : { source: 'KERNEL::flex.ui.window.move.js' }, //Controller of window resize resize : { source: 'KERNEL::flex.ui.window.resize.js' }, //Controller of window resize focus : { source: 'KERNEL::flex.ui.window.focus.js' }, //Controller of window maximize / restore maximize: { source: 'KERNEL::flex.ui.window.maximize.js' }, }, //Controller of templates templates : { source: 'KERNEL::flex.ui.templates.js' }, //Controller of patterns patterns : { source: 'KERNEL::flex.ui.patterns.js' }, //Controller of scrollbox scrollbox : { source : 'KERNEL::flex.ui.scrollbox.js' }, //Controller of itemsbox itemsbox : { source: 'KERNEL::flex.ui.itemsbox.js' }, //Controller of areaswitcher areaswitcher: { source: 'KERNEL::flex.ui.areaswitcher.js' }, //Controller of areascroller areascroller: { source: 'KERNEL::flex.ui.areascroller.js' }, //Controller of arearesizer arearesizer : { source: 'KERNEL::flex.ui.arearesizer.js' }, }, presentation: { source: 'program/presentation.js' }, }; 


As you can see, this is just a list of all modules used in the application. If you noticed, then for each module we can define a couple of variables (besides the [source] path itself):


Another point that you probably noticed is grouping. The modules are not presented through the list, but are divided into groups, which makes the whole modular system as a whole more meaningful and transparent.

Well, once again just in case - this is just a register (list) of modules. The definition of a module here does not mean that it will be loaded. Loading is otherwise regulated.

Go ahead. Let's look at the settings. File [flex.settings.js] from a working site.

 flex.init({ resources: { MODULES: [ 'presentation', 'ui.patterns' ], EXTERNAL: [ { url: '/program/body.css', hash: 'HASHPROPERTY' }, ], ASYNCHRONOUS: [ { resources: [ { url: 'http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js' }, { url: '/program/highcharts/highcharts.js', after: ['http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js'] }, { url: '/program/highcharts/highcharts-more.js', after: ['/program/highcharts/highcharts.js'] }, { url: '/program/highcharts/exporting.js', after: ['/program/highcharts/highcharts.js'] }, ], storage : false, finish : function () { //Do something } } ], }, events: { onFlexLoad: function () { //Do something }, onPageLoad: function () { var presentation = flex.libraries.presentation.create(); presentation.start(); } }, settings: { CHECK_PATHS_IN_CSS: true }, logs: { SHOW: ['CRITICAL', 'LOGICAL', 'WARNING', 'NOTIFICATION', 'LOGS', 'KERNEL_LOGS'] } }); 


Here, too, everything is quite simple.

In the section with the speaker name [MODULES] the list of those modules that must be loaded before the start of the entire application is defined. Please note that we specify not the links, but the names of the modules in accordance with the register, that is, as defined in [flex.registry.modules.js] (excluding “flex.libraries”).

The array [EXTERNAL] contains a list of those resources that should be loaded at a given URL. Just like in the list of modules, here you can operate with such properties as [hash] and [authHash] to manage the cache of a single resource.

The [ASYNCHRONOUS] section is actually the same as [EXTERNAL], but: first, it starts loading right away (without waiting for the main modules to load); secondly, here we have the ability to predetermine the order of loading. In this particular example, the [highcharts.js] file will not load until the JQ library is loaded.

In addition, in the [ASYNCHRONOUS] section, we can define any number of resource groups (bundles) with our own handlers for loading completion (event [finish]).

Resources from the [ASYNCHRONOUS] section are also cached, but cache management is less flexible here. We can only turn it on or off, determining the value of the [storage: true / false] property.

Very large in the boot process is as follows:
  1. Loading flex.core.js
  2. Catching flex.registry.modules.js, flex.registry.events.js and flex.settings.js
  3. Start loading modules defined in [MODULES] and here start loading all that is in [ASYNCHRONOUS]
  4. Creating a list of dependencies (modules and resources requested by modules from [MODULES]). Download all required modules and resources.
  5. Upon completion of loading modules from [MODULES] (along with dependencies) start loading resources from [EXTERNAL]
  6. Upon completion of loading all of the [EXTERNAL] and [ASYNCHRONOUS] (if the settings indicate that you need to expect asynchronously loaded resources) call the event [onFlexLoad] and wait for the event [onPageLoad]


That's actually the whole boot process.

I intentionally made a division into three groups [MODULES], [EXTERNAL] and [ASYNCHRONOUS]. This approach allows me to clearly and clearly see that there is a part of the current application, and that there are third-party solutions used in the project.

What else is flex.registry.events.js ?



This file is part of a common system, but it comes in handy only when some of the libraries I have already written are used. Here is its content:

 flex.registry.events = { //Events of UI ui: { //Events of scrollbox scrollbox : { GROUP : 'flex.ui.scrollbox', REFRESH : 'refresh', }, //Events of itemsbox itemsbox : { GROUP : 'flex.ui.itemsbox', REFRESH : 'refresh', }, //Events of arearesizer arearesizer : { GROUP : 'flex.ui.arearesizer', REFRESH : 'refresh', }, window : { //Events of window resize module resize : { GROUP : 'flex.ui.window.resize', REFRESH : 'refresh', FINISH : 'finish', }, //Events of window maximize / restore module maximize: { GROUP : 'flex.ui.window.maximize', MAXIMIZED : 'maximized', RESTORED : 'restored', CHANGE : 'change', } } }, //Events of Flex (system events) system: { //Events of logs logs: { GROUP : 'flex.system.logs.messages', CRITICAL : 'critical', LOGICAL : 'logical', WARNING : 'warning', NOTIFICATION: 'notification', LOGS : 'log', KERNEL_LOGS : 'kernel_logs', }, cache: { GROUP : 'flex.system.cache.events', ON_NEW_MODULE : 'ON_NEW_MODULE', ON_UPDATED_MODULE : 'ON_UPDATED_MODULE', ON_NEW_RESOURCE : 'ON_NEW_RESOURCE', ON_UPDATED_RESOURCE : 'ON_UPDATED_RESOURCE', } } }; 


As you may have guessed, these are just event identifiers in the kernel and modules. Why did I put it in a separate file? To create a certain level of abstraction and to enable the modules to "communicate" with each other. In addition, having such a register in public space, the developer gets a wonderful opportunity to respond to interesting events:

 flex.events.core.listen( flex.registry.events.ui.window.resize.GROUP, flex.registry.events.ui.window.resize.REFRESH, function (node, area_id) { //Do something } ); 


Well, finally it is time to look at the template module and if you are not yawning from boredom, then here it is:

  var protofunction = function () { //Constructor of module }; protofunction.prototype = function () { //Module body var //Get modules html = flex.libraries.html.create(), events = flex.libraries.events.create(); return { //Some methods }; }; flex.modules.attach({ name : 'ui.itemsbox', protofunction : protofunction, reference : function () { flex.libraries.events(); flex.libraries.html(); }, resources : [ { url: 'KERNEL::/css/flex.ui.itemsbox.css' } ], }); 


All magic, as it is not difficult to guess, is hidden in the method [flex.modules.attach]. Let's go over it by properties.


The modules themselves can be called anywhere in the code using our register, namely via the - create function, for example: html = flex.libraries.html.create (), after which the html variable will become a reference to the functionality of the called module.

So, here is the main part of how the modular system is organized using my flex-bike. There is one place where modules are described; there is a place where the settings are specified and the application is launched; and there are modules themselves.

For very small projects (literally with a pair, with a triple of modules) such a system may seem redundant, and I think that this is the way it is. However, to solve such problems, we can not define the register and settings at all, that is, not create the files flex.registry.modules.js and flex.settings.js. In this case, the creation of modules will be very much like the way RequireJS does it:

 _append({ name : 'Base.B', require : [ { url: 'ATTACH::D_file.js' }, ], module : function () { var //Get modules. D = flex.libraries.D.create(); //Module body return { //Module methods }; }, }); 


As you can see, despite the fact that the register file flex.registry.modules.js is not used, the list of modules is still created and they are called the same as for regular modules using the create function.

The only difference is that now we are forced to specify dependencies in the form of links (paths), which for small projects is certainly not critical.

Also, without the configuration file flex.settings.js, the question of launching the application arises, because the onFlexLoad and onPageLoad events are not defined. This issue is resolved with the help of the launched module:

 _append({ name : 'A', require : [ { url: 'ATTACH::B_file.js' }, { url: 'ATTACH::C_file.js' }, { url: 'ATTACH:css/B_file.css' } ], launch : function(){ var //Get modules. B = flex.libraries.Base.B.create(), C = flex.libraries.C.create(); //Do something } }); 


That is, replacing the [module] property with the [launch] property, we will create a launch module, which, of course, there can be only one in the application.

In addition, you can also group your modules. Notice how the module name [B] - “Base.B” is defined. This means that the “Base” group will be created and our module [B] will be attached to it.

A little more about boring and the most interesting


Naturally, you may have a question: “Cycling is, of course, useful, but bro, you just repeated everything that RequireJS does. What for?".

All for two things: the first is ordering (via the settings file and a single register of modules), and the second is caching.

Flex does not rely on standard browser caching, using local storage for this purpose - localStorage. Suppose we have in the system from 20 to 30 modules (depending on the page). In the first launch of the application, everything will be connected by creating a banal <script> and <link> for JS-files and CSS-files, respectively. But at the same time in the background flex will try to “extract” the content of all resources (the contents of the files). After receiving it, flex will save it and link it to specific URLs for which they (resources) were requested. And already from the second page load there will be not 20 - 30 calls to the server for files, but only 4 (for flex.core.js, settings, module register and event register). Everything else will be taken from localStorage and integrated into the page, which most favorably affects the download speed of the entire application.

But that's not all. Caching can be controlled either manually (by setting hashes for various modules) or by entrusting this routine to flex, which, after the final page load in the background, produces a “survey” of HEADERs at the URL of all resources, trying to determine the file size and the date it was created ( changes). Based on the data received, flex initializes the update of the module (or resource) whose parameters have changed, thus keeping the entire system up to date.

I, probably, already very tired you, but now there will be the most interesting. I hope)

Templates


All that I have met with respect to templates in one way or another relies on the server part. I admit that I looked badly (with my scanty experience), but what I found was loading the template every time I opened the page again and again.

I didn't like it very much. After all, if the template is more or less permanent, wouldn't it be easier to keep it on the client side and update it only when necessary?

One more thing that I always wanted to have was the opportunity not only to see the template separately from the application, but to launch it “in isolation” from the page. For example, we have an authorization window template - I would like in a couple of clicks to open a page on which only this template would be. What for? Quickly debug styles, for example, or quickly catch a bug in the code.

According to the results of long reflections, the following requirements for the template controller were derived:


In general, let's see what happened (controller - flex.ui.patterns.js ).

The main template file is trite html file. Like this one, for example:

 <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Flex.Template</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <link rel="stylesheet" type="text/css" href="pattern.css" /> <script type="text/javascript" src="controler.js"></script> </head> <body> <div data-type="Pattern.Login"> <p>Login</p> {{login}} <p>Password</p> {{password}} <div data-type="Pattern.Controls">{{controls}}</div> </div> </body> </html> 


That is, task # 1 on opening the template as a file has already been solved (which allows the developer to very quickly debug the styles).

Please note that styles and scripts are connected in the most usual way, via link and script. Here it should also be noted that it is “more beautiful” (in my opinion) to lay out templates in separate folders.

Next, let's take an example. Create a pop-up window to authorize the user. We will need several templates (to save space, I cite only the contents of the <body> tag).

(A) Popup Template

  <div data-style="Popup" data-flex-ui-window-move-container="{{id}}" data-flex-ui-window-resize-position-parent="{{id}}" data-flex-ui-window-maximize="{{id}}"> <div data-style="Popup.Container" data-flex-ui-window-resize-container="{{id}}"> <div data-style="Popup.Title" data-flex-ui-window-move-hook="{{id}}"> <p data-style="Popup.Title">{{title}}</p> <div data-style="Popup.Title.Switcher" data-state="max" data-flex-window-maximize-hook="{{id}}"></div> </div> <div data-style="Popup.Content">{{content}}</div> <div data-style="Popup.Bottom"> <p data-style="Popup.Bottom" id="test_bottom_id">{{bottom}}</p> <div data-style="Window.Resize.Coner"></div> </div> </div> </div> 


(B) An authorization window template (it was already slightly higher)

  <div data-type="Pattern.Login"> <p> </p> {{login}} <p> </p> {{password}} <div data-type="Pattern.Controls">{{controls}}</div> </div> 


(C) Text field template

  <p>: <span>{{::value}}</span></p> <div data-type="TextInput.Wrapper"> <div data-type="TextInput.Container"> <input data-type="TextInput" type="{{type}}" value="{{::value}}" name="TestInput" /> </div> </div> 


(D) And the pattern of buttons

 <a data-type="Buttons.Flat" id="{{id}}">{{title}}</a> 


By syntax there are a couple of moments. The curly brackets {{controls}} specify hooks. Yes, borrowed from WordPress. In double-colon braces, {{:: value}} specify the data that must be linked to the DOM tree and placed in the model. See everything later.

So, the call (assembly) of the final template will look like this:

 _node(document.body).ui().patterns().append({ url : '/patterns/popup/pattern.html', //A hooks : { id : id, title : 'Login popup', content : patterns.get({ url : '/patterns/patterns/login/pattern.html',//B hooks : { login : patterns.get({ url : '/patterns/controls/textinput/pattern.html',//C hooks : { type: 'text' } }), password: patterns.get({ url : '/patterns/controls/textinput/pattern.html',//C hooks : { type: 'password' } }), controls: patterns.get({ url : '/patterns/buttons/flat/pattern.html',//D hooks : [{ title: '', id: 'login_button' }, { title: '', id: 'cancel_button' }] }), }, resources: { one: 'one', two: 'two' }, }) }, callbacks: { success: function (model, binds, map, resources) { var instance = this; } }, }); 


After executing this method, the authorization window will be “collected” and attached to the body tag. Of course, the method works asynchronously (after all, at least when you first load the page, you have to contact the server for the template files), so it returns nothing.
Pay attention to the arguments to the success callback function (from the callbacks section). Let's take a closer look - there are useful things inside.

model is a link to a model that was compiled for this particular instance of the template. If you look at the © text box template, you will see that we have linked the value of input.value and span.innerHTML (located in the first paragraph). Now, if the value of the text field changes, the inscription above it will change. In addition, you can "get into the model" and change the value of input.value through the model, namely:

Notice that the properties framed in a double underline __something__ repeat the nesting of the template (its structure).

binds its structure completely repeats the model. That is, binds will also be model .__ content __.__ login __. Value and model .__ content __.__ password __. Value. But only now you will not have access to the value of the [value] property, but two methods: addHandle (handle) and removeHandle (id). As you have already guessed, we can “pick up” a handler that will work both when the model is changed directly (by changing the model properties), or if the DOM (that is, the input value) changes.

map- these are links to parent nodes for each hook. In other words, this is the node map for this particular template. It can be used for different purposes, but personally it comes in handy for me to indicate the context when searching for nodes through selectors.

And the last is resources . This is just an auxiliary object. Look at the method that creates the template. As you can see, there is a similar property. That's it will be returned here.

Now let's go back to the first example. As I said, we are free to connect any number of JS and CSS resources to the template.

  <link rel="stylesheet" type="text/css" href="pattern.css" /> <script type="text/javascript" src="controler.js"></script> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script> 


Template initialization will be started only after all resources are loaded. Well, it goes without saying, everything is “stored” in localStorage and, if possible, comes from there instead of requests to the server.

But in this particular case, the important thing is not the ability to connect JQ to a single template, but the controller.js file. He, by the way, can be called whatever you like, the main thing is inside. And inside - the controller:

 _controller(function (model, binds, map, resources) { var instance = this, clone = null; clone = instance.clone({ id : 'clonned_pattern', title : 'Clonned dialog window', content : { login: { type: 'text', }, password: { type: 'password', }, controls: [{ title: 'login', id: 'login_button' }, { title: 'cancel', id: 'cancel_button' }], } }); //Do something; }); 


I have already introduced you to the controller arguments (they are the same as in callbacks.success). I can only add that, having received a copy of the template, you can create its clones. Notice that the clone creation syntax is greatly simplified compared to template initialization.

The controller will be called every time not only when the template is initialized, but when creating a clone.

And finally, about what it was all about. If we have a simple template (without nesting and controllers), then we can open it with the browser simply as an html file and debug the styles. Fast and efficient.

If our template is nested, and even a controller, then for debugging we just need to create a separate html-file for the test. Its creation is the advantage of copy / paste.

Test file for our example with a pop-up authorization window

 <!DOCTYPE html> <html> <head> <title></title> <meta charset="utf-8" /> <script type="text/javascript" src="../../../kernel/flex.core.js"></script> </head> <body> <script> function flexPatternTest() { var id = flex.unique(), patterns = flex.libraries.ui.patterns.create(), _pattern = patterns.get({ url : '/patterns/popup/pattern.html', node : document.body, hooks : { id : id, title : 'Test dialog window', content : patterns.get({ url : '/patterns/patterns/login/pattern.html', hooks : { login : patterns.get({ url : '/patterns/controls/textinput/pattern.html', hooks : { type: 'text', } }), password : patterns.get({ url : '/patterns/controls/textinput/pattern.html', hooks : { type: 'password', } }), controls : patterns.get({ url : '/patterns/buttons/flat/pattern.html', hooks : [{ title: 'login', id: 'login_button' }, { title: 'cancel', id: 'cancel_button' }] }), } }) }, callbacks: { success: function (model, binds, map, resources) { var instance = this, } } }).render(); }; flexPatternTest.include = [ 'ui.window.move', 'ui.window.resize', ]; flexPatternTest.exclude = [ 'flex.presentation', ]; </script> </body> </html> 


All we did was “hook” flex and define a function [flexPatternTest]. As soon as flex finishes initializing, an attempt will be made to find this function and start it. Profit - your template is running with all the necessary infrastructure in the “standalone” mode.
I note that as can be seen from the example, you can exclude individual modules from the download, as well as include the necessary ones through the [exclude] and [include] properties, respectively.

The first “build” of the sample template shown takes on average 200 to 300 ms; the next 100 - 150 ms.

Directly template controller ( flex.ui.patterns.js) first extracts the contents of the <body> tag (which allows us to transfer only this tag to the template HTML files (if no JS or CSS connection is required)); then builds the DOM tree of the template and performs all further manipulations only with the DOM tree. This mode of operation is at least 10-15% (according to my observations) slower if regular expressions were used to manipulate the contents of innerHTML. However, I tried to balance performance as much as possible and “delay” the moment I started working with DOM to the last.

And yet, for tasks, where even if there is nesting of templates, but there is no need for either the model, or the DOM card of the assembled template, or the controller, I made a template-light - flex.ui.templates.js. The difference is exactly in the approach, if flex.ui.patterns.js mainly works with the DOM tree, then flex.ui.templates.js lasts as long as possible with regular expressions, replacing hooks in innerHTML and “collecting” the DOM tree only last moment.

Here is a controller (s) of templates I have turned out. But, we go further.

And what does JQ have to do with it?


Indeed at the very beginning I mentioned the jQuery library. I have very mixed feelings for her. On the one hand, it is incredibly comfortable. On the other hand, I sometimes come across such code (written in the JQ style), with such chains, that I want to shoot myself before shooting the developer.

Of course, there is no perfect code, but, as it seems to me, JQ strongly relaxes something. I do not know.In general, whenever possible, I strive to refuse from JQ and not to apply it in projects.

However, such a cool thing as the mentioned call chains I like, but not as it is implemented in JQ. I like the concept itself, that there is some kind of wrapper function that checks or converts an input object, and then provides the ability to call various methods that will be applied to it.

Therefore, I have defined five types of input objects:


These are global "wrappers". They can be called from anywhere in the code and do something like this:

 _nodes('.buttons').events().add('click', function (event) { //Do something }); var pos = _node('#this_button').html().position().byPage(); _object(some_object).forEach(function (key, value) { //Do something }); 


For me the moment of grouping was very important. As you can see, the function of determining the position of a node on a page is in the [html.position] group, and the function of adding an event handler in the [events] group. In my subjective opinion, this makes the code more clear, because the same position can be defined both as [byPage ()] and as [byWindow ()].

What is more, adding a new functional is a very simple and not intricate business.

For example, we will make two methods: concealment and display of a node.

 flex.callers.define.node('html.hide', function () { if (this.target) { this.target.__previous_display = this.target.style.display; this.target.style.display = 'none'; } }); flex.callers.define.node('html.show', function () { if (this.target) { this.target.style.display = this.target.__previous_display !== void 0 ? this.target.__previous_display : ''; delete this.target.__previous_display; } }); 


And now we can use the newly created methods.

 _node('.buttons').html().hide(); _node('.buttons').html().show(); 


***


Here you go.Now everything seems to be. I really hope that you have not fallen asleep, and if you have fallen asleep, you slept enough (that is a holiday for people of our profession).

It is important to understand that everything described above was developed "by itself", so there is no whatever acceptable documentation and description of the API, although there are files on github that create the necessary code highlighting (intellisense) for Visual Studio. It is because of them, assholes, I still have not forced myself to describe the API - because everything is highlighted.

In addition, there are probably a lot of bugs and places in the project for optimization, because it is used very “parochially”, which simply cannot cover many applications where any flaws could be revealed. There are unresolved problems. For example, when creating a table template, all hooks inside <table> should be defined as html-comments - <! - {{rows}} ->.

I am very excited on the eve of your reaction, but I will try to control myself. Anyway, thanks for your attention.

PS
Several links:


Pps
Forgot, there is an incomplete wiki in bad English

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


All Articles