5

Define a web 4.0 app to be multi threaded

 3 years ago
source link: https://itnext.io/define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Define a web 4.0 app to be multi threaded

Take your skills to an entirely new level by learning to create a visually stunning and blazing fast next generation web app using web workers.

The app as well as your components will live within an application web worker.

Content

  1. What are we going to build?
  2. Prerequisites
  3. Setting up the Infrastructure
  4. Using npx neo-app to create the app shell
  5. The three different environments
  6. Inspecting the output
  7. Class configs & basic concepts
  8. Creating the HeaderContainer
  9. Creating the MainContainerController
  10. Connecting our app to the disease.sh API
  11. Deploying your app for production
  12. Summary
  13. Final thoughts

1. What are we going to build?

We will start from scratch going step by step and progressively enhance our app, while looking into basic architecture and framework concepts as well.

Don’t be afraid :)

While the final result is definitely complex, this tutorial is as easy as possible. High level components like the 3d gallery and helix already exist, so we don’t need to create them on our own.

To keep the scope reasonable, I will split the tutorial into multiple parts.

Welcome to part 1!

We will start with a single page app (SPA) version and transform it into a multi window app inside the last part of this blog series.

The app is already finished, so you can take a look at the full source code. The final result obviously goes beyond Part 1 of the tutorial, so think twice if you want to look into this code early.

apps/covid (single page app version)

apps/sharedcovid (multi window version)

2. Prerequisites

To follow this tutorial, you need:

  1. a solid understanding of Javascript, CSS & HTML
  2. to be familiar with the ES6+ based class system
  3. to know the concepts of OOP
  4. Google Chrome

You do not need any prior experience in using frameworks / libs like Angular, React or neo.mjs.

It would literally be impossible to create this app using Angular or React (at least the multi window version), so neo.mjs is the obvious choice.

3. Setting up the Infrastructure

There are 2 different options to create a neo.mjs app.
Important: We will stick to option two.

Option one:

  1. Clone the neo.mjs repo
  2. Run npm install
  3. Run the build-all program inside the package.json
  4. Run the create-app program and your new app folder appears under “apps”:
1*BQWGDgsqZvIXsvvh0P6a-A.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

Especially in case you want to work on the framework source code as well, like adding new components or a new theme, it makes sense to start this way. You can later on move your app folder into a real app shell.

Option two:

We can create a new app shell (workspace) with simply entering npx neo-app into our terminal. This way we can keep your own code base separate from the framework and just use neo.mjs as a node module.

[Optional] Before we do this, let us create a new GitHub repository first:

And there we go:

You can see the result of this tutorial (part 1) inside the repo:
github.com/neomjs/tutorial-shared-covid-app-part-1

Feel free to create an own repo as well, but this is optional for this tutorial.

I will clone the new repo next:

As you might already see I am using WebStorm. Feel free to stick to any IDE you feel comfortable with.

4. Using npx neo-app to create the app shell

We are getting close to finish everything we need to start coding. The last missing step is to create the neo.mjs app shell.

To do this, we need to open our terminal (or CMD on Windows) and enter the folder 1 level above our fresh repo folder:

1*xZzGqRUp_RK_AVCk6dAXPA.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

Let us take a look at our program options. Enter npx neo-app --help :

1*COeZ_gknk_sFDuPi2AJJrQ.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

[Side note] I am using @latest to ensure getting the newest version.

There are some options which you can pass on the command line if you want to, but there is also a visual interface in place in case you don’t.

We can change all passed options later on.

Now enter npx neo-app :

1*61O2-rVkTCThpdjc9CjYoA.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

In case you are below version 2.3, please add the latest flag.

The program will ask you for your workspace (app shell folder) name. The important part here is to match the name of the repo folder.

1*p8vbvuQunBg17khi8nTrjw.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

The next question is the app name. Since this also equals the namespace inside our code base, it should be PascalCase. Let’s keep it short: Covid.

1*mzBHwrQyNU2rTLXUFyt1Zg.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

We do want to use both themes and dynamically switch between them at run time, so just hit enter.

1*Z6_efU-19oBt8mch1we6Wg.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

The main thread addons DragDrop and Stylesheet are already selected. We will need more later on. For now, just hit enter again.

1*IzrM_yav6JVjnh1N7XnAJg.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

The last question is if we want to use dedicated workers or SharedWorkers.

While our final version will use shared workers, I strongly recommend to start with “normal” ones. Just hit enter again.

[Side note] The reason to not use shared workers directly is that this makes the debugging a bit more complicated. For workers you can see all console logs directly inside your browser devtools. For shared workers you need to open a new window for each of them:

chrome://inspect/#workers

The framework uses an abstraction layer on top of the workers communication, so we can switch later on with just changing one top level config. The API will stay exactly the same. Pretty damn cool, right? [End side note]

It takes a bit of time now, since a lot is happening on the background:

Total time for covid buildAll: 44.68s

Once done, the dev server will automatically open a new browser tab inside your default browser. In case this is not Google Chrome, you should copy the URL and enter it there.

1*C1PWUWTla8AqOV0fj5x8Ww.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

Click on the “apps” folder, then click on “covid”:

1*CeGzQUWarRx8hinrp23bqw.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

Congratulations! You got your first neo.mjs app up and running.

This is pretty much your “Hello World” starting point.

Change the URL to “docs” next:

As you can see, you got the full framework source code documentation here, but also a documentation for your own new app. “Covid” is the name(space) we just picked.

[Side note] The docs app is build with neo.mjs as well. It is good for getting an overview of the class hierarchy as well as seeing all available class configs and methods. You can click on each one to enter a source code view. The docs app is not yet helpful though to learn working with the framework. For now it is limited to desktop browsers. In case you would like to see a mobile version of this app, feel free to open a ticket. Help appreciated. [End side note]

5. The three different environments

One design goal of the neo.mjs project is getting UI development back to where it belongs: directly into the browser.

development mode
The dev mode runs without any JavaScript builds or transpilations at all. We don’t need JS based source maps and can debug the real code directly without any external side effects or hot module replacements. This has saved me a lot of time already and is super convenient.

As you can see, we get the real code and on top of this, it is running inside our application worker.

The dev mode requires browsers to support JS modules as well as dynamic imports inside the worker scope. Chromium is ready for quite a while, so it works in Google Chrome and Microsoft Edge. Webkit (Safari) has resolved the ticket, but it is not yet live. The dev mode also works inside Safari Technology Preview.

Mozilla (Firefox) is not ready yet, but the team is actively working on it:

dist/development
In case you are using Angular or React, this is what you would call the dev mode. We are using webpack in a smart way to generate the split chunks we need including source maps for JS and CSS.

This mode runs in all major browsers.

dist/production
We are using webpack in a smart way to generate the split chunks we need without source maps for JS and CSS. The code is minified.

This mode runs in all major browsers.

6. Inspecting the output

Let us take a quick look at the output of the npx neo-app program:

1*pSdOfaFpewyreutia3q_yA.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

We already have a .gitignore file in place which ignores the distfolder as well as the node_modules, so i am just callinggit addon the top level folder and push it to the repo.

You can add more external dependencies here if you like to or create an html skeleton. Our main thread starting point includes the MicroLoader:

The MicroLoader will import the Main.mjs file of the framework. This one will create the workers setup. Your app will not live within the main thread.

The neo-config.json file contains the framework options. The build programs will adjust this file for the different environments for us.

You will find all available framework configs inside DefaultConfig.mjs. E.g. themes defaults to

themes: ['neo-theme-light', 'neo-theme-dark']

which is the reason why it was not included inside our config file (we picked the default value → both).

Let us add the themesconfig inside the config file and switch the order:

Reload your app inside the browser:

1*k603LiCsWciZDzMlVTwh5Q.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

→ The order when using multiple themes matters, the first one will get applied by default.

Your main entry point is the app.mjsfile.

Think about this one as the “index.html” for our application web worker.

Once the neo.mjs main thread is done with creating the worker setup, the app worker will import the file and trigger onStart() .

We can include a mainView in case we want to put this one into the DOM right away, but this is optional. Again:

This is the main design goal of the neo.mjs framework: most parts of it as well as your apps run within the app worker and not inside the main thread.

The virtual dom engine can live inside its own thread as well and there is a thread for the data worker. Not important at this point.

Now is a good time for a quick break, since this is a lot to think about. Especially in case it is your first time using neo.mjs.

7. Class configs & basic concepts

Let us take a look at the main view of your new app:

We import the JavaScript modules (classes) which we are going to use at the top. This means, that the dev & dist versions of our app will only contain the files we use and not everything else to keep the total file size small. Make sure to add file name extensions to your imports, since your code is supposed to directly run inside a Browser.

[Side note] Meaning no bare module specifiers for the dev mode itself

It is important to think carefully about which base class you want to extend. Is your new class a utility file with no DOM related content? Go for e.g. core.Base. In case there is DOM related content, go for component.Baseor classes extending this one.

E.g. the class hierarchy for Viewport is:

→ You can easily check the class hierarchy inside the Docs App (top right). The Docs App UI itself is written using neo.mjs, so you can dive into the source code to see another example on how to create Apps:
docs/app/view

Using a container.Viewportas the base class will use the full available size of the screen (height & width 100%) and since it is extending container.Base, you can add items.

In general neo.mjs is highly config driven using a custom class config system.

In short: You need to add configs (similar to class fields) inside the static getConfig() method.

static getConfig() { return {
// add your configs here
}}

The order of the configs does not matter. Configs are applied via

Object.defineProperty()

so they are get & set driven.

myButton.text = 'Something else';

Meaning you can dynamically change them just with assigning a new value and your UI will update (consistency: adding & changing configs the same way).

I just used “myButton”, meaning an instance of button.Base. Time to look into the class here: src/button/Base.mjs#L81

I highlighted the line of the text config. If you look close, there is a trailing underscore:

text_: '',

A trailing underscore will get consumed by Neo.applyClassConfig(), which you can find at the end (right before the export) of all neo.mjs class definitions.

As a result of using the trailing underscore, the following methods will optionally become available:

beforeGetText(value)beforeSetText(value, oldValue)afterSetText(value, oldValue)

With this, he have the pre- and post-processing covered. Especially the “afterSetX” methods are very helpful for mapping configs to the virtual dom or firing events.

1*wy7RaX5mz5T-Qul7ZfOPLQ.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

src/button/Base.mjs#L222

[Side note vdom] The part which matters is:

textNode.innerHTML = value;

→ We map the new value to the vdom. At the end of this method we are using:

me.vdom = vdom; // setter

In short: this “assignment” is sending the current vdom and the previous version to the vdom worker via main, the vdom worker will create the deltas, send those back to the main thread, main will apply them to the real dom and send a success response back to your scope: the App worker.

The vdom has an optional property called “removeDom” => you can remove a node from the real dom without removing it from the vdom this way. The benefit is to keep the structure of the vdom consistent. It can be convenient to alway have the iconCls as the first node and the text as the second node in case multiple methods can change them. [End side note vdom]

Important: You only use a trailing underscore once for each class config inside the class hierarchy. Example:

class MyButton extends Button {
static getConfig() { return {
text: 'My Button'
}}
}

No trailing underscore, since we already have it inside the Button class. The framework will warn you if you do add it again by mistake. We are just changing the value of the config, this will not override the business logic.

Working with instances:

const myButton = Neo.create(Button, {
iconCls: 'fa fa-home',
text : 'My Button'
});myButton.set({
iconCls: 'fa fa-user',
text : 'Something else'
});
  1. Please use Neo.create()instead of the new operator, since this will trigger onConstructed()as well as init()internally.
  2. Like with extending classes, you do not use a trailing underscore here (you are just passing values).
  3. You can use set()to change multiple configs at once. Especially when multiple configs get mapped to the vdom, this makes sense.

Bad example:

myButton.iconCls = 'fa fa-user';
myButton.text = 'Something else';

would change the configs the same way, but this would trigger the vdom engine twice. Using set(), this only happens once. Since we do care about performance, set()is the way to go.

8. Creating the HeaderContainer

The coding part can finally start :)

Let us go back to our MainContainer.mjsfile and remove the content of the dummy app:

Now reload your browser tab:

1*K2Faq__k4D4LL6hR13nOCQ.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

Especially in case you already have been using JavaScript before someone came up with the idea to move the entire UI development into nodejs, you might feel relieved now.

You just changed an ES8+ module, you reloaded your browser and your change is there. No build process or transpilation involved.

The reason is so trivial, but at this point worth mentioning: It works because … guess what … browsers are supposed to handle JavaScript.

The first thing to pick when using a Container is the layout. The most useful one is most likely Flexbox, with the extensions HBox and VBox (horizontal box, vertical box). Let us apply VBox and add some dummy items:

1*dATIkTvcYDVvxpFx-qRJ_Q.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

Nice, we got the country flag of Germany. You can use camelCase for your style definitions, or apply them as strings. I prefer the camelCase version.

Let us switch our layout to hbox and switch the colors for item 2 & 3:

1*J0Me1RujB5CQwekIpGjB4A.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

You switched to the country flag of Belgium.

layout: {ntype: 'hbox', align: 'stretch', direction: 'row-reverse'},
1*iBWYZ4TKQotTvHf9E1Kklg.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

You might have invented a new country flag. Easy, right?

For more infos on layout, the docs App is your best friend:

I am using the top left search field and unchecked the parent class checkboxes (top right).

Obviously can can dive into the code as well to look for config options:
src/layout/Flexbox.mjs

Difference to other common frameworks / libraries: a layout is extending core.Base, not component.Basesince it does not have a DOM related output.

The question which you might think of is: “What is ntype?”

ntype is just a convenience shortcut for Components which you know are already there. You remember for sure: Viewport is extending component.Base at some point. So, Component has to be available inside our module.

You can of course import it:

and then switch ntype to module and use it directly. You will agree though that this feels like creating boiler plate code here, so we will remove it again.

1*7SI7tVR_d-MCUMfDa66uxg.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

Since we care about OOP, let us move the Header into its own class. We create the file HeaderContainer.mjsinside our view folder:

A pretty basic class definition: We are extending container.Base, we are using the HBox layout. The “cls” config will get applied to the vdom root node of this class, height is a shortcut to add a height style to the vdom root node.

We also added a logo component. This one is using an image tag instead of the default div tag, so we do add tag & src to the logo vdom object.

The next step is to include our new HeaderContainer into the MainContainer:

The cool thing here is that you actually can just drop a JS Module into the container items directly.

Reload your app browser tab:

1*5ckLe3i87A5lXgqfkciS0Q.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

Perfect! You just got an idea how to keep your code base modular & clean.

Since height is more like a layout driven config, let us remove it from the HeaderContainer class and add it to the instance config instead:

We switched to passing the HeaderContainer using the moduleconfig, since this allows us to pass configs just for this instance and not affect the HeaderContainer class which we could use in other spots with a different height.

Reload your browser tab, the result is the same.

We will now add the other DOM markup into the HeaderContainer. This is really just HTML/ CSS. Just replace your HeaderContainer file with the following code:

Afterwards reload your browser tab again:

1*bopIkfNpTNUz6qh8cNtkhA.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

Now this already looks a lot closer to the app we are going to build.

I cheated a little bit here and already included the needed style definitions into the neo.mjs themes. Take a look at:

resources/scss/theme-dark/apps/covid/HeaderContainer.scss
resources/scss/src/apps/covid/HeaderContainer.scss

In case you look close, you will notice that exporting CSS variables is optional.

Looking at our current app, you will notice that

  1. clicking on the 2 buttons will throw errors
  2. there is no data yet

In case you would like to learn more about working with JSON based virtual DOM:

More input on application based themed styling:

9. Creating the MainContainerController

Let us start with the switch theme button inside our HeaderContainer.

1*nY18AhWbsgqmDc7FYNkiSQ.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

I added a string based (click) handler, which does not exist yet. This explains the error when clicking the button. We could just map the handler to a method inside this class. The following screenshot is just an example, don’t write it. Something like:

  1. you can set configs inside methods like constructoror onConstructed(which gets triggered after all constructors inside the class hierarchy are done).
  2. you can use non string based handlers and directly map them to class methods.

We do like patterns like MVVM though. So a view controller feels needed here. We are in luck, controller.Componentis available.

Now we could just add a controller for the HeaderContainer itself, but since this one doesn’t really do much, let us just add a controller to the main container instead.

Add the file view/MainContainerController.mjs:

Just a basic setup. A quick look into the docs to check the class hierarchy:

1*6ZsuPmTZKvk8RPhcIJ7oxQ.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

It is not extending component.Base .

Back to our MainContainer file, add the import statement, add the JS module into the controller config:

Lovely :)

The framework will create a controller instance for you when just dropping in the imported module.

Reload your browser tab:

We can see the onConstructed()log which we added and now we also get 2 real errors, since the controller complains about the missing button handler mappings.

We learned something really important here:

  1. Not every view needs its own controller. In case a view does not have a controller on its own, it can communicate to the closest parent controller instead.
  2. Imagine we would have added a HeaderContainerController as well. Then you could either add the handler methods inside this controller or still stick to the MainContainerController and put them there. Or put some in 1 controller and the rest into the other. As it makes sense for your architecture. Get creative!

Add the 2 missing handlers, reload the browser tab, click on the buttons:

Alright, we can start working on the business logic.

Before we do this, I would like to show you something else regarding the “config driven” approach:

Expand the instance inside the console, scroll down and look for:

iconPosition: (...)

Click on the 3 dots (it is a getter) and they will change to

iconPosition: "left"

Okay, we just have to do this now:

Under the hood we just changed configs inside the app worker. These configs are tied to the button vdom, so the app worker will send the new & old vdom to the vdom worker, this one creates the deltas, sends them back to main, they get applied to the real dom and we get a success message inside our app scope.

Still not easy, but repeating this one feels important.

Back to our “Hello World”… I mean “Switch Theme” Button.

Replace onSwitchThemeButtonClick(data) with the following code:

Reload your browser tab and click the switch theme button:

When we now click our “Switch Theme” Button, 3 things happen

  1. We switch the theme, more precisely: since we are using CSS variables, we just switch the class on the MainContainer top level DOM node to switch to different CSS vars for each theme. You can apply themes to parts of your app as well (see e.g. the docs app as a live example).
  2. We exchanged the logo.
  3. We changed the iconClsof our “Switch Theme” Button

One important thing here is that we gave the logo a reference config:

1*RA8pza37MfkZM2GRkJxHLw.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

This allows us to get the logo component instance inside the controller:

1*A9P0tBOIx271M2TwjYXjnA.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

10. Connecting our app to the disease.sh API

An app without data is kind of boring, so as the second last Step of this tutorial, let us connect it to the following external API:

https://github.com/disease-sh/API

Let us enhance our MainContainerController a little bit:

We added:

  1. the apiSummaryUrl as a JS module variable
  2. loadSummaryData() just using fetch() on the endpoint
  3. applySummaryData() just logging the output

Reload your browser tab, hit the “Reload Data” Button:

Now this looks promising (not the content, that one is scary!).

Since we do want to format our numbers in a nice way and use the formatting methods in different spots, we add a last file:

covid/Util.mjs

We are extending core.Baseand are using a static method called formatNumber(). We do not need to create an instance of this class.

Importing it into the MainContainerController and adding the applySummaryData()logic next:

We are grabbing the total-stats container via reference as well as the last update text, then change the virtual dom via adding the API data formatted with our Utility class.

We also added onConstructed() to trigger a first call to the API right away (without the need to click on the Button).

At the end, there is once more the

container.vdom = vdom;

magic (you know, the worker ping pong game).

Reload your browser tab:

1*UmbMa-Lb4Nd2MQm7FOizOw.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

This is it, the final result for part 1!

11. Deploying your app for production

Now you will most likely want to see your app in Firefox & Safari as well.

Fair point :)

npm run build-all

Open the dist/developmentor dist/production versions in Firefox & Safari:

Firefox:

Safari (non tech preview):

1*P_a09wb0RtwGvSn9tZ7Ltg.png?q=20
define-a-web-4-0-app-to-be-multi-threaded-9c495c0d0ef9

Open your docs app again:

Since build-all contains the generate-docs task, you have the latest version of your app inside the docs now.

12. Summary

Wow! In case you are reading this I am extremely proud of you!

This was the hell of a tutorial with a massive amount of content.

You just learned:

  1. how to use npx neo-app to create an app shell
  2. the basic concepts of the config system
  3. how to modify your main view
  4. how to keep your architecture modular with separating classes
  5. how to create and use Component Controllers
  6. how to switch themes
  7. how to connect to an external API

In case you have questions, make sure to ask them!

At this point you are ready to create an app on your own.

After playing with neo.mjs a bit more, you could also contribute to the project. In case you like the concepts, enhance them. In case you don’t, start discussions what should change.

Join the Slack Channel:

13. Final thoughts

In case you would like to get more input on how to create your own components, there are some in depth guides inside the neo.mjs blog:

https://neomjs.github.io/pages/

For example:

You can find the neo.mjs framework repository here:

I already wrote a part 2 article for neo.mjs v1 a while ago. The logic is similar, but view models did not exist at this point yet:

Call for action:

In case you would like to see a rewrite of part 2 for neo.mjs v2.3, please add a comment here!

If you create your own neo.mjs app and write a blog post on this one, I would love to share it inside the blog. Just give me a ping :)

Looking forward to your feedback!

Best regards & happy coding,
Tobias

Preview image:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK