Hello everyone, my name is Dmitry Karlovsky and I ... I want to hurt you. A lot of pain. I will remind you of all the suffering that you experience while being held captive by your favorite js framework. I will bring you to the very bottom of the deepest hopelessness. And then, when you are completely desperate and lose faith in the community, I will give you a helping hand and show you a bright future.
Further, your text is a transcript of the speech of the same name with IT Global Meetup # 11 . You can read it as an article or open it in the presentation interface . Enjoy reading.
Here you have chosen the framework. How do you imagine working with him? Well, it should be a rich set of ready-made solutions from which you can easily and quickly build an application of any complexity.
Examples of such frameworks: ExtJS, SAPUI5, VCL.JS, $ mol - they provide you with a bunch of ready-made widgets, from banal buttons to complex grids with sorting, filtering and other beautiful ladies.
But have they heard about them? And how many people use them in their daily work?
Not many. What is strange, is not it?
Unfortunately, low-level frameworks are now more popular, giving only the most basic abstractions over the platform, and everything else has to be implemented by hand or searched on the Internet, and fastened with a blue electrical tape. That is, for each project we collect a unique high-level framework based on a low-level one.
Examples of such frameworks are widely known. These are: Angular, VueJS, EmberJS and many others.
Recently, the trend to build an ecosystem around individual highly specialized libraries has again gained popularity. It is still worse here - in each project, you first have to build a low-level framework from your library heaps to your liking, and on top of it you already have to cycle components that often do not differ in re-usability outside the project being developed.
The most famous examples are: React, Backbone, jQuery
Why are high-level frameworks so unpopular and from project to project we saw the same bikes?
The fact is that most of them are of extremely poor quality. They are huge, slow, difficult to learn, and most importantly, very oaky, which is why customizing their components for a project is comparable in complexity to writing these components from scratch.
But the $ mol framework is knocked out of the total number of monsters. It is lighter and faster than many low-level frameworks, but it comes with a rich set of batteries in the set, allowing you to build a complex application with just a few lines of code.
And this wealth does not at all aggravate the final application, because only the code that is really necessary is included in it.
So, let's take the brightest representatives from three different levels: the React library, the low-level Angular framework, and the high-level $ mol. Let's create each of them in a trivial static application and see what overhead costs give different tools.
Level | Proj create (s) | Proj size (KB) | Deps | App size (KB) | App start (ms) | |
---|---|---|---|---|---|---|
React | Lib | 70 | 120,000 | 892 | 45 | 230 |
Angular | Low | 50 | 160,000 | 752 | 80 | 350 |
$ mol | High | 0 | 200 | 0 | 15 | 150 |
Manually creating projects on React and Angulyar is not a trivial task, so we use standard code generators (create-react-app and angular / cli). They generate a bunch of files for about a minute, after which you can already delete the extra ones and add the necessary ones.
In $ mol, a new application is created simply by creating a directory and locating the files you need, without installing eight hundred dependencies. As you can see, the $ mol high-level framework provides a 3 times smaller increase in the page size than the React library alone.
What do you usually need to use a third-party component?
Not very comfortable, right?
The basic principle of $ mol is automation, so using a third-party component is extremely simple.
The collector will take care of downloading his sources and including them in the correct places of the resulting bundles.
And what usually have to do when the component is no longer needed?
It is necessary not to forget so much of everything so that your application does not have extra code.
And what to do when you no longer need any $ mol component?
The other basic principle of $ mol is not to pay for what you don’t use. And this means that it is enough to stop using the component so that it stops being included in the bundle. Everything is extremely simple and reliable, because the human factor is completely excluded.
Suppose you found two great components: a nice datepicker and an advanced datagrid. But the trouble is, one uses the full capabilities of the new version of the framework, and the other is not yet compatible with it. What to do if you don’t want to give up one of them, but you want to use new features yourself, but rewriting all the old components may take more than one month?
You can try to include both versions of the framework in the bundle, but this is not only that the size of the application will be significantly inflated, it is also not likely to start. If only because the package manager will not allow you to install two versions of the same framework at the same time. And even if it succeeds, the fight between versions will not be avoided. Therefore...
The $ mol modules have no versions. Instead, they are developed in accordance with the principle of "openness / closure."
This means that you can safely upgrade all modules to the latest revisions without worrying about their changed API.
But what to do when you need to introduce a more convenient API that is not compatible with the existing one? And everything is simple - if the API is not compatible, then this is another module with a different name. The name may be similar. For example, with an increased number at the end. And since these are two different modules, then they can coexist in the project at the same time. At the same time, they may have a lot of common code, up to: one - this is nothing more than a lightweight facade for another.
In the diagram, you can see that the hypothetical new version of the rendering engine with the $ mol component, when coexisting with the previous one, will only slightly increase the total volume of the bundle. Could you imagine such a thing in any other framework?
By the way, do not be surprised that the modules in $ mol are so small, because it is a pleasure to create and use microscopic modules when automating work with dependencies.
Manually driving data streams is a real hell. You seem to have thought through all the options, took into account all the events, but you should give the result to testers, as a lot of bug reports start to fall on you as a result of contradictory logic. To cope with it, you have two ways.
In Angular, we bet on the concept of streams, where you statically adjust the flow of events and reactions to them. Unfortunately, any nontrivial application has a lot of dynamics, which means to create for it an efficient configuration of threads is a real puzzle. Look only at how streams look like the correct conditional branching in two variables.
const Result = Rx.Observable.combineLatest( Pos , Start ) .select( ([ pos , start ])={ return pos < start ? Left : Rx.Observable.of( null ) } ) .switch() .distinctUntilChanged() .debounce( 0 )
And this is a pretty simple example.
Reactive cells are simply containers that themselves track dependencies between themselves, dynamically building data streams in the most optimal way.
@ $mol_mem result() { return this.pos() < this.start() ? this.left() : null }
Here we simply declare a property that is a function of other properties, if we change them, ours will also be updated in the most efficient way.
I'm sure you heard about VueJS or MobX - they are just based on the concept of cells. But the implementation in $ mol is the most advanced. This is what $ mol can do, which no other framework can do.
Yes, yes, we have destructors with predictable execution moment.
All this is a good example of how competent architecture allows you to write flexible, intelligent and efficient code, without losing visibility or inflating its size.
Surely everyone who has eaten JavaScript dog may recall in its practice not one fascinating story, when, because of one small typo, he fought for half a day with some impossible bug. The problem is that man is essentially inattentive by nature. He easily makes the most stupid mistakes, and then may not see them, looking at them point-blank.
The solution to this problem is static typing, which allows the computer to automatically and in a finite time check the correctness of the code, relieving the programmer’s brain from a heap of trifles and allowing it to concentrate on really important things.
But how much do you know of frameworks written in TypeSript?
I know only 4. These are low-level Angular and CycleJS, as well as high-level VCL.JS, and ... $ mol. All the rest are simply unsuitable for the development of more or less large projects. Well, they are not suitable ... you just need to double the number of developers every year, just to keep the same pace of development.
Ok, we screwed the datepicker, and it even works. But the trouble, he looks like a white crow in the black quarter.
If the developer of the component, in the name of great encapsulation, isolated styles using css-in-js, css-modules or even web-components, then you can only congratulate you on the need to style the third-party component through a bundle of crutches. For example, you can have this kind of selectors if you need to style the days in the drop-down calendar in datepicker.
/* No isolation */ .my_date .acme_calendar_day { ... } /* Angular default isolation */ :host ::ng-deep .calendar ::ng-deep .day { ... } /* CSSModules isolation */ .date [class*="calendar"] [class*="day"] { ... }
It is important to understand the difference between encapsulation and isolation.
Encapsulation allows you to work with a complex thing through a simple interface, but does not interfere and immerse yourself in the inside when it is really necessary. Isolation is a much stronger constraint that binds us hand and foot and forks a third-party component to organically fit it into its application.
$ mol is permeated by encapsulation from head to toe. But there is no isolation in it. At all. No. Even in scripts. But in $ mol there are global unique human-readable names and uniform rules of stylization, which are their own, which are third-party components.
/* ( $mol_view as "Day" ) from $mol_calendar */ [mol_calendar_day] { ... } /* Day from ( $mol_calendar as "Calendar" ) from $mol_date */ [mol_date_calendar_day] { ... } /* Day from Calendar from $my_date that inherits $mol_date */ [my_date_calendar_day] { ... }
In the given example, the name "my_date_calendar_day" is formed automatically according to the following principle ... First, the global name of the component ("my_date") is taken, then the local name of the calendar in the context of the owner ("calendar") is glued to it, and so on. he was not in the hierarchy.
The most pleasant thing about styling the $ mol component is that the programmer does not need to manually assign a class-wrapper to each house-element — they are generated automatically based on the generated component tree.
<mol_view my_date_calendar_day mol_date_calendar_day mol_calendar_day mol_view> 03 </mol_view>
Just imagine: you can develop a component without thinking about its styling, but at the same time have complete control over the visualization of any house element. Without crutches, mountains of copy-paste, increasing the specificity of selectors and other nasty things.
Take the first Angular pattern from the documentation.
<h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" [class.selected]="hero === selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span{{hero.name}} </li> </ul>
What is wrong with this code?
From the point of view of support, he has two misfortunes: the absence of a unique class for each element and its incredible oakenness. What will we do if on one page out of 20 we need to remove the subtitle, on the other add a paragraph with a description after it, and on the third print the names of the characters before the identifier, and not after? There are not many options here ...
The simplest thing is to copy the template several times and fix it in place.
<h1>{{title}}</h1> <ul class="heroes"> <li *ngFor="let hero of heroes" [class.selected]="hero === selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span{{hero.name}} </li> </ul>
But then we get a lot of duplication and the need for these similar, but different duplicates synchronously refactor. This monkey job is one more hotbed of bugs.
The other extreme is to teach the component to any frills that may be required from it anywhere. And every time someone needs something else, expanding the component with new features.
<h1>{{title}}</h1> <h2 *ngIf="subTitleVisible">My Heroes</h2> <p *ngIf="description">{{ description }}<p> <ul class="heroes"> <li *ngFor="let hero of heroes" [class.selected]="hero === selectedHero" (click)="onSelect(hero)"> <ng-template [ngIf]="badgeFirst; else badgeLast"> <span class="badge">{{hero.id}}</span{{hero.name}} </ng-template> <ng-template #badgeLast> {{hero.name}} <span class="badge">{{hero.id}}</span> </ng-template> </li> </ul>
With this approach, the component very quickly turns into a large heavy Swiss knife, which is inconvenient to use and very difficult to maintain.
And all the same, copy-paste appears in it when it is necessary to rearrange elements in places, add wrappers and change the structure in another way depending on the state.
Any dynamic in templates is the pain and suffering of HTML programming.
Reflection on the problems of patterns inevitably leads to the idea that they ... are not needed at all. If the programmer operates with components, and JS and DOM browser objects, then why do we need this mimicry under HTML, which does not have expressive power to describe components? HTML has a very limited model.
This is in addition to its extreme verbosity. Components are needed: strings, numbers, flags, dictionaries, arrays, user-defined data types, two-way and one-way property binding, the unique name of each subcomponent, inheritance, mixing, partial redefinition and, finally, program logic written in a full-fledged programming language. All of this with a very tight creak stretches on the core of HTML. Therefore..
There are no templates in $ mol at all.
No more imitation of HTML — only components and their interrelations. In $ mol there is a simple and visual language for the composition of the component, which allows us to declare in a declarative form which component it contains, which name it has and how it is associated with it. It is so simple that I will teach you its basics in just 5 minutes.
$my_heroes $mol_view
Here, we created the $ my_heroes component, which inherits properties from the very base component $ mol_view. One line of code of two names. What could be easier? In the same Angular, you will need at least 5, plus another 2 for import and registration.
To declare a new or override an existing property - just write its name with an indent, and after the space - the default value.
$my_heroes $mol_view sub /
Here we have redefined the "sub" property, which will return a list of nested components.
Inside we will have 3 components: the title, subtitle and the actual list of heroes.
$my_heroes $mol_view sub / <= Title $mol_view <= Title_sub $mol_view <= Rows $mol_list
The left arrow means getting the property value, and after it comes the actual declaration of this property indicating its name and value separated by a space. All that begins with "$" are component classes. Their properties can also be overridden.
Introducing more and more new properties, we can build a visual hierarchy of any depth we need.
$my_heroes $mol_view sub / <= Title $mol_view sub / <= title \ <= Title_sub $mol_view sub / <= title_sub @ \My Heroes <= Rows $mol_list rows <= rows /
Here, for example, it is immediately obvious that the title is displayed in the nested component named Title, the subtitle is in Title_sub, and the lines with the characters in Rows.
The backslash precedes the raw data, which may contain any characters other than line breaks.
<= descriptions \ \ \ \ \ - \ " ' & < \
Have you dreamed of such happiness in html or json?
Sewing texts in code is, of course, simple and fast, but what about localization? Just add a dog and the text after it will be moved to a separate file with translations, and in the class generated from view.tree TypeScript there will be only an appeal using a passable key.
web.locale = en.json
{ "$my_heroes_title_sub" : "My Heroes" }
heroes.view.tree.ts
/// title_sub @ \My Heroes title_sub() { return this.$.$mol_locale.text( "$my_heroes_title_sub" ) }
Please note that a piece of the source responsible for this property is always displayed side by side in the comments. That allows you to quickly master the syntax view.tree.
Normal properties always return a single value. Multi-properties can return different values depending on the key passed to them.
$my_heroes $mol_view - ... Row!index $mol_row sub <= item_content!index / <= Badge!index $my_badge title <= hero_id!index \ <= hero_name!index \
In this case, the key for the strings of heroes and everything that is in them we will have their ordinal index.
So, why do we need all this whistle with a bunch of properties? The fact is that each such property is an extension point. We can change any aspect of behavior in a component simply by redefining the corresponding property.
$my_top_heroes $my_heroes Title_sub null
Here we have created a new component based on the base one, but instead of the nested component, we simply return null, which leads to its complete exclusion from rendering.
By overriding lists, we can add new components between existing ones.
$my_new_heroes $my_heroes sub / <= Title - <= Title_sub - <= Description $mol_text text <= description \ <= Rows -
Here we declare a component that is exactly the component of $ my_heroes, but after the headers has an additional immediately declared block with a description.
And, of course, we can easily and easily rearrange components, somewhere in the depths. For example, the name and identifier of the hero.
$my_reflect_heroes $my_heroes item_content!index / <= hero_name!index - <= Badge!index -
The view.tree language is so convenient for describing components that it pulls it to be used to describe any classes at all. And this is actually possible, but only with caution.
But sometimes you need nontrivial logic to calculate properties. And the logic is best described in the language intended for it. Therefore, we simply inherit from the automatically generated class from view.tree and redefine its properties, implementing any trick.
export $my_heroes extends $.$my_heroes { heroes() { return $mol_http.resource( '/heroes' ).json() } rows() { return Object.keys( this.heroes() ).map( index =this.Row( index ) ) } hero_name( id : string ) { return this.heroes()[ id ].name } }
Here we loaded the json array with the heroes' data from the server, created for each hero a display line declared in view.tree, and declared a multi-property that returns the name of the hero by its ordinal number.
Somehow too easy, is not it? But this is the whole point, that abstractions should simplify development, and not complicate it.
And now about the sad - the problem of a rooster and eggs.
Developers are not eager to learn a tool for which there are few vacancies. A company - tied to a tool for which few professionals. So it turns out that bright ideas that simplify and cheapen the development die in the shadow of larger, but less functional libraries, forcing to write a bunch of the same type of code with a bunch of bicycles.
Remarkable is the experience of Tinkoff Bank , where a team of experts better than me for a long time has been developing a new version of Internet banking in the fashionable React. It turned out pretty braking monster with less functionality, which is then optimized for a very long time. But the result is still far from perfect.
At $ mol, a smaller team in a shorter period could have achieved much greater responsiveness of the interface. And immediately, but not after user complaints.
Or an example from the RIT 2017 conference, where the guys from Wrike talked about Angular and React. They showed slides through an application specially written for the speech, allowing the audience to vote in real time for one of the frameworks.
What do you think, on what framework did they implement it?
On jQuery. Because it turned out to be easier to do. By the way, it turned out to be easier for me to write down the application on $ mol. Draw conclusions.
There is a more significant disadvantage of a small community ...
When the maintainer falls exhausted to the ground, his flag has to be picked up by the one who needs it most. Understanding this, I designed $ mol such that it was no more time consuming to maintain than the final application based on it. There are no more than 10 thousand lines of code, grouped into small highly specialized replaceable modules that have a very flat structure. This is a drop in the ocean compared to any more or less large application. And this is the drop in which it is most likely the easiest to understand.
Do not forget about the social component. We do not have huge marketing budgets, so we cannot afford the fragmentation of the community in the spirit of Angular, where one half saws applications in TypeScript, the other half in JavaScript, and the third in general in Dart. We concentrate on the fact that all our small community can work together to develop a common component base.
There is no need to bike everyone in your project the same typical solutions. It is better to devote time to creating something truly new.
Hope I managed to infect you with $ mol ideas. Offer yours. Or just tell us about your pain, and together we will think of something so that it will no longer exist.
Source: https://habr.com/ru/post/341146/
All Articles