๐ก 100 tiny steps to build cross-platform desktop application using Electron/Node.js/C++
MIT License
English | ็ฎไฝไธญๆ | เคนเคฟเคจเฅเคฆเฅ
100 tiny steps to build cross-platform desktop application using Electron/Node.js/C++
It's simple tutorial/guide for absolute beginners to present some tips for
creating desktop application. Unlike @electron/electron-quick-start, which presents the typical hello world
.
This project aims to focus on real-live scenario, where we will try to implement
a complete product (like cross-platform Apple's AirDrop replacement).
Download from GitHub Releases and install it.
To clone and run this repository you'll need Git and Node.js (and yarn) installed on your computer. From your command line:
# clone this repository
git clone https://github.com/maciejczyzewski/airtrash
# go into the repository
cd airtrash
# install dependencies
yarn
# run the app
yarn start
Note: If you're using Linux Bash for Windows, see this guide or use node
from the command prompt.
The macOS users can install airtrash using brew cask
.
brew update && brew cask install airtrash
(nice try, you can't)
Let's begin our journey.
Clone and run for a quick way to see Electron in action. From your command line:
yarn
is strongly recommended instead ofnpm
.
# clone this repository
$ git clone https://github.com/electron/electron-quick-start
# go into the repository
$ cd electron-quick-start
# install dependencies
$ yarn
# run the app
$ yarn start
You should see:
And have this file structure:
.
โโโ LICENSE.md # - no one's bothered
โโโ README.md # - sometimes good to read
โโโ index.html # body: what you see
โโโ main.js # heart: electron window
โโโ package-lock.json # - auto-generated
โโโ package.json # configuration/package manager
โโโ preload.js # soul: application behavior
โโโ renderer.js # - do after rendering
0 directories, 8 files
Our next goal will be to build .dmg
and .app
files with everything packed
up.
Run: $ yarn add electron-builder --dev
Modify package.json:
+ "name": "airtrash",
"scripts": {
"start": "electron .",
+ "pack": "electron-builder --dir",
+ "dist": "electron-builder",
+ "postinstall": "electron-builder install-app-deps"
},
...
+ "build": {
+ "appId": "maciejczyzewski.airtrash",
+ "mac": {
+ "category": "public.app-category.utilities"
+ }
+ },
yarn dist
You should see:
$ electron-builder
โข electron-builder version=21.2.0 os=17.7.0
โข loaded configuration file=package.json ("build" field)
โข writing effective config file=dist/builder-effective-config.yaml
โข packaging platform=darwin arch=x64 electron=7.1.7 appOutDir=dist/mac
โข default Electron icon is used reason=application icon is not set
โข building target=macOS zip arch=x64 file=dist/airtrash-1.0.0-mac.zip
โข building target=DMG arch=x64 file=dist/airtrash-1.0.0.dmg
โข building block map blockMapFile=dist/airtrash-1.0.0.dmg.blockmap
โข building embedded block map file=dist/airtrash-1.0.0-mac.zip
โจ Done in 59.42s.
And have this additional files:
Let's add some popular package (like bootstrap) to understand how to do it.
$ yarn add bootstrap --dev
$ yarn add normalize.css --dev # good practise
$ yarn add popper.js --dev # bootstrap needs this
$ yarn add jquery --dev # and this to be complete
nodeIntegration
in main.js: webPreferences : {
+ nodeIntegration : true,
preload : path.join(__dirname, 'app/preload.js'),
}
<head>
<head>
- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
- <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'">
+ <link rel="stylesheet" href="node_modules/normalize.css/normalize.css" />
+ <link
+ rel="stylesheet"
+ href="node_modules/bootstrap/dist/css/bootstrap.min.css"
+ />
...
</head>
<body>
...
+ <script>
+ window.$ = window.jquery = require('jquery');
+ window.popper = require('popper.js');
+ require('bootstrap');
+ </script>
</body>
</html>
In Electron you can modify the window interface. Let's play with it.
mainWindow = new BrowserWindow({
+ titleBarStyle: 'hiddenInset',
+ width : 625,
+ height : 400,
+ // resizable: false, # user's don't like this option
webPreferences : {
nodeIntegration : true,
preload : path.join(__dirname, 'app/preload.js'),
+ icon : __dirname + '/icon.png'
}
})
titleBarStyle: 'hiddenInset'
, it need to be defined new+ <body style="-webkit-app-region: drag">
Result should be:
Refer to a quick-start Nan Boilerplate for a ready-to-go project that utilizes basic Nan functionality (Node Native Extension).
$ yarn add node-gyp --dev
$ yarn add electron-rebuild --dev # to fix some common problems
"scripts": {
"start": "electron .",
"pack": "electron-builder --dir",
"dist": "electron-builder",
+ "build": "node-gyp build",
+ "configure": "node-gyp configure",
+ "postinstall": "electron-builder install-app-deps && \
+ ./node_modules/.bin/electron-rebuild"
},
...
"build": {
+ "files": [
+ "**/*",
+ "build/Release/*"
+ ],
+ "nodeGypRebuild": true,
+ "asarUnpack": "build/Release/*",
"appId": "maciejczyzewski.airtrash",
"mac": {
"icon": "icon.png",
"category": "public.app-category.utilities"
}
},
{
"targets": [
{
"target_name": "airtrash",
"sources": [
"airtrash.cc",
"src/api.cc",
],
"include_dirs" : [
"<!(node -e \"require('nan')\")"
]
}
],
}
#include "src/api.h"
using v8::FunctionTemplate;
#define NAN_REGISTER(name) \
Nan::Set(target, Nan::New(#name).ToLocalChecked(), \
Nan::GetFunction(Nan::New<FunctionTemplate>(name)).ToLocalChecked());
NAN_MODULE_INIT(InitAll) {
NAN_REGISTER(return_a_string);
}
NODE_MODULE(airtrash, InitAll)
src/api.cc:
#include "api.h"
void return_a_string(const Nan::FunctionCallbackInfo<v8::Value> &args) {
std::string val_example = "haha, just a string ;-)"
args.GetReturnValue().Set(
Nan::New<v8::String>(val_example).ToLocalChecked());
}
src/api.h:
#ifndef NATIVE_EXTENSION_GRAB_H
#define NATIVE_EXTENSION_GRAB_H
#include <nan.h>
NAN_METHOD(return_a_string);
#endif
var NativeExtension = require("bindings")("airtrash");
console.log(NativeExtension.return_a_string());
// => haha, just a string ;-)
Recommended reading: Node.js multithreading: What are Worker Threads and why do they matter?
Run: $ yarn add worker-farm --dev
Create file app/push.js:
var NativeExtension = require('bindings')('airtrash');
module.exports = (input, callback) => {
console.log("PUSH", input.address, input.path)
NativeExtension.push(input.address, input.path)
callback(null, input)
}
const workerFarm = require("worker-farm");
const service_push = workerFarm(require.resolve("./push"));
service_push(data,
function(err, output) {
new Notification(
"Transmission Closed!",
{body : output.path + " from " + output.address})
});
console.log("hello!");
Result should be (random order of lines):
hello!
PUSH 192.168.0.?:9000
<killing service signal>
Transmission Closed!
It should define 3 main functions:
nmap
) through defined ranged of ports for wholeTo be a real P2P, nodes should be propagated (without user action) through network (additionally only parts of files, not whole).
If you are interested in participating in joint development, PR and Forks are welcome!
MIT Copyright (c) Maciej A. Czyzewski