| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [View Raw Code] [Original HTTPS Page] |
Angular 2 includes native support for writing applications which live in a WebWorker. This document describes how to write applications that take advantage of this feature. It also provides a detailed description of the underlying messaging infrastructure that angular uses to communicate between the main process and the worker. This infrastructure can be modified by an application developer to enable driving an angular 2 application from an iFrame, different window / tab, server, etc..
WebWorker support in Angular2 is designed to make it easy to leverage parallelization in your web application. When you choose to run your application in a WebWorker angular runs both your application's logic and the majority of the core angular framework in a WebWorker. By offloading as much code as possible to the WebWorker we keep the UI thread free to handle events, manipulate the DOM, and run animations. This provides a better framerate and UX for applications.
Bootstrapping a WebWorker application is not much different than bootstrapping a normal application. The primary difference is that you don't pass your root component directly to bootstrap. Instead you pass the name of a background script that calls bootstrapWebWorker with your root component.
To bootstrap Hello World in a WebWorker we do the following in TypeScript
<html>
<head>
<script src="https://github.jspm.io/jmcriffey/bower-traceur-runtime@0.0.87/traceur-runtime.js"></script>
<script src="https://jspm.io/system@0.16.js"></script>
<script src="angular2/web_worker/ui.js"></script>
</head>
<body>
<hello-world></hello-world>
<script>System.import("index")</script>
</body>
</html>// index.js
import {bootstrap} from "angular2/web_worker/ui";
bootstrap("loader.js");// loader.js
importScripts("https://github.jspm.io/jmcriffey/bower-traceur-runtime@0.0.87/traceur-runtime.js", "https://jspm.io/system@0.16.js", "angular2/web_worker/worker.js");
System.import("app");// app.ts
import {Component, View, bootstrapWebWorker} from "angular2/web_worker/worker";
@Component({
selector: "hello-world"
})
@View({
template: "<h1>Hello {{name}}</h1>
})
export class HelloWorld {
name: string = "Jane";
}
bootstrapWebWorker(HelloWorld);There's a few important things to note here:
For reference, here's the same HelloWorld example in Dart.
<html>
<body>
<script type="application/dart" src="index.dart"></script>
<script src="packages/browser.dart.js"></script>
</body>
</html>// index.dart
import "package:angular2/web_worker/ui.dart";
import "package:angular2/src/core/reflection/reflection.dart";
import "package:angular2/src/core/reflection/reflection_capabilities.dart";
main() {
reflector.reflectionCapabilities = new ReflectionCabilities();
bootstrap("app.dart");
}import "package:angular2/web_worker/worker.dart";
import "package:angular2/src/core/reflection/reflection.dart";
import "package:angular2/src/core/reflection/reflection_capabilities.dart";
@Component(
selector: "hello-world"
)
@View(
template: "<h1>Hello {{name}}</h1>"
)
class HelloWorld {
String name = "Jane";
}
main(List<String> args, SendPort replyTo) {
reflector.reflectionCapabilities = new ReflectionCapabilities();
bootstrapWebWorker(replyTo, HelloWorld);
}
This code is nearly the same as the TypeScript version with just a couple key differences:
You can do almost everything in a WebWorker component that you can do in a typical Angular 2 Component. The main exception is that there is no DOM access from a WebWorker component. In Dart this means you can't import anything from dart:html and in JavaScript it means you can't use document or window. Instead you should use data bindings and if needed you can inject the Renderer along with your component's ElementRef directly into your component and use methods such as setElementProperty, setElementAttribute, setElementClass, setElementStyle, invokeElementMethod, and setText. Not that you cannot call getNativeElementSync. Doing so will always return null when running in a WebWorker. If you need DOM access see Running Code on the UI.
When running your application in a WebWorker, the majority of the angular core along with your application logic runs on the worker. The two main components that run on the UI are the Renderer and the RenderCompiler. When running angular in a WebWorker the bindings for these two components are replaced by the WebWorkerRenderer and the WebWorkerRenderCompiler. When these components are used at runtime, they pass messages through the MessageBroker instructing the UI to run the actual method and return the result. The MessageBroker abstraction allows either side of the WebWorker boundary to schedule code to run on the opposite side and receive the result. You can use the MessageBroker Additionally, the MessageBroker sits on top of the MessageBus. MessageBus is a low level abstraction that provides a language agnostic API for communicating with angular components across any runtime boundary such as WebWorker <--> UI communication, UI <--> Server communication, or Window <--> Window communication.
See the diagram below for a high level overview of how this code is structured:
If your application needs to run code on the UI, there are a few options. The easiest way is to use a CustomElement in your view. You can then register this custom element from your html file and run code in response to the element's lifecycle hooks. Note, Custom Elements are still experimental. See MDN for the latest details on how to use them.
If you require more robust communication between the WebWorker and the UI you can use the MessageBroker or MessageBus directly.
The MessageBus is a low level abstraction that provides a language agnostic API for communicating with angular components across any runtime boundary. It supports multiplex communication through the use of a channel abstraction.
Angular currently includes two stable MessageBus implementations, which are used by default when you run your application inside a WebWorker.
Angular also includes three experimental MessageBus implementations:
Note: If you want to pass custom messages between the UI and WebWorker, it's recommended you use the MessageBroker. However, if you want to control the messaging protocol yourself you can use the MessageBus directly.
To use the MessageBus you need to initialize a new channel on both the UI and WebWorker. In TypeScript that would look like this:
// index.ts, which is running on the UI.
var instance = bootstrap("loader.js");
var bus = instance.bus;
bus.initChannel("My Custom Channel");// background_index.ts, which is running on the WebWorker
import {MessageBus} from 'angular2/web_worker/worker';
@Component({...})
@View({...})
export class MyComponent {
constructor (bus: MessageBus) {
bus.initChannel("My Custom Channel");
}
}Once the channel has been initialized either side can use the from and to methods on the MessageBus to send and receive messages. Both methods return EventEmitter. Expanding on the example from earlier:
// index.ts, which is running on the UI.
import {bootstrap} from 'angukar2/web_worker/ui';
var instance = bootstrap("loader.js");
var bus = instance.bus;
bus.initChannel("My Custom Channel");
bus.to("My Custom Channel").next("hello from the UI");// background_index.ts, which is running on the WebWorker
import {MessageBus, Component, View} from 'angular2/web_worker/worker';
@Component({...})
@View({...})
export class MyComponent {
constructor (bus: MessageBus) {
bus.initChannel("My Custom Channel");
bus.from("My Custom Channel").observer((message) => {
console.log(message); // will print "hello from the UI"
});
}
}This example is nearly identical in Dart, and is included below for reference:
// index.dart, which is running on the UI.
import 'package:angular2/web_workers/ui.dart';
main() {
var instance = bootstrap("background_index.dart");
var bus = instance.bus;
bus.initChannel("My Custom Channel");
bus.to("My Custom Channel").add("hello from the UI");
}
// background_index.dart, which is running on the WebWorker
import 'package:angular2/web_worker/worker.dart';
@Component(...)
@View(...)
class MyComponent {
MyComponent (MessageBus bus) {
bus.initChannel("My Custom Channel");
bus.from("My Custom Channel").listen((message) {
print(message); // will print "hello from the UI"
});
}
}The only substantial difference between these APIs in Dart and TypeScript is the different APIs for the EventEmitter.
Note Because the messages passed through the MessageBus cross a WebWorker boundary, they must be serializable. If you use the MessageBus directly, you are responsible for serializing your messages. In JavaScript / TypeScript this means they must be serializable via JavaScript's structured cloning algorithim.
In Dart this means they must be valid messages that can be passed through a SendPort.
The MessageBus API includes support for zones. A MessageBus can be attached to a specific zone (by calling attachToZone). Then specific channels can be specified to run in the zone when they are initialized. If a channel is running in the zone, that means that any events emitted from that channel will be executed within the given zone. For example, by default angular runs the EventDispatch channel inside the angular zone. That means when an event is fired from the DOM and received on the WebWorker the event handler automatically runs inside the angular zone. This is desired because after the event handler exits we want to exit the zone so that we trigger change detection. Generally, you want your channels to run inside the zone unless you have a good reason for why they need to run outside the zone.
Note Implementing and using a Custom MesageBus is experimental and requires importing from private APIs.
If you want to drive your application from something other than a WebWorker you can implement a custom message bus. Implementing a custom message bus just means creating a class that fulfills the API specified by the abstract MessageBus class.
If you're implementing your MessageBus in Dart you can extend the GenericMessageBus class included in angular. if you do this, you don't need to implement zone or channel support yourself. You only need to implement a MessageBusSink that extends GenericMessageBusSink and a MessageBusSource that extends GenericMessageBusSource. The MessageBusSink must override the sendMessages method. This method is given a list of serialized messages that it is required to send through the sink. The MessageBusSource needs to provide a Stream of incoming messages (either by passing the stream to GenericMessageBusSource's constructor or by calling attachTo() with the stream). It also needs to override the abstract decodeMessages method. This method is given a List of serialized messages received by the source and should perform any decoding work that needs to be done before the application can read the messages.
For example, if your MessageBus sends and receives JSON data you would do the following:
import 'package:angular2/src/web_workers/shared/generic_message_bus.dart';
import 'dart:convert';
class JsonMessageBusSink extends GenericMessageBusSink {
@override
void sendMessages(List<dynamic> messages) {
String encodedMessages = JSON.encode(messages);
// Send encodedMessages here
}
}
class JsonMessageBusSource extends GenericMessageBuSource {
JsonMessageBusSource(Stream incomingMessages) : super (incomingMessages);
@override
List<dynamic> decodeMessages(dynamic messages) {
return JSON.decode(messages);
}
}Once you've implemented your custom MessageBus in either TypeScript or Dart you can tell angular to use it like so: In TypeScript:
// index.ts, running on the UI side
import {bootstrapUICommon} from 'angular2/src/web_workers/ui/impl';
var bus = new MyAwesomeMessageBus();
bootstrapUICommon(bus);// background_index.ts, running on the application side
import {bootstrapWebWorkerCommon} from 'angular2/src/web_workers/worker/application_common';
import {MyApp} from './app';
var bus = new MyAwesomeMessageBus();
bootstrapWebWorkerCommon(MyApp, bus);In Dart:
// index.dart, running on the UI side
import 'package:angular2/src/web_workers/ui/impl.dart' show bootstrapUICommon;
import "package:angular2/src/core/reflection/reflection.dart";
import "package:angular2/src/core/reflection/reflection_capabilities.dart";
main() {
reflector.reflectionCapabilities = new ReflectionCapabilities();
var bus = new MyAwesomeMessageBus();
bootstrapUiCommon(bus);
}// background_index.dart, running on the application side
import "package:angular2/src/web_workers/worker/application_common.dart" show bootstrapWebWorkerCommon;
import "package:angular2/src/core/reflection/reflection.dart";
import "package:angular2/src/core/reflection/reflection_capabilities.dart";
import "./app.dart" show MyApp;
main() {
reflector.reflectionCapabilities = new ReflectionCapabilities();
var bus = new MyAwesomeMessageBus();
bootstrapWebWorkerCommon(MyApp, bus);
}Notice how we call bootstrapUICommon instead of bootstrap from the UI side. bootstrap spans a new WebWorker / Isolate and attaches the default angular MessageBus to it. If you're using a custom MessageBus you are responsible for setting up the application side and initiating communication with it. bootstrapUICommon assumes that the given MessageBus is already set up and can communicate with the application. Similarly, we call bootstrapWebWorkerCommon instead of boostrapWebWorker from the application side. This is because bootstrapWebWorker assumes you're using the default angular MessageBus and initializes a new one for you.
The MessageBroker is a higher level messaging abstraction that sits on top of the MessageBus. It is used when you want to execute code on the other side of a runtime boundary and may want to receive the result. There are two types of MessageBrokers. The ServiceMessageBroker is used by the side that actually performs an operation and may return a result. Conversely, the ClientMessageBroker is used by the side that requests that an operation be performed and may want to receive the result.
To use MessageBrokers in your application you must initialize both a ClientMessageBroker and a ServiceMessageBroker on the same channel. You can then register methods with the ServiceMessageBroker and instruct the ClientMessageBroker to run those methods. Below is a lightweight example of using MessageBrokers in an application. For a more complete example, check out the WebWorkerRenderer and MessageBasedRenderer inside the Angular WebWorker code.
// index.ts, which is running on the UI with a method that we want to expose to a WebWorker
import {bootstrap} from 'angular2/web_worker/ui';
var instance = bootstrap("loader.js");
var broker = instance.app.createServiceMessageBroker("My Broker Channel");
// assume we have some function doCoolThings that takes a string argument and returns a Promise<string>
broker.registerMethod("awesomeMethod", [PRIMITIVE], (arg1: string) => doCoolThing(arg1), PRIMITIVE);// background.ts, which is running on a WebWorker and wants to execute a method on the UI
import {Component, View, ClientMessageBrokerFactory, PRIMITIVE, UiArguments, FnArgs}
from 'angular2/web_worker/worker';
@Component(...)
@View(...)
export class MyComponent {
constructor(brokerFactory: ClientMessageBrokerFactory) {
var broker = brokerFactory.createMessageBroker("My Broker Channel");
var arguments = [new FnArg(value, PRIMITIVE)];
var methodInfo = new UiArguments("awesomeMethod", arguments);
broker.runOnService(methodInfo, PRIMTIVE).then((result: string) => {
// result will be equal to the return value of doCoolThing(value) that ran on the UI.
});
}
}// index.dart, which is running on the UI with a method that we want to expose to a WebWorker
import 'package:angular2/web_worker/ui.dart';
main() {
var instance = bootstrap("background.dart");
var broker = instance.app.createServiceMessageBroker("My Broker Channel");
// assume we have some function doCoolThings that takes a String argument and returns a Future<String>
broker.registerMethod("awesomeMethod", [PRIMITIVE], (String arg1) => doCoolThing(arg1), PRIMITIVE);
}
// background.dart, which is running on a WebWorker and wants to execute a method on the UI
import 'package:angular2/web_worker/worker.dart';
@Component(...)
@View(...)
class MyComponent {
MyComponent(ClientMessageBrokerFactory brokerFactory) {
var broker = brokerFactory.createMessageBroker("My Broker Channel");
var arguments = [new FnArg(value, PRIMITIVE)];
var methodInfo = new UiArguments("awesomeMethod", arguments);
broker.runOnService(methodInfo, PRIMTIVE).then((String result) {
// result will be equal to the return value of doCoolThing(value) that ran on the UI.
});
}
}Both the client and the service create new MessageBrokers and attach them to the same channel. The service then calls registerMethod to register the method that it wants to listen to. Register method takes four arguments. The first is the name of the method, the second is the Types of that method's parameters, the third is the method itself, and the fourth (which is optional) is the return Type of that method. The MessageBroker handles serializing / deserializing your parameters and return types using angular's serializer. However, at the moment the serializer only knows how to serialize angular classes like those used by the Renderer. If you're passing anything other than those types around in your application you can handle serialization yourself and then use the PRIMITIVE type to tell the MessageBroker to avoid serializing your data.
The last thing that happens is that the client calls runOnService with the name of the method it wants to run, a list of that method's arguments and their types, and (optionally) the expected return type.
| Back | FazBrowse Home | New Git URL |