How to create a webworkers driven multithreading App — Part 1
Content
- What are we going to build?
- Prerequisites
- Setting up the Infrastructure
- Using npx neo-app to create the App shell
- Inspecting the output
- Class configs & basic concepts
- Creating the HeaderContainer
- Creating the MainContainerController
- Connecting our App to the NovelCOVID API
- Deploying your App for production
- Summary
- Preview on Part 2
1. What are we going to build?
Here is a demo video showing the different views of the App:
As you can see, this is already a pretty complex app. We will start from scratch and progressively enhance it, while looking into some of the basic concepts as well. To keep the scope reasonable, I will split the tutorial into multiple parts.
Welcome to Part 1!
Since the App is already finished, you can take a look at the full source code. This version goes beyond Part 1 of the tutorial, so think twice if you already want to look into the final result at this point.
https://github.com/neomjs/covid-dashboard
2. Prerequisites
To follow this tutorial, you need:
- a solid understanding of Javascript, CSS & HTML
- to be familiar with the ES6+ based class system
- to know the concepts of OOP
- Chrome v80+
- You do not need any prior experience in using neo.mjs.
3. Setting up the Infrastructure
There are 2 different options (We will stick to option 2):
- Clone the neo.mjs repo: https://github.com/neomjs/neo, run the build all program inside the package.json, run the create-app program and your new app folder appears under “apps”:
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.
2. Since the new build programs are in place now with the v1.2.0 release,
we will give them a try and create a new repository. This way you can keep your own code base separate and just use neo.mjs as a node module.
Setting up a new repo on GitHub is fairly easy:
And there we go:
You can see the result of this tutorial inside the repo:
https://github.com/neomjs/tutorial-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, but you can 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:
Let us take a look at our program options. Enter “npx neo-app --help”:
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 if you don’t.
Now just enter “npx neo-app”:
The program will ask you for your workspace (folder) name. The important part here is to match the name of the repo folder.
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.
We do want to use both themes and toggle between them, so just hit enter.
For the main thread addons “Stylesheet” is already checked by default, I also added “AmCharts” & “MapboxGL”. No worries, you can easily change this inside the code later on.
After selecting the main thread addons, several tasks will run (including an npm install), which can take a couple of minutes. Now is the perfect time to grab a cup of coffee or tea.
The program will end with starting the webpack-dev-server. This will throw 2 errors:
Those errors are expected and you can ignore them. The reason is, that we are using the dev-server on the workspace folder instead of workspace/dist, since we do want access to the non dist version as well.
The dev server will automatically have opened a new browser tab. In case it was not Google Chrome, you should copy the URL and enter it there.
Click on the “apps” folder, then click on “covid”:
Congratulations! You got your first neo.mjs App up and running.
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.
> http://localhost:8093/dist/development/apps/covid/
The dist (development & production) versions also run in Firefox & Safari. While FF & Safari do support using JS Modules inside the main thread, they still lack support for them inside the worker scope. This is the reason why the dev mode is limited to Chrome for now.
Details here:
https://bugzilla.mozilla.org/show_bug.cgi?id=1247687
https://bugs.webkit.org/show_bug.cgi?id=164860
5. Inspecting the output
Let us take a quick look at the output of the npx neo-app program:
We already have a .gitignore file in place which ignores the node modules, so i am just calling “git add” on the top level folder and push it to the repo.
If you look at the index.html file you will notice 2 things:
- You can pass framework configs
- The index file just includes one script at the bottom, which is the neo.mjs main thread. It does not include your App, since this will run inside a separate thread (web worker).
Open node_modules/neo.mjs/src/DefaultConfig.mjs
You will find all available framework configs here. E.g. themes defaults to
themes: ['neo-theme-light', 'neo-theme-dark']
which is the reason why it was not included inside our index file (we picked both).
Let us add the config inside the index file and switch the order:
Reload your page inside the browser:
=> The order when using multiple themes matters, the first one will get applied by default.
[Side note] I am going to use the WebStorm based webserver from here on.
Your main entry point is the app.mjs file.
You add a method for Neo.onStart() which creates your new App using Neo.app(). This is a shortcut for Neo.controller.Application.create().
https://github.com/neomjs/neo/blob/dev/src/controller/Application.mjs#L70
In case you don’t want to render your main view right away, you can pass
createMainView: false
Once the neo.mjs main thread is done with creating the worker setup, the app worker will import the file and trigger onStart()
https://github.com/neomjs/neo/blob/dev/src/worker/App.mjs#L77
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 lives inside its own thread as well and there is a 4th thread for the data worker (top right of the screenshot). Not important at this point.
Now is a good time for a quick break, since this is a lot to think about in case it is the first time using neo.mjs.
6. 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 your App will only contain the files you 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.
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.Base or sub-classes.
Viewport is extending container.Base which is extending component.Base:
=> 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:
https://github.com/neomjs/neo/tree/dev/docs/app/view
Using a container.Viewport as 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. Since ES6+ classes do not support class properties, I enhanced them with a custom config system.
More details here:
https://codeburst.io/javascript-classes-state-management-v2-2df7663de580
In short: You need to add configs (properties) 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 component.Button. Time to look into the class here:
https://github.com/neomjs/neo/blob/dev/src/component/Button.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 of the file (right before the export) of all neo.mjs class definitions.
https://github.com/neomjs/neo/blob/dev/src/Neo.mjs#L48
As a result of using the trailing underscore, the following methods will (optionally become available):
beforeGetText()
beforeSetText()
afterSetText()
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.
https://github.com/neomjs/neo/blob/dev/src/component/Button.mjs#L214
[side note vdom] The part which matters is:
textNode.innerHTML = value;
=> We map the new value to the vdom.
At the end this is calling:
me.vdom = vdom;
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 should warn you if you do add it again by mistake.
Working with instances:
const myButton = Neo.create(Button, {
iconCls: 'fa fa-home',
text : 'My Button'
});myButton.set({
iconCls: 'fa fa-user',
text : 'Something else'
});
- Please use Neo.create() instead of the new operator, since this will trigger onConstructed() as well as init() internally.
- Like with extending classes, you do not use a trailing underscore here (you are just passing values, right?).
- You can use set() to change multiple configs at once. Especially when multiple configs get mapped to the vdom, this makes sense:
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.
7. Creating the HeaderContainer
The coding part can start :)
Let us go back to our MainContainer.mjs file and remove the content of the dummy app:
Now reload your browser tab:
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:
Nice, we got the country flag of Germany. By default, each vdom object is using a div tag and we applied a background color. 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:
You switched to the country flag of Belgium.
layout: {ntype: 'hbox', align: 'stretch', direction: 'row-reverse'},
You might have invented a new country flag. Easy, right?
For more infos on layout, the docs App is your best friend:
Difference to other common frameworks / libraries: a layout is extending core.Base, not component.Base since it does not have a DOM related output.
The second 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 container.Base which is extending component.Base. 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, so we will remove it again.
Might not look fancy yet, but this is the basic structure of our App.
Since we care about OOP, let us move the Header into its own class. We create the file “HeaderContainer.mjs” inside 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 browser tab:
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 and add it to the instance config instead:
We switched to passing the HeaderContainer using the “module” config, 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 a lot more stuff into the HeaderContainer. Since you won’t learn much from typing this by yourself, just replace your HeaderContainer file with the following code:
Afterwards reload your browser tab again:
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:
https://github.com/neomjs/neo/blob/dev/resources/scss/src/apps/covid/_HeaderContainer.scss
https://github.com/neomjs/neo/blob/dev/resources/scss/theme-dark/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 see that
- clicking on the 2 buttons will throw errors (see the console inside the screenshot)
- there is no data yet
8. Creating the MainContainerController
Let us start with the switch theme button inside our HeaderContainer.
I added a string based (click) handler, which does not exist yet. This explains the error. 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:
- you can set configs inside methods like the constructor or onConstructed (which gets triggered after all ctors inside the class hierarchy are done).
- 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.Component is already implemented.
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:
Okay, not related to component.Base (last time of this anecdote, I promise!).
Back to our MainContainer file, add the import statement, add the JS module into the controller config:
Lovely :)
Reload your browser tab:
We can see the ctor log which we added and now also get 2 real errors, since the controller complains about the missing button handler mappings.
We learned something really important here:
- Not every view needs its own controller. In case a view does not have an controller on its own, it can communicate to the closest controller instead.
- Imagine we did add the HeaderContainerController. 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 to work 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, I 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 via the main thread 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:
Unless you like typing a LOT, here you go:
Not judging here: IF you like typing this, make sure to also import NeoArray.
Reload your browser tab and click the switch theme button:
You get console logs for every delta update (dom manipulation) which happens in main. Looking into this can be very helpful!
When we now click our “Switch Theme” Button, 3 things happen
- 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 the docs App as a live example).
- We exchanged the logo.
- We changed the iconCls of our “Switch Theme” Button
One important thing here is that we gave the logo a reference config:
This allows us to get the logo component instance inside the controller:
9. Connecting our App to the NovelCOVID 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/NovelCOVID/API
Let us enhance our MainContainerController a little bit:
We added:
- the apiSummaryUrl config (we could also add this one into getStaticConfig())
- loadSummaryData() just using fetch() on the endpoint
- 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
You can just copy it from here:
We are extending core.Base and 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).
Here is the code for applySummaryData():
Reload your browser tab:
This is it, the final result for part 1!
10. Deploying your App for production
Now you will most likely want to see your App in Firefox & Safari as well.
Fair point :)
Since we did switch the order of our themes inside the index.html file, let us open buildScripts/myApps.json:
add the following line into the Covid Object:
"themes": "'neo-theme-dark', 'neo-theme-light'",
npm run build-all
29s here.
Open the dist development or production versions in Firefox & Safari:
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.
11. 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:
- how to use npx neo-app to create an App shell
- the basic concepts of the config system
- how to modify your main view
- how to keep your architecture modular with separating classes
- how to create and use Component Controllers
- how to switch themes
- 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.
12. Preview on Part 2
In Part 2 we are going to look into the data package: How to create a collection or a store and connect them to different views like Tables, the 3d Gallery and the Helix.
We will work with Routing and connect views in an event driven way.
In case you got curious and can’t wait for Part 2, there is still the link at the top of this article => pointing to the final solution ;)
Since writing this article took a lot of effort, sharing it and leaving feedback is greatly appreciated!
Best regards & happy coding,
Tobias
P.S.: I am very excited to see what talented developers like you can do with using neo.mjs!