๐Ÿ“œ โฌ†๏ธ โฌ‡๏ธ

$ mol: reactive micromodular ui-framework

How long does it take to simply display a large list using modern frameworks?


2000 row listReactJSAngularJSRaw HTMLSAPUI5$ mol
Appearance list170 ms420 ms260 ms1200 ms50 ms
Update all his data75 ms75 ms260 ms1200 ms10 ms

Let's write a simple application - a personal task list. What are his characteristics?


ToDoMVCReactJSAngularJSPolymerJSVanillaJS$ mol
Size (html + js + css + templates) * gzip322 KB326 KB56 KB20 KB23 KB
Load time1.4 s1.5 s1.0 s1.7 s0.7 s
Time to create and delete 100 tasks.1.3 s1.7 s1.4 s1.6 s0.5s

A small puzzle: in front of you is a synchronous code that loads and processes the contents of 4 files , but they are loaded from the server in parallel . How can this be?


Synchronous parallel loading of resources


And now I ask for me in the rabbit hole, it is time for amazing stories ...



Club of Named Cyclists


Hello, my name is Dmitry Karlovsky and I ... the head of the web development team at SAPRUN . Our company is primarily engaged in the implementation and support of SAP products in leading companies in Russia and the near abroad. SAP itself is a huge complex system consisting of many components.


One of these components is the SAPUI5 web framework , designed to create one-page applications. This is a typical representative of boxed frameworks, that is, those that provide you with not only architecture, but also a rich library of widgets. And like any boxed framework, this is subject to the terrible disease of our time - obesity.


Old, fat, gloomy goose


There is obesity in everything: huge amounts of code from the refined German pasta; awkward widgets, barely moving lists on 100 items; sprawling trees of classes, in the wilds of which even a forest elf will get lost. All this leads to a fairly long development, and time - money.


As a result, it is quite difficult to win tenders for the development of web applications, if you indicate real time estimates. And if you win, the result is, let's say, not impressive: the application is either too simple or too slow. Especially sad on smartphones, where every kilowatt counts.


We needed a more efficient tool that allows us to create competitive large-scale cross-platform applications with low bloods, so we decided on a terrible thing - to reinvent the wheel - our own web framework with the telling name $ mol . Designed from scratch, it has incorporated a lot of fresh ideas, about which further narration will go.


Reactive programming


Invented 50 years ago, it only recently reached the world of user interfaces on the web. Moreover, youโ€™ve gotten into a fairly simple โ€œpushโ€ form: you describe some sequence of actions, you submit some data to the input, and these actions are consistently applied to each data element. However, this approach leads to difficulties in the implementation of lazy and dynamically changing calculations.


$ mol is built on a "pull" architecture, where the initiator of any action is the consumer of the result of these actions, and not the source of the data. This allows rendering only those parts of the application that fall into the visible area; create only those objects that are required for rendering at the current moment; request from the server only the data that is required for the created objects.


$ mol is saturated with "lazy calculations" and automatic release of resources. You can cache the result of the function execution with just one line and not worry about the invalidation and cleaning of this cache - the $ mol_atom module will track all dependencies by itself and do all the routine work.


const source = new $mol_atom( 'source' , ( next? : number )=> next || Math.ceil( Math.random() * 1000 ) )

const middle = new $mol_atom( 'middle' , ()=> source.get() + 1 )

const target = new $mol_atom( 'target' , ()=> middle.get() + 1 )

console.assert( target.get() === source.get() + 2 , 'Target must be calculated from source!' )
console.assert( target.get() === target.get() , 'Value must be cached!' )

source.push( 10 )

console.assert( target.get() === 12 , 'Target value must be changed after source change!' )

source middle target, target , source target .



, . , (if, for, while, switch, case, break, continue, throw, try, catch, finally).


, JS โ€” , , , , : . node-fibers , NodeJS. async/await/generators , , - , . , .


, $mol, . , , :


content() {

    const paths = [
        '/mol/app/quine/quine.view.tree' ,
        '/mol/app/quine/quine.view.ts' ,
        '/mol/app/quine/quine.view.css' ,
        '/mol/app/quine/index.html' ,
    ]

    const sources = paths.map( path => {
        return $mol_http.resource( path ).text()
    } )

    const content = sources.map( ( source , index )=> {
        const header = `# ${ paths[ index ] }\n`
        const code = '```\n' + source.replace( /\n+$/ , '' ) + '\n```\n'
        return `${ header }\n${ code }`
    } ).join( '\n' )

    return content
}

. , , 4 . , , . , .



, . , . , , โ€” .


LEGO , . $mol . , , . , - โ€” .


- , ; ; - , .


, , .


$mol_pager, , :


$mol_page $mol_view
    sub /
        <= Head $mol_view
            sub <= head /
                <= Title $mol_view
                    sub /
                        <= title -
        <= Body $mol_scroll
            sub <= body /
        < Foot $mol_view
            sub <= foot /

, , , :


$mol_app_quine $mol_page
    head /
        <= Logo $mol_icon_refresh
        <= Title -
    body /
        <= Text $mol_text
            text <= content \
    Foot null


, , . , โ€” . , . - . .


, :


-


, โ€” : , , . , ISO8601 "YYYY-MM-DD", "MM/DD/YYYY". , ? , , , ?


โ€” DOM , . DOM - , :


-


? ? JS โ€ฆ ..


,


, , - , : $mol_viewer DOM .


, : , , , . , โ€” objectOwner() โ€” :



, , . childs, , .



! . :



, . , , . childs โ€” , content -. , , , , , $mol_app_supplies.root(0).detailer().position(0).supplyDate():



โ€” , . , ?


package.json. , NodeJS , :


> npm install --depth 0

Type 'npm start' to start dev server or 'npm start {relative/path}' to build some package.
.
+-- body-parser@1.15.2
+-- compression@1.6.2
+-- concat-with-sourcemaps@1.0.4
+-- express@4.14.0
+-- mam@1.0.0
+-- portastic@1.0.1
+-- postcss@5.2.4
+-- postcss-cssnext@2.8.0
+-- source-map-support@0.4.3
`-- typescript@2.0.3

, . , : typescript, postcss, source-maps.


C , , :


> npm start

22:23 Built mol/build/-/node.deps.json
22:23 Built mol/build/-/node.js
22:23 Built mol/build/-/node.test.js

$mol_build.root(".").server() started at http://127.0.0.1:8080/

โ€” :



, . , , , .


, , . ? :


$mol_build.root(".").server() started at http://127.0.0.1:8080/

mol> git fetch & git log --oneline HEAD..origin/master
> git fetch & git log --oneline HEAD..origin/master
jin> git fetch & git log --oneline HEAD..origin/master

23:00:23 Built mol/app/supplies/-/web.css
23:00:27 Built mol/app/supplies/-/web.js
23:00:27 Built mol/app/supplies/-/web.locale=en.json
23:00:41 Built mol/app/todomvc/-/web.css
23:00:45 Built mol/app/todomvc/-/web.js
23:00:45 Built mol/app/todomvc/-/web.locale=en.json

, . , โ€” , โ€” . , :


4 ,  -


โ€” 4 , , : 30 . , . 30 jQuery , โ€” . web.js , ! !


, , . positioner.view.ts :


namespace $.$mol {
    export class $mol_app_supplies_position extends $.$mol_app_supplies_position {

        position() {
            return null as $mol_app_supplies_domain_supply_position
        }

        product_name() {
            return this.position().name()
        }

        price() {
            return this.position().price()
        }

        quantity() {
            return this.position().quantity().toString()
        }

        cost() {
            return this.position().cost()
        }

        supply_date() {
            return this.position().supply_moment().toString( 'YYYY-MM-DD' )
        }

        division_name() {
            return this.position().division().name()
        }

        store_name() {
            return this.position().store().name()
        }

    }
}

- . ? ? , 8 โ€” . , โ€” CTRL , , '-/view.tree/positioner.view.tree.ts':


/// $mol_app_supplies_position $mol_card
namespace $ { export class $mol_app_supplies_position extends $mol_card {

    /// heightMinimal 68
    height_minimal() {
        return 68
    }

    /// productLabel @ \Product
    product_label() {
        return this.text( "product_label" )
    }

    /// productName \
    product_name() {
        return ""
    }

    /// product_item $mol_labeler 
    ///     title <= product_label
    ///     content <= product_name
    @ $mol_mem()
    product_item( next? : any , prev? : any ) {
        return ( next !== undefined ) ? next : new $mol_labeler().make({ 
            title : () => this.product_label() ,
            content : () => this.product_name() ,
        } )
    }
// ...

. , DOM-. , :



, - , typescript . positioner.view.tree - .


$mol_app_supplies_position $mol_card
    height_minimal 68

    content <= Groups $mol_view sub /

        <= Main_group $mol_row sub /

            <= product_item $mol_labeler
                title <= product_label @ \Product
                content <= product_name \

            <= cost_item $mol_labeler
                title <= cost_label @ \Cost
                content <= Cost $mol_cost
                    value <= cost $mol_unit_money
                        valueOf 0
- ...

โ€” , view.tree , , , .


, , . supplyDate :


        supply_date() {
            return this.position().supply_moment().toString( 'MM/DD/YYYY' )
        }

โ€” . , . positioner.view.tree.ts:


    /// product_label @ \Product
    product_label() {
        return this.text( "product_label" )
    }

text() , :


    export class $mol_locale extends $mol_object {

        @ $mol_mem()
        static lang( next? : string ) {
            return $mol_state_local.value( 'locale' , next ) || 'en'
        }

, , :


$mol_locale.lang()

, .


, . ? .


mol/dateFormat/dateFormat.ts :


namespace $ {

    export const $mol_dateFormat_formats : { [ key : string ] : string } = {
        'en' : 'MM/DD/YYYY' ,
        'ru' : 'DD.MM.YYYY' ,
    }

    export function $mol_dateFormat() {
        return $mol_dateFormat_formats[ $mol_locale.lang() ] || 'YYYY-MM-DD'
    }

}

โ€” import, require. , ? ? :


$.$mol_dateFormat() // Uncaught TypeError: $.$mol_dateFormat is not a function

, โ€” ? ? :


        supply_date() {
            return this.position().supply_moment().toString( $mol_dateFormat() )
        }

, , , :



dateFormat2.ts โ€” . dateFormat2 โ€” . $mol_dateFormat2 โ€” . โ€” // , . โ€” .


, , , , .



, , . , โ€” . $mol , :



> $mol_log.filter('task')
< "task"
12:27:36 $mol_state_local.value("task=1476005250333") push Object {completed: true, title: "Hello!"} Object {completed: false, title: "Hello!"}
12:27:36 $mol_app_todomvc.root(0).task_completed(0) obsolete
12:27:36 $mol_app_todomvc.root(0).task_title(0) obsolete
12:27:36 $mol_app_todomvc.root(0).task_completed(0) push true false
12:27:36 $mol_app_todomvc.root(0).Task_row(0).completer().render() obsolete
12:27:36 $mol_app_todomvc.root(0).Task_row(0).render() obsolete




, , , , . , , .


, ACME ( , ) - HabHub. , .


: Git, WebStorm, NodeJS NPM.


MAM:


git clone https://github.com/eigenmethod/mam.git ./mam && cd mam

:



WebStorm, , "Start" , , , :


npm start

acme/habhub index.html, :


<!doctype html>
<html style=" height: 100% ">

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />

<script src="-/web.js"></script>
<script src="-/web.test.js"></script>
<link rel="stylesheet" href="-/web.css" />

<body mol_viewer_root="$acme_habhub">

, mol_viewer_root , . , $mol_viewer , , .


, ./acme/habhub/habhub.view.tree:


$acme_habhub $mol_view

http://localhost:8080/acme/habhub/ , , โ€” , , .




view.tree โ€” , , LEGO. , , , " ". view.ts view.tree, , . : div . view.tree , ( !) . $mol_view div DOM, , excel .


( ):


-, :


- Label over simple text
$mol_labeler_demo_text $mol_labeler
    title @ \Provider
    content @ \ACME Provider Inc.

- Label over string form field
$mol_labeler_demo_string $mol_labeler
    title @ \User name
    Content $mol_string
        hint <= hint @ \Jack Sparrow
        value?val <=> user_name?val \

, :


$mol_labeler $mol_view
    sub /
        <= Title $mol_view
            sub /
                <= title -
        <= Content $mol_view
            sub /
                <= content null

, , :


[mol_labeler_title] {
    color: var(--mol_skin_passive_text);
    font-size: .75rem;
}

. XML:


<!-- Label over string form field -->
<component name="mol_labeler_demo_string" extends="mol_labeler">

    <L10n name="title">User name</L10n>

    <mol_stringer name="content">

        <hint>
            <String name="hint">Jack Sparrow</String>
        </hint>

        <value name="user_name">
            <String />
        </value>

    </mol_stringer>

</component>

; , ; xml-entities; , . , $mol Tree, .


view.tree:


/ 3 :


  1. /
  2. ()

$ โ€” . , css, .


\ โ€” . ( ), - . , \ .


@ โ€” , .


/ โ€” . .


* โ€” . . .


< โ€” ( ). , ( ) ( ).


> โ€” ( ). , , .


# โ€” . ,


, null , - .



view.tree . , . $mol_page:


$acme_habhub $mol_page
    title \HabHub
    body /
        \Hello HabHub!


! . GitHub markdown, , markdown โ€” $mol_texter:


$acme_habhub $mol_page
    title \HabHub
    body <= Gists /
        <= Gist1 $acme_habhub_gist
            text \
                \# Hello markdown!
                \
                \*This* **is** some content.
        < Gist2 $acme_habhub_gist
            text \
                \# Some List
                \
                \* Hello from one!
                \* Hello from two!
                \* Hello from three!

$acme_habhub_gist $mol_text

[acme_habhub_gist] {
    margin: 1rem;
}


! :


$acme_habhub $mol_page
    title \HabHub
    body <= Gists /
    Gist!id $mol_text
        text <= gist_content!id \

. habhub.view.ts , :


namespace $.$mol {

    export class $acme_habhub extends $.$acme_habhub {

    }

}

:


interface Gist {
    id : number
    title : string
    body : string
}

, , . , :


uri_source() {
    return 'https://api.github.com/search/issues?q=label:HabHub+is:open&sort=reactions'
}

, , $mol_http_resource_json, json-rest :


gists() {
    return $mol_http.resource( this.uri_source() ).json().items as Gist[]
}

gister#, view.tree:


Gists() {
    return this.gists().map( ( gist , index ) => this.Gist( index ) )
}

gister# gist_content# , , , :


gist_content( index : number ) {
    const gist = this.gists()[ index ]
    return `#${ gist.title }\n${ gist.body }`
}

:


namespace $.$mol {

    interface Gist {
        id : number
        title : string
        body : string
    }

    export class $acme_habhub extends $.$acme_habhub {

        uri_source(){
            return 'https://api.github.com/search/issues?q=label:HabHub+is:open&sort=reactions'
        }

        gists() {
            return $mol_http.resource( this.uri_source() ).json().items as Gist[]
        }

        Gists() {
            return this.gists().map( ( gist , index ) => this.Gist( index ) )
        }

        gist_content( index : number ) {
            const gist = this.gists()[ index ]
            return `#${ gist.title }\n${ gist.body }`
        }

    }

}

: uriSource , gists typescript , Gist , gist_content , habhub.test.ts:


namespace $.$mol {
    $mol_test({

        'gist content is title + body'() {

            const app = new $acme_habhub

            app.gists = ()=> [
                {
                    id : 1 ,
                    title : 'hello' ,
                    body : 'world' ,
                }
            ]

            $mol_assert_equal( app.gist_content( 0 ) , '# hello\nworld' )

        }

    })
}

, :



, . , , .


GitHub


GitHub


! , , , , markdown ...


?


- โ€” .



$mol_texter $mol_lister, , . , . $mol_texter , , $mol_lister:


$acme_habhub $mol_page
    title \HabHub
    body /
        <= List $mol_list
            rows <= Gists /
    Gist!id $mol_text
        text <= gist_content!id \



. minimal_height. , $mol_text_row 40 , , , css . $mol_scroll $mol_view_visible_height , ( ). this.$.$mol_view_visible_height(). , $mol_list , . , , , .


$mol . , $mol Angular. - , . , , Virtual DOM React, , . , , , โ€” .


, . , , , . โ€” , . , , , , .



, - . DOM- :



$acme_habhub.root(0).list() , mol_view_error . $mol_atom_wait . , , . โ€” :


[acme_habhub_list] {
    min-height: 1rem;
}


- ?



! . Gists() . , , , โ€” , . $mol_status:


$acme_habhub $mol_pager
    title \HabHub
    body /
        <= Status $mol_status
            status <= Gists /
        <= List $mol_list
            rows <= Gists /
    Gist!id $mol_text
        text <= gist_content!id \


, $mol . - , , , , . , .


$mol , try-catch . ? . , , ( ), $mol_atom_wait, , . , , , . , "" (async-await).


, Proxy API. , , , . Proxy API, , , . , "" , .


, , , . greeting, :


@ $mol_mem()
greeting() {

    const config = $mol_http.resource( './config.json' ).json()
    //   ,   config  Proxy

    const profile = $mol_http.resource( './profile.json' ).json()
    //   ,   profile  Proxy

    //      ,    greeting    $mol_atom_wait
    const greeting = config.greeting.replace( '{name}' , profile.name ) 

    //     
    return greeting
}

, , . , , :


@ $mol_mem()
greeting() {

    const config = $mol_http.resource( './config.json' ).json()
    //  config  json,   

    const profile = $mol_http.resource( './profile.json' ).json()
    //  profile  json,   

    const greeting = config.greeting.replace( '{name}' , profile.name ) 
    // greeting     config  profile 

    // ,       
    return greeting
}


, $mol , . , , $mol_filler, "Lorem ipsum", , $mol_grider, .


DOM- , -. , , :


[mol_button] {
    cursor: pointer;
}

- :


$mol_button_major $mol_button

[mol_button_major] {
    background: var(--mol_skin_accent);
    color: var(--mol_skin_accent_text);
}

, - my_signup_submit, my_signup โ€” , submit โ€” , :


$my_signup $mol_page
    body /
        <= Submit $mol_button_major
            sub /
                <= submit_label @ \Submit

[mol_signup_submit] {
    font-size: 2em;
}

dom- "css-" . , โ€” - , - .


, , , , . . , , , . $mol $mol_skin. , :


:root {
    --mol_skin_base: #c85bad;
}


โ€” . , , , .



$mol , , , , "" "". "", "", . $mol :



, $mol, , , :




โ€ฆ . , , . :



UPD: , , , โ€” .


')

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


All Articles