Getting Started with NodeJS and Thrust
I’ve used a lot of cross platform desktop toolkits over the years. I’ve even built some when I worked on Swing and JavaFX at Sun, and I continue development of Amino, an OpenGL toolkit for JavaScript. I know far more about toolkits than I wish. You would think the hardest part of making a good toolkit is the graphics or the widgets or the API. Nope. It’s deployment. Client side Java failed because of deployment. How to actually get the code running on the end user’s computer 100% reliably, no matter what system they have. Deployment on desktop is hard. Perhaps some web technology can help.
Thrust
Today we’re going to play with a new toolkit called Thrust. Thrust is an embeddable web view based on Chromium, similar to Atom-Shell or Node-webkit, but with one big difference. The Chromium renderer runs in a separate process that your app communicates with over a simple JSON based RPC pipe. This one architectural decision makes Thrust far more flexible and reliable.
Since the actual API is over a local connection instead of C bindings, you can use Thrust with the language of your choosing. Bindings already exist for NodeJS, Go, Scala, and Python. The bindings just speak the RPC protocol so you could roll your own bindings if you want. This split makes Thrust far more reliable and flexible than previous Webkit embedding efforts. Though it’s still buggy, I’ve already had far more success with Thrust than I did with Atom-Shell.
For this tutorial I’m going to show you how to use Thrust with NodeJS, but the same principles apply to other language bindings. This tutorial assumes you already have node installed and a text editor available.
A simple app
First create a new directory and node project.
mkdir thrust_tutorial cd thrust_tutorial npm init
ccept all of the defaults for `npm init`.
Now create a minimal `start.html` page that looks like this.
<html> <body> <h1>Greetings, Earthling</h1> </body> </html>
var thrust = require('node-thrust'); var path = require('path'); thrust(function(err, api) { var url = 'file://'+path.resolve(__dirname, 'start.html'); var window = api.window({ root_url: url, size: { width: 640, height: 480, } }); window.show(); window.focus(); });
This launches Thrust with the `start.html` file in it. Notice that you have to use an absolute URL with a `file:` protocol because Thrust acts like a regular web browser. It needs real URLs not just file paths.
Installing Thrust
Now install Thrust and save it to the package.json file. The node bindings will fetch the correct binary parts for your platform automatically.
npm install --save node-thrust
Now run it!
node start.js
You should see something like this:
A real local app in just a few lines. Not bad. If your app has no need for native access (loading files, etc.) then you can stop right now. You have a local page up and running. It can load JavaScript from remote servers, though I’d copy them locally for offline usage.
However, you probably want to do something more than . The advantage of Node is the amazing ecosystem of modules. My personal use case is an Arduino IDE. I want the Node side for compiling code and using the serial port. The web side is for editing code and debugging. That means the webpage side of my app needs to talk to the node side.
Message Passing
Thrust defines a simple message passing protocol between the two halves of the application. This is mostly hidden by the language binding. The node function ‘window.focus()” actually becomes a message sent from the Node side to the Chromium side over an internal pipe. We don’t need to care about how it works, but we do need to pass messages back and forth.
On the browser side add this code to `start.html` to send a message using the `THRUST.remote` object like this:
<script type='text/javascript'> THRUST.remote.listen(function(msg) { console.log("got back a message " + JSON.stringify(msg)); }); THRUST.remote.send({message:"I am going to solve all the world's energy problems."}); </script>
Then receive the message and respond on the Node side with this code:
window.on('remote', function(evt) { console.log("got a message " + JSON.stringify(evt)); window.remote({message:"By blowing it up?"}); });
The messages may be any Javascript object that can be serialized as JSON, so you can't pass functions back and forth, just data.
If you run this code you'll see a bunch of debugging information on the command line including the `console.log` output.
[55723:1216/141201:INFO:remote_bindings.cc(96)] REMOTE_BINDINGS: SendMessage got a message {"message":{"message":"I am going to solve all the world's energy problems."}} [55721:1216/141202:INFO:api.cc(92)] [API] CALL: 1 remote [55721:1216/141202:INFO:thrust_window_binding.cc(94)] ThrustWindow call [remote] [55721:1216/141202:INFO:CONSOLE(7)] "got back a message {"message":"By blowing it up?"}", source: file:///Users/josh/projects/thrust_tutorial/start.html (7)
Notice that both ends of the communication are here; the node and html sides. Thrust automatically redirects console.log from the html side to standard out. I did notice, however, that it doesn't handle the multiple arguments form of console.log, which is why I use `JSON.stringify()`. Unlike in a browser, doing `console.log("some object",obj)` would result in only the some object text, not the structure of the actual object.
Now that the UI can talk to node, it can do almost anything. Save files, talk to databases, poll joysticks, or play music. Let’s build a quick text editor.
Building a text editor
Create a file called `editor.html`.
<html> <head> <script src="http://cdnjs.cloudflare.com/ajax/libs/ace/1.1.3/ace.js" type="text/javascript" charset="utf-8"></script> <script src='http://code.jquery.com/jquery-2.1.1.js'></script> <style type="text/css" media="screen"> #editor { position: absolute; top: 30; right: 0; bottom: 0; left: 0; } </style> </head> <body> <button id='save'>Save</button> <div id="editor">function foo(items) { var x = "All this is syntax highlighted"; return x; }</div> <script> var editor = ace.edit("editor"); editor.setTheme("ace/theme/monokai"); editor.getSession().setMode("ace/mode/javascript"); $('#save').click(function() { THRUST.remote.send({action:'save', content: editor.getValue()}); }); </script> </body> </html>
This page initializes the editor and creates a handler for the save button. When the user presses save it will send the editor contents to the node side. Note the `#editor` css to give it a size. Without a size an Ace editor will shrink to 0.
This is the new Node side code, `editor.js`
var fs = require('fs'); var thrust = require('node-thrust'); thrust(function(err, api) { var url = 'file://'+require('path').resolve(__dirname, 'editor.html'); var window = api.window({ root_url: url, size: { width: 640, height: 480, } }); window.show(); window.focus(); window.on('remote',function(evt) { console.log("got a message" + JSON.stringify(evt)); if(evt.message.action == 'save') return saveFile(evt.message); }); }); function saveFile(msg) { fs.writeFileSync('editor.txt',msg.content); console.log("saved to editor.txt"); }
Run this with `node editor.js` and you will see something like this:
Sweet. A real text editor that can save files.
Things I haven't covered
You can control native menus with Thrust. On Windows or Linux this should be done in app view itself with the various CSS toolkits. On Mac (or Linux with Unity) you will want to use the real menubar. You can do this with the api documented here. It's a pretty simple API but I want to mention something that might bite you. Menus in the menubar will in the order you create them, not the order you add them to the menubar.
Another thing I didn’t cover, since I’m new to it myself, is webviews. Thrust lets you embed a second webpage inside the first, similar to an iFrame but with stricter permissions. This webview is very useful is you need to load untrusted content, such as an RSS reader might do. The webvew is encapsulated so you can run code that might crash in it. This would be useful if you were making, say, an IDE or application editor that must repeatedly run some (possibly buggy) code and then throw it away. I’ll do a future installment on web views.
I also didn't cover is packaging. Thrust doesn’t handle packaging with installers. It gives you an executable and some libraries that you can run from a command line, but you still must build a native app bundle / deb / rpm / msi for each platform you want to support if you want a nicer experience. Fortunately there are other tools to help you do this like InnoSetup and FPM .