Tuesday, October 22, 2013

Node-Webkit - an example of AngularJS using AMQP.

I just recently discovered Node-Webkit and it's pretty awesome.  In this post, I'm going to show you how I created an AngularJS application that communicates using AMQP.

That's right.  AngularJS using AMQP.  This isn't some gimmick where I'm calling to an Express backend via websockets and the server is communicating to RabbitMQ.  One of the Angular controllers is literally using AMQP.

Honestly, this isn't magic.  Node-Webkit provides the essential binding for Webkit (Chromium) to use the Node.js runtime.  All I need to do is wire up the application.

Node-Webkit has an application structure that's kind of a combination between a Node.js application and a web application.  You could probably even use MimosaJS without any changes to build and test the application if you wanted to.  Node-Webkit requires a package.json to existing the root folder, with a property called main to point at the HTML page that serves as the entry point of the application.
{
    "name": "nw-demo",
    "version": "0.0.1",
    "main": "index.html",
    "dependencies": {
        "amqp": "0.1.7",
        "uuid": "1.4.1"
    }
}
You can even, and should, specify your Node.js dependencies via NPM.  In our case, I need the Node-AMQP and Node-UUID libraries.

The entry point, index.html, is simply a webpage.  In a Node-Webkit application, you bootstrap your application like you would a web app.  Instead of loading dependent libraries via HTTP, there accessed from the file system.  In my case, I'm using AngularJS, which has special semantics for wiring up an application.  The following is an abbreviated sample of my index.html:

Bootstrapping the Application
<!DOCTYPE html>
<html lang="en" ng-app="AmqpApp">
<head>
  <!-- ... -->
  <script type="text/javascript" src="vendor/angular.min.js"></script>
  <script type="text/javascript" src="lib/amqp.js"></script>
  <script type="text/javascript" src="lib/app.js"></script>
</head>
<body>
  <!-- ... -->
  <div ng-controller="MainCtrl" class="container">
    <div>
Form controls for publishing.
      <h2>Publish a Message</h2>
      <div class="row">
        <div class="col-md-12">
          <strong>Message ID</strong>
        </div>
        <div class="col-md-12">
          <input type="text" 
                 class="form-control" 
                 ng-model="id_box" 
                 disabled/>
        </div>
      </div>
      <br />
      <div class="row">
        <div class="col-md-6">
          <strong>Headers (JSON)</strong>
          <textarea class="form-control" 
                    rows="10" 
                    ng-model="header_box">
          </textarea>
        </div>
        <div class="col-md-6">
          <strong>Body (String or JSON)</strong>
          <textarea class="form-control" 
                    rows="10" 
                    ng-model="body_box">
         </textarea>
        </div>
      </div>
      <br />
      <div class="row">
        <div class="col-md-6">
          <button type="button" 
                  class="btn btn-default" 
                  ng-click="useSample()">
            Use Sample
          </button>
        </div>
        <div class="col-md-6 text-right">
          <button type="button" 
                  class="btn btn-default" 
                  ng-click="publish()">
            <i class="glyphicon glyphicon-send"></i>
            <span>Publish</span>
          </button>
          <button class="btn btn-danger" type="button"
              ng-show="!usingHeartbeat"
              ng-click="heartbeatOn()">
            <i class="glyphicon glyphicon-heart-empty"></i>
            <span>On</span>
          </button>
          <button class="btn btn-danger" type="button"
              ng-show="usingHeartbeat"
              ng-click="heartbeatOff()">
            <i class="glyphicon glyphicon-heart"></i>
            <span>Off</span>
          </button>
        </div>
      </div>
Form controls displaying received messages.
      <h2>Messages Received</h2>
      <div class="row">
        <div class="col-md-12" ng-show="messages.length > 0">
          <table class="table table-striped">
            <thead>
              <tr>
                <th>
                  <button type="button"
                      class="btn btn-default btn-xs"
                      ng-show="messages.length > 0"
                      ng-click="clearMessages()">
                    <i class="glyphicon glyphicon-remove"></i>
                    <span>Clear Messages</span>
                  </button>
                </th>
                <th>Headers</th>
                <th>Body</th>
              </tr>
            </thead>
            <tbody>
              <tr ng-repeat="message in messages">
                <td>
                  <i class="glyphicon glyphicon-envelope"></i>
                </td>
                <td>
                  <div ng-repeat="(key, value) in message.headers">
                    <strong>{{key}}:</strong> {{value}}
                  </div>
                </td>
                <td>
                  <div ng-repeat="(key, value) in message.body">
                    <strong>{{key}}:</strong> {{value}}
                  </div>
                </td>
              </tr>
            </tbody>
          </table>
        </div>
        <div class="col-md-12" ng-show="messages.length == 0">
          <div class="jumbotron">
            <lead><strong>No messages in the queue.</strong><br />
              Try clicking on the 
              <i class="glyphicon glyphicon-heart-empty"></i>
              button to start receiving messages.</lead>
          </div>
        </div>
      </div>
    </div>
  </div>
</body>
</html>
This is what the layout looks like.
If you noticed above, there are two JavaScript libraries I rely on to implement the controller functionality. These were actually written in CoffeeScript.  The controller code is pretty simple:
uuid = require("uuid")

Ex1Headers =
  principal: "me@me.com"
  event_type: "hello.world"

Ex1Body =
  foo: "bar"
  bar: 123
  foobar: [ "foo", "bar", 123 ]

app = angular.module 'AmqpApp', []

app.controller 'MainCtrl', ($scope) ->

  $scope.id_box = uuid.v4()

  $scope.useSample = ->
    $scope.header_box = JSON.stringify Ex1Headers, undefined, 4
    $scope.body_box = JSON.stringify Ex1Body, undefined, 4

  $scope.useSample()

  $scope.messages = []

  message_handler = (message) ->
    $scope.$apply ->
      $scope.messages.push message

  amqpConnection = new AmqpConnection(message_handler)

  $scope.usingHeartbeat = false

  $scope.heartbeatOff = ->
    amqpConnection.stopHeartbeat()
    $scope.usingHeartbeat = false

  $scope.heartbeatOn = ->
    amqpConnection.startHeartbeat()
    $scope.usingHeartbeat = true

  $scope.clearMessages = ->
    $scope.messages.length = 0

  $scope.publish = ->
    headers = {}

    try
      headers = JSON.parse $scope.header_box
    catch e1
      alert("Invalid Headers input.")
      return

    headers.id = $scope.id_box

    body = null

    try
      bjson = JSON.parse $scope.body_box
      body = bjson if bjson?
    catch e2
      body = { msg: $scope.body_box }

    try
      amqpConnection.publish(body, headers)
      $scope.id_box = uuid.v4()
    catch e3
      alert("Error publishing message: " + e3)

The AMQP Connection class is also not too difficult to follow:
amqp = require("amqp")
uuid = require("uuid")
gui  = require("nw.gui")

class AmqpConnection

  constructor: (@handler) ->
    @connection = amqp.createConnection { 
         host: "localhost", 
         login: "guest", 
         password: "guest" 
    }
    @connection.on "ready", =>
      @queue_name = uuid.v4()
      gui.Window.get().on "close", => @connection.end()
      @connection.queue @queue_name, (q) =>
        @q = q
        @q.bind "#"
        @q.subscribe @receive

  publish: (message, headers) =>
    headers = headers ? {}
    headers.sent = new Date().getTime()
    @connection.publish @queue_name, message, { 
      headers: headers, 
      contentType: "application/json" 
    }

  receive: (msg, headers, deliveryInfo) =>
    headers.received = new Date().getTime()
    message = { headers: headers, body: msg }
    @handler message

  startHeartbeat: (interval) =>
    interval = interval ? 1500
    sendHeartbeat = =>
      @publish { ping: "pong!" }
    @heartbeat_handle = setInterval(sendHeartbeat, interval)

  stopHeartbeat: =>
    if @heartbeat_handle?
      clearInterval(@heartbeat_handle)

window.AmqpConnection = AmqpConnection

One thing you've probably noticed is the use of the Node.js require directive for importing dependencies.  This will play some havoc with RequireJS or AMD/CommonJS loader, but the Node-Webkit site has some discussion on work arounds.

Ok, when the publish button is pressed, you will see something like this:
When the heartbeat is turned on, you will repeatedly get messages:
And if you browse the RabbitMQ Management Console, you will see that our application is connected:
That's it.

You can find all of the source code on Github:  https://github.com/berico-rclayton/Node-Webkit-AMQP-Example.




2 comments:

  1. How would you test the angularjs controllers within a test runner?

    ReplyDelete
    Replies
    1. That's a great question. You can definitely separate the Node code from the Angular, running both in separate contexts. Probably the best strategy is to inject the AmqpConnection as an Angular service; this way you can more logically decouple the AMQP component with the Controller (injecting a mock into the Angular controller for test purposes). I apologize for the example, it's certainly not a real-world solution; typically I manage the structure of my applications with MimosaJS.

      Delete