My quest to build a light app on desktop based on a web view

03/04/21 - Arnaud Duforat

Everyone knows Electron to build some "native" cross-platform applications. In fact, it runs an embedded Chromium. We can see the architecture like this:

We obtain a web page interpreted by an embedded browser:

source: Electron official documentation

But, you expect it, it is fat.

So what are the alternatives?

Native application from the Desktop community

Some examples:

  • Gtk
  • JavaFX
  • Qt

The Game Design community

An example: Haxeui. It can target Heaps that can run on Hashlink VM or compile and run on all operating systems.

The Frontend & Mobile community

React native can target Windows & MacOS with React Native for Windows. React native transpiles javascript components to native components depending on the operating system. With the library vue-native you can also develop with Vue.js. A webview component is available if needed.

Nodegui that uses Qt under the hood to enable the developer to use React or Vue.js to develop native applications.

The frontend community and the Webview application

Electron, Neutralinojs and NW.js can be used to develop web applications embedded in webviews. Electron and NW.js are based on an embedded Node and Chromium, Neutralinojs is based on a C++ web server and a Webkit web client.

Source: official Neutralinojs documentation

Neutralino offers a lightweight alternative for Electron and NW.js:

Same sample app memory consumptionLinuxWindows
Electron~ 62 - 65 MB~ 45 - 50 MB
Neutralinojs~ 8 - 9 MB~ 6 - 7 MB
NW.js~ 40 - 42 MB~ 40 - 45 MB
Source: official Neutralinojs documentation

Trying Neutralinojs

npm install -g @neutralinojs/neu
neu create RichTextWindows
cd RichTextWindows
neu run

Neutralinojs initially loads the contents of app/index.html file (this is specified in neutralino.config.json and app directory has been replaced by resources directory in 1.9.0 version). Create an app directory and add an index.html:

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>NeutralinoJs</title> </head> <body> <div id="neutralinoapp"> <h1>Hello <span id="name"></span></h1> </div> <script src="/neutralino.js"></script> <script src="/assets/app.js"></script> </body> </html>

And an app.js:

var getUsername = function () { var key = NL_OS == 'Windows' ? 'USERNAME' : 'USER'; Neutralino.os.getEnvar(key, function(data) { document.getElementById('name').innerText = data.value; }, function () { //handle the error } ); } Neutralino.init({ load: function () { getUsername(); } });

And run (with the live-reload):

neu listen

It doesn't work... It seems that we can't reach the server port. This issue shows that we have to run the application as an administrator... For security reasons, this does not suit me.

A better architecture for a webview

We could bundle the webview into the application to remove the network call if the webview engine is in the same programming language as the application bundle:

We could do it in JavaFX. Let's start from one of my sample projects: JavaFXDesign. After a renaming and removing some useless source code in our case (items, table in home...), we add javafx-web dependency to access to the WebView component. Then we create a WebViewController and a WebView.fxml. Note that since 2015 JavaFX controllers doesn't need to implements Initializable (see: Initializable doc). To see more details, the project is here: JavaFXWebView. To embed a local web page from the classpath see this commit. To embed a Vue.js SPA see that.

Now how to expose a Java object to Javascript in the WebView?

We add a change listener to the WebEngine that inject the Java object to the window object of the Javascript engine.

The translation in code:

A POJO CallFromJs:

package webview; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class CallFromJs { private static final Logger logger = LoggerFactory.getLogger(CallFromJs.class); public void doIt() { logger.debug("CallFromJs::doIt() called"); } }

And the addition of the listener on the WebEngine:

engine.getLoadWorker().stateProperty().addListener( (ObservableValue<? extends Worker.State> observable, Worker.State oldState, Worker.State newState) -> { if (newState != Worker.State.SUCCEEDED) { return; } JSObject window = (JSObject) engine.executeScript("window"); window.setMember("callFromJs", new CallFromJs()); } );

The result:

And we see that in the Eclipse console when we click on the button:

13:52:37.666 [JavaFX Application Thread] DEBUG webview.CallFromJs - CallFromJs::doIt() called

One last word

We demonstrate here that we can get rid of an embedded web server and quickly develop a light desktop application that embed a webview. This opens up the opportunity for us to use javascript libraries for parts of the application. For the impatients, you can find the repository here: JavaFXWebView.