This tutorial is out of date and no longer maintained.
Development folks work tirelessly to make building programs as easy as possible. The JavaScript, Web, and Mobile app developers communities have increased drastically since Node and Cordova were introduced. Developers who had web design skills could, with less effort, roll out a server using JavaScript for their applications, through the help of Node.js.
Mobile lovers can with the help of Cordova now build rich hybrid apps using just JavaScript. Today, although it is old news, I am excited to share the ability to use JavaScript to build desktop standalone applications.
Node WebKit normally written: “node-webkit” or “NW.js” is an app runtime based on Node.js and Chromium and enables us to develop OS native apps using just HTML, CSS, and JavaScript.
Simply put, Node WebKit just helps you utilize your skill as a web developer to build a native application that runs comfortably on Mac, Windows, and Linux with just a grunt/gulp (if preferred) build command.
This article concentrates a lot more on using Node WebKit, but in order to make things more interesting, we will be including other amazing solutions and they will include:
Furthermore, the application has three sections:
The web section will not be covered here, but it will serve as a test platform but don’t worry, the code will be provided.
Level: Intermediate (Knowledge of MEAN is required)
We need to grab node-webkit
and other dependencies for our application. Fortunately, there are frameworks that make workflow easy and we will be using one of them to scaffold our application and concentrate more on the implementation.
Yo and Slush are popular generators and any of these will work. I am going to be using Slush, but feel free to use Yo if you prefer to. To install Slush make sure you have node and npm installed and run
- npm install -g slush gulp bower slush-wean
The command will install the following globally on our system.
slush-wean
: the generator for Node WebKitbower
: for frontend dependenciesJust like YO, make your directory and scaffold your app using:
- mkdir scotch-chat
- cd scotch-chat
- slush wean
Running the below command will give us a glance of what we have been waiting for:
- gulp run
The image shows our app loading. The author of the generator was generous enough to provide a nice template with a simple loading animation. To look cooler, I replaced the loading text with Scotch’s logo.
If you are not comfortable with Slush automating things you can head right to Node WebKit on GitHub.
Now that we have set up our app, though empty, we will give it a break and prepare our server now.
The server basically consists of our model, routes, and socket events. We will keep it as simple as possible and you can feel free to extend the app as instructed at the end of the article.
Setup a folder in your PC at your favorite directory, but make sure the folder content looks like the below:
|- public
|- index.html
|- server.js
|- package.json
In the package.json
file located on your root directory, create a JSON file to describe your application and include the application’s dependencies.
{
"name": "scotch-chat",
"main": "server.js",
"dependencies": {
"mongoose": "latest",
"morgan": "latest",
"socket.io": "latest"
}
}
That will do. It is just a minimal setup and we are keeping things simple and short. Run npm install
on the directory root to install the specified dependencies.
- npm install
It is time to get our hands dirty! The first thing is to set up global variables in server.js
which will hold the application’s dependencies that are already installed.
// Import all our dependencies
var express = require('express');
var mongoose = require('mongoose');
var app = express();
var server = require('http').Server(app);
var io = require('socket.io')(server);
Ok, I didn’t keep to my word. The variables are not only holding the dependencies, but some are configuring it for use.
To serve static files, express exposes a method to help configure the static files folder. It is simple:
...
// tell express where to serve static files from
app.use(express.static(__dirname + '/public'));
Next up is to create a connection to our database. I am working with a local MongoDB which obviously is optional as you can find it’s hosted by Mongo databases. Mongoose is a node module that exposes amazing API which makes working with MongoDB a lot much easier.
...
mongoose.connect("mongodb://127.0.0.1:27017/scotch-chat");
With Mongoose we can now create our database schema and model. We also need to allow CORS in the application as we will be accessing it from a different domain.
...
// create a schema for chat
var ChatSchema = mongoose.Schema({
created: Date,
content: String,
username: String,
room: String
});
// create a model from the chat schema
var Chat = mongoose.model('Chat', ChatSchema);
// allow CORS
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-type,Accept,X-Access-Token,X-Key');
if (req.method == 'OPTIONS') {
res.status(200).end();
} else {
next();
}
});
Our server will have three routes in it. A route to serve the index file, another to set up chat data, and the last to serve chat messages filtered by room names:
/*||||||||||||||||||||||ROUTES|||||||||||||||||||||||||*/
// route for our index file
app.get('/', function(req, res) {
//send the index.html in our public directory
res.sendfile('index.html');
});
//This route is simply run only on first launch just to generate some chat history
app.post('/setup', function(req, res) {
//Array of chat data. Each object properties must match the schema object properties
var chatData = [{
created: new Date(),
content: 'Hi',
username: 'Chris',
room: 'php'
}, {
created: new Date(),
content: 'Hello',
username: 'Obinna',
room: 'laravel'
}, {
created: new Date(),
content: 'Ait',
username: 'Bill',
room: 'angular'
}, {
created: new Date(),
content: 'Amazing room',
username: 'Patience',
room: 'socet.io'
}];
//Loop through each of the chat data and insert into the database
for (var c = 0; c < chatData.length; c++) {
//Create an instance of the chat model
var newChat = new Chat(chatData[c]);
//Call save to insert the chat
newChat.save(function(err, savedChat) {
console.log(savedChat);
});
}
//Send a resoponse so the serve would not get stuck
res.send('created');
});
//This route produces a list of chat as filterd by 'room' query
app.get('/msg', function(req, res) {
//Find
Chat.find({
'room': req.query.room.toLowerCase()
}).exec(function(err, msgs) {
//Send
res.json(msgs);
});
});
/*||||||||||||||||||END ROUTES|||||||||||||||||||||*/
The first route I believe is easy enough. It will just send our index.html
file to our users.
The second /setup
is meant to be hit just once and at the initial launch of the application. It is optional if you don’t need some test data. It basically creates an array of chat messages (which matches the schema), loops through them, and inserts them into the database.
The third route /msg
is responsible for fetching chat history filtered with room names and returned as an array of JSON objects.
The most important part of our server is the real-time logic. Keeping in mind that we are working towards producing a simple application, our logic will be comprehensively minimal. Sequentially, we need to:
Therefore:
/*||||||||||||||||SOCKET|||||||||||||||||||||||*/
//Listen for connection
io.on('connection', function(socket) {
//Globals
var defaultRoom = 'general';
var rooms = ["General", "angular", "socket.io", "express", "node", "mongo", "PHP", "laravel"];
//Emit the rooms array
socket.emit('setup', {
rooms: rooms
});
//Listens for new user
socket.on('new user', function(data) {
data.room = defaultRoom;
//New user joins the default room
socket.join(defaultRoom);
//Tell all those in the room that a new user joined
io.in(defaultRoom).emit('user joined', data);
});
//Listens for switch room
socket.on('switch room', function(data) {
//Handles joining and leaving rooms
//console.log(data);
socket.leave(data.oldRoom);
socket.join(data.newRoom);
io.in(data.oldRoom).emit('user left', data);
io.in(data.newRoom).emit('user joined', data);
});
//Listens for a new chat message
socket.on('new message', function(data) {
//Create message
var newMsg = new Chat({
username: data.username,
content: data.message,
room: data.room.toLowerCase(),
created: new Date()
});
//Save it to database
newMsg.save(function(err, msg){
//Send message to those connected in the room
io.in(msg.room).emit('message created', msg);
});
});
});
/*||||||||||||||||||||END SOCKETS||||||||||||||||||*/
Then the traditional server start:
server.listen(2015);
console.log('It\'s going down in 2015');
Fill the index.html
with any HTML that suits you and run node server.js
. localhost:2015
will give you the content of your HTML.
Time to dig up what we left to create our server which is running currently. This section is quite easy as it just requires your everyday knowledge of HTML, CSS, JS, and Angular.
We don’t need to create any! I guess that was the inspiration for generators. The first file you might want to inspect is the package.json
.
Node WebKit requires, basically, two major files to run:
index.html
)package.json
to tell it where the entry point is locatedpackage.json
has the basic content we are used to, except that its main is the location of the index.html
, and it has a set of configurations under "window":
from which we define all the properties of the app’s window including icons, sizes, toolbar, frame, etc.
Unlike the server, we will be using bower to load our dependencies as it is a client application. Update your bower.json
dependencies to:
"dependencies": {
"angular": "^1.3.13",
"angular-material" : "^0.10.0",
"angular-socket-io" : "^0.7.0",
"angular-material-icons":"^0.5.0",
"animate.css":"^3.0.0"
}
For a shortcut, just run the following command:
- bower install --save angular angular-material angular-socket-io angular-material-icons animate.css
Now that we have our frontend dependencies, we can update our views/index.ejs
to:
<html><head>
<title>scotch-chat</title>
<link rel="stylesheet" href="css/app.css">
<link rel="stylesheet" href="css/animate.css">
<link rel="stylesheet" href="libs/angular-material/angular-material.css">
<script src="libs/angular/angular.js"></script>
<script src="http://localhost:2015/socket.io/socket.io.js"></script>
<script type="text/javascript" src="libs/angular-animate/angular-animate.js"></script>
<script type="text/javascript" src="libs/angular-aria/angular-aria.js"></script>
<script type="text/javascript" src="libs/angular-material/angular-material.js"></script>
<script type="text/javascript" src="libs/angular-socket-io/socket.js"></script>
<script type="text/javascript" src="libs/angular-material-icons/angular-material-icons.js"></script>
<script src="js/app.js"></script>
</head>
<body ng-controller="MainCtrl" ng-init="usernameModal()">
<md-content>
<section>
<md-list>
<md-subheader class="md-primary header">Room: {{room}} <span align="right">Userame: {{username}} </span> </md-subheader>
<md-whiteframe ng-repeat="m in messages" class="md-whiteframe-z2 message" layout layout-align="center center">
<md-list-item class="md-3-line">
<img ng-src="img/user.png" class="md-avatar" alt="User" />
<div class="md-list-item-text">
<h3>{{ m.username }}</h3>
<p>{{m.content}}</p>
</div>
</md-list-item>
</md-whiteframe>
</md-list>
</section>
<div class="footer">
<md-input-container>
<label>Message</label>
<textarea ng-model="message" columns="1" md-maxlength="100" ng-enter="send(message)"></textarea>
</md-input-container>
</div>
</md-content>
</body>
</html>
We included all our dependencies and custom files (app.css and app.js). Things to note:
ng-repeat
and rendering its values to the browserENTER
key is pressedinit
, the user is asked for a preferred usernameThe main part of this section is the app.js
file. It creates services to interact with the Node WebKit GUI, a directive to handle the ENTER
keypress and the controllers (main and dialog).
//Load angular
var app = angular.module('scotch-chat', ['ngMaterial', 'ngAnimate', 'ngMdIcons', 'btford.socket-io']);
//Set our server url
var serverBaseUrl = 'http://localhost:2015';
//Services to interact with nodewebkit GUI and Window
app.factory('GUI', function () {
//Return nw.gui
return require('nw.gui');
});
app.factory('Window', function (GUI) {
return GUI.Window.get();
});
//Service to interact with the socket library
app.factory('socket', function (socketFactory) {
var myIoSocket = io.connect(serverBaseUrl);
var socket = socketFactory({
ioSocket: myIoSocket
});
return socket;
});
Next up, we create three Angular services. The first service helps us get that Node WebKit GUI object, the second returns its Window property, and the third bootstraps Socket.io with the base URL.
//ng-enter directive
app.directive('ngEnter', function () {
return function (scope, element, attrs) {
element.bind("keydown keypress", function (event) {
if (event.which === 13) {
scope.$apply(function () {
scope.$eval(attrs.ngEnter);
});
event.preventDefault();
}
});
};
});
The above snippet is one of my favorites ever since I have been using Angular. It binds an event to the ENTER
key, which thereby an event can be triggered when the key is pressed.
Finally, with the app.js
is the almighty controller. We need to break things down to ease understanding as we did in our server.js
. The controller is expected to:
ENTER
key.With our objectives defined let us code:
//Our Controller
app.controller('MainCtrl', function ($scope, Window, GUI, $mdDialog, socket, $http){
//Menu setup
//Modal setup
//listen for new message
//Notify server of the new message
});
That is our controller’s skeleton with all of its dependencies. As you can see, it has four internal comments which is serving as a placeholder for our codes as defined in the objectives. So let’s pick on the menu.
//Global Scope
$scope.messages = [];
$scope.room = "";
//Build the window menu for our app using the GUI and Window service
var windowMenu = new GUI.Menu({
type: 'menubar'
});
var roomsMenu = new GUI.Menu();
windowMenu.append(new GUI.MenuItem({
label: 'Rooms',
submenu: roomsMenu
}));
windowMenu.append(new GUI.MenuItem({
label: 'Exit',
click: function () {
Window.close()
}
}));
We simply created instances of the menu and appended some menu (Rooms and Exit) to it. The rooms menu is expected to serve as a drop-down and so we have to ask the server for available rooms and append it to the rooms menu:
//Listen for the setup event and create rooms
socket.on('setup', function (data) {
var rooms = data.rooms;
for (var r = 0; r < rooms.length; r++) {
//Loop and append room to the window room menu
handleRoomSubMenu(r);
}
//Handle creation of room
function handleRoomSubMenu(r) {
var clickedRoom = rooms[r];
//Append each room to the menu
roomsMenu.append(new GUI.MenuItem({
label: clickedRoom.toUpperCase(),
click: function () {
//What happens on clicking the rooms? Swtich room.
$scope.room = clickedRoom.toUpperCase();
//Notify the server that the user changed his room
socket.emit('switch room', {
newRoom: clickedRoom,
username: $scope.username
});
//Fetch the new rooms messages
$http.get(serverBaseUrl + '/msg?room=' + clickedRoom).success(function (msgs) {
$scope.messages = msgs;
});
}
}));
}
//Attach menu
GUI.Window.get().menu = windowMenu;
});
The above code with the help of a function, loops through an array of rooms when they are available from the server and then append them to the rooms menu. With that, Objective #1 is completed.
Our second objective is to ask the user for username using angular material modal.
$scope.usernameModal = function (ev) {
//Launch Modal to get username
$mdDialog.show({
controller: UsernameDialogController,
templateUrl: 'partials/username.tmpl.html',
parent: angular.element(document.body),
targetEvent: ev,
})
.then(function (answer) {
//Set username with the value returned from the modal
$scope.username = answer;
//Tell the server there is a new user
socket.emit('new user', {
username: answer
});
//Set room to general;
$scope.room = 'GENERAL';
//Fetch chat messages in GENERAL
$http.get(serverBaseUrl + '/msg?room=' + $scope.room).success(function (msgs) {
$scope.messages = msgs;
});
}, function () {
Window.close();
});
};
As specified in the HTML, on init, the usernameModal
is called. It uses the mdDialog
service to get username of a joining user and if that is successful it will assign the username entered to a binding scope, notify the server about that activity and then push the user to the default (GENERAL) room. If it is not successful we close the app. Objective #2 completed!
//Listen for new messages (Objective 3)
socket.on('message created', function (data) {
//Push to new message to our $scope.messages
$scope.messages.push(data);
//Empty the textarea
$scope.message = "";
});
//Send a new message (Objective 4)
$scope.send = function (msg) {
//Notify the server that there is a new message with the message as packet
socket.emit('new message', {
room: $scope.room,
message: msg,
username: $scope.username
});
};
The third, and the last, objective is simple. #3 just listens for messages and if any push it to the array of existing messages and #4 notifies the server of new messages when they are created. At the end of app.js
, we create a function to serve as the controller for the Modal:
//Dialog controller
function UsernameDialogController($scope, $mdDialog) {
$scope.answer = function (answer) {
$mdDialog.hide(answer);
};
}
To fix some ugly looks, update the app.css
.
body {
background: #fafafa !important;
}
.footer {
background: #fff;
position: fixed;
left: 0px;
bottom: 0px;
width: 100%;
}
.message.ng-enter {
-webkit-animation: zoomIn 1s;
-ms-animation: zoomIn 1s;
animation: zoomIn 1s;
}
Note the last style. We are using ngAnimate
and animate.css
to create a pretty animation for our messages.
I already wrote on how you can play with this concept here.
I can guess what you are worried about after looking at the image! The address bar, right? This is where the window
configuration in the package.json
comes in. Just change "toolbar": true
to "toolbar": false
.
I also set my icon to "icon": "app/public/img/scotch.png"
to change the window icon to the Scotch logo. We can also add notification once there is a new message:
var options = {
body: data.content
};
var notification = new Notification("Message from: "+data.username, options);
notification.onshow = function () {
// auto close after 1 second
setTimeout(function () {
notification.close();
}, 2000);
}
And even more fun…
I suggest you test the application by downloading the web client from Git Hub. Run the server, then the web client, and then the app. Start sending messages from both the app and the web client and watch them appear in real-time if you are sending them in the same room.
If you want to challenge yourself further, you can try to add the following to our app
gulp deploy --{{platform}}
eg: gulp deploy --mac
. * etc…I am glad we made it to the end. Node WebKit is an amazing concept. Join the community and make building apps easier. Hope you had a lot of scotch today and that I made someone smile…
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.