10 minutes read
In this installment we’ll write an app that retrieves data from a remote OData service and displays it in a data table. But unlike most of web apps that depend on some kind of external storage we’re not going to write the query-logic inside the app itself. Instead, we’ll outsource the data retrieval part into a separate TypeScript file that’ll constitute our WebWorker. This file will be loaded in a context separated from ‘window’ context. Our UI will be based on the nice jquery.dataTables library and just to make it somewhat more realistic we’ll implement another component, Console, for event logging.
Our app’s structure and data flow look like this:
And this is the final app:
Here’s a live demo for testing.
What are WebWorkers?
A WebWorker is a JavaScript file containing implementations of the onmessage event handler and the postMessage method. WebWorkers react to incoming messages by executing onmessage handlers which usually end up calling the postMessage methods. There are, of course, another event handlers available: onchange, onerror etc. but in this article we’ll focus on the first two. But the most important thing to be aware of isn’t the interface itself. It’s actually the environment in which WebWorkers run. Unlike most other browser scripts they must not access the DOM and have to run in a thread separated from the one that instantiated them. The only way to send data from and to WebWorkers is via message passing (that is, postMessage). In our case we’re building a simple app comprising of three components and a single web worker.
At the first sight the first component, MyAppComponent, looks like a mere container containing the other two. But there’s more as we want to instantiate the WebWorker outside of any specialized components. Additionally, we’d like to have the event handling and message passing being centralized inside a single component. Therefore we decide to instantiate our web worker within the MyAppComponent. Also we provide two event handlers.
The heart of the whole is located in the initWebWorker method because there we instantiate the logic residing in the external worker.js script. This script must be located within a path known by the web server. Together with the instantiation we take care of providing a proper listener to message events. In our case we decide to take the data-property reference because we know that all data provided by WebWorkers will always be copied when leaving its execution context. Additionally, we want to maintain a log of such events by writing a short entry into another property, lastEntry. Finally, we send a signal to Angular’s ChangeDetector to update this component and all of its children. Now let’s examine our component’s template to understand the data flow.
Ignoring the ubiquitous Bootstrap UI library I’m using here the core of our template is based on two special tags: customers and console (Lines 4 and 7) and a button on line 12.
Let’s first dissect the customers-tag:
<customers [list]=“customersList” (rowSelected)=“onRowSelected($event)”></customers>
In this part of the template we’re passing down the content from MyAppComponent.customersList to its child component’s property CustomersComponent.list
Also we’ve declared our event handler onRowSelected to react to emitted rowSelected events. The event handler expects a single argument $event that’ll contain data provided by the event originator.
Our logging component, Console, doesn’t emit any events up the chain but expects a single value to be provided by its parent component:
<console [message]=“lastEntry”></console>
The third player in this game is the button we use to initialize the execution of the retrieval logic that’ll query a remote OData service, then pass a message from the Worker to our component and finally land as a set of rows inside the table.
To activate the whole logic we use the standard click event and wire it up to the onGetCustomersClicked handler from MyAppComponent. Inside this method we find a single command, a postMessage call of the Worker we instantiated previously. Of course, the message we’re using here is just a demo without any complex semantics behind. WebWorkers and their postMessage methods are very flexible as they allow us to define APIs that react to any kind of commands, messages, arguments etc. We’re completely free here to define our API of choice and indeed, one should take some time to think about the definition of a proper messaging API before developing a WebWorker.
WebWorker Source
Now let’s examine our WebWorker’s source code. As already described, the worker must live outside the thread of our main application and will only react to messages passed via standardized interfaces. As the standard says we must provide a proper event handler for message events. Here we’re using TypeScript, just like with the rest of our code, to make ourselves more happy (strong typing!) but when it comes to postMessage we must resort to some weird casting because a method with the same name is also a part of our browser’s interface. Of course, we know that our WebWorker would never get a chance to do anything with the DOM and there’s no sane logic to even try to map our postMessage to the one from the browser. However, there’s still a problem with this collision as the TypeScript transpiler would run into errors when trying to produce a working JS-script. Therefore, we cast the current object (WebWorker) into any, call the method postMessage on it and pass our result as its argument. Now TypeScript will correctly transpile the Worker’s code.
Although a simple demo our worker executes a slightly more complex task than just passing some messages back and forth. We’re accessing a remote OData service and transforming retrieved values into tabular data. To make it easier to control the logic we’re outsourcing it into another script called nw.client. There we’re using a very flexible library called o.js that does all the tedious and error prone stuff regarding OData access, parsing, filtering etc.
NwClient is a class containing a private setup method and a single public interface get() that expects a table name as its sole argument. It returns an Observable of values containing a data property where our expected results should be located. The setupOData method is based on the tutorial from o.js GitHub pages. More detailed info on o.js and how to use it can be found here. All that our client does is effectively taking a table name and querying the service whose URL is hard-coded in a variable above the class. But it’s also possible to define other OData services by using the NwClient’s constructor.
Retrieving data and displaying it
Now, what happens to the data we get from the service? First, the main component, MyAppComponent, reacts to WebWorker’s message event and copies the data to its own property, customersList . Then it forwards the data to its subordinated child components as we saw it in MyAppComponent’s template definition. There, in CustomersComponent, the data goes a step further and gets parsed into columns by the table component.
Angular’s OnChanges lifecycle hook is responsible for refreshing the table. Each time the main component changes the properties of its child components the life cycle method ngOnChanges will be called. We refresh the table because we know that our main component only changes properties after having received a message from the WebWorker. Of course one thing’s still missing: our table’s definition. This is how it looks like when we define a data table that’s based on jquery.dataTables plugin. The most important part is the data property that receives the reference to the list property that’s decorated with @Input().
Regarding tabular data, there’s no reason not to use any other widget library or table, so I’ll leave it to the reader to decide if he/she prefers this or any other table. I like jquery.dataTables because it’s easy to use, it’s open-source and isn’t a part of some bigger widget framework.
Our table is capable of emitting events when users click on rows. The value provided by rowSelected, which is of type EventEmitter, contains data from the row that was clicked. This data will be sent up the chain to the parent component that’ll use it to populate the message property which belongs to ConsoleComponent.
Again, we’re utilizing the life cycle method OnChanges to react to incoming data from our parent component. And just to make our UI more user friendly we constrain the amount of visible messages to 10. And as with any other components that are capable of receiving external data, we use @Input() annotation to declare the property message as a data-bound property that’ll be changed during change detection.
Building the App, building the WebWorker
No matter how trivial a JavaScript app may look like if it’s based on some of the available frameworks it’ll ultimately end up having some kind of a building machinery. And our app is no exception as it uses WebPack with several of its more or less known plugins to build up everything. While working on the app we can utilize WebPack’s dev-server via npm scripts defined in package.json. For more realistic scenarios there’s also a separate server.ts script to run the app with the hapi.js web server. Also, please consult the README of this project.
Conclusion
WebWorkers are an alternative way to separate heavy-weight chunks of code from the main thread (mostly UI). They can help us overcome some limitations of our browser’s environments which often result in slow UIs and ugly pauses between refreshes. Dealing with WebWorkers looks easy because of its rather simple API structure. However, deploying them and keeping everything in sync takes some time and I hope this article will be of some value to you.
Have fun! 🙂
5 thoughts on “Intro to Angular 2 – Part 5 – WebWorkers”
Nice article,informative
I liked the article. Thanks for a quick show of the WebWorker API with onmessage and postmessage.
Here are my thoughts on your article:
– It seems like there should be something like an Rxjs subscribe API for web workers. the .addEventListener method is nice and seems to work that way.
– Would it make sense to have the WebWorker bind to an observable stream of inputs from the component? This would allow observable stream API in and out. Effectively addEventListener and postMessage provide a similar interface and it could be wrapped with the Observable API.
– How do you develop a proper messaging API for use in communicating to WebWorkers? What is a good way to come up with messaging semantics?
– Why are you saying onmessage = () =>{} instead of onmessage() {}?
Thanks for writing this. I see that using WebWorkers seems simple.
Hi Kent,
Glad you liked the article. Thanks!
You can always convert a Promise into an Observable because a Promise is basically a ‘poor man’s Observable’ that can only deliver a single value. There’s a method called “fromPromise” that you can use to convert a Promise into an real Observable.
But the reason they use imperative-style methods is, in my opinion, because they want developers to have the greatest possible influence on the design of those services. The same methodology applies to ServiceWorkers as well. Previous ‘solutions’ like AppCache were more or less declarative and this didn’t worked well. Now they shift more towards imperative building blocks like addEventListener. And I suppose the same strategy applies to WebWorkers as well. If you look at the fundamentals of those Workers (SharedWorkers, ServiceWorkers, WebWorkers etc.) they all rely upon the same set of methods & events.
But, as I’ve said, this is just how I see all these things. I may be completely wrong and there could be more sane explanations to why they want us to code imperatively. However, the presence of imperative code is usually a sign for “hacking is welcome here” mind set while declarative approach works mostly towards “do no hack just let me know what you want from me”.
Kind regards,
Harris
Did you get a chance to explore https://www.npmjs.com/package/angular2-web-worker ? Whats your opinion about it?
Thanks so much for the article. it is really very helpful.but i seems not able to get it run.are you using any third party pakages for the web worker. have seen some woker.js file in the repo. thanks