An Electron is a framework to create native desktop applications for Windows, MacOS, and Linux. And what is the wow part in it, you can use vanilla javascript or any other javascript framework for building UI.
First years in my software craftsmanship started with Delphi 7. It was amazing, it was time when the internet was semi-empty. It was hard to find examples, ask help, basically, you were on your own. Ohh good old times, I wouldn’t change that experience, but I wouldn’t want to do that again.
Time goes on, everything evolves, new technologies come in play. Time to time, I stop and look at what has been changed. And that is so cool, always there is something new to look into. To change my own biases, yes painful, but the world isn’t stuck in time warp.
Ladies and gentlemen I bring to you my experience with Electron.
An Electron is a framework to create native desktop applications for Windows, MacOS, and Linux. And what is the wow part in it, you can use vanilla javascript or any other javascript framework for building UI.
This is amazing, you can be a web app developer and by reusing the same skillset you can build a desktop app.
If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.### The project
Let us create an empty npm project
npm init --yes
Add Electron stuff and start command
npm i -D electron
Add start script inside package.json
"start":"electron ."
{
"name": "electron-demo",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"start": "electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Kristaps Vītoliņš",
"license": "MIT",
"devDependencies": {
"electron": "^4.0.6"
}
}
If we execute the npm start, we should get a popup error from Electron. That is ok, this only means that Electron is alive and we don’t know how to boot it.
Before we dive into coding, it is important to understand the basics of Electrons architecture.
If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.> If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.> If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.
My oversimplified explanation would be. There is a main process which creates a window and that window is a Chromium. Where Chromium is a process by itself aka renderer.
Starting from the 1 June of the year 2017, Electron supports Typescript. Good, let’s use it.
npm i -D typescript
npm i -D tslint
npm i -D prettier
Add tslint.json
in the project root
{
"extends": "tslint:recommended",
"rules": {
"max-line-length": {
"options": [
120
]
},
"new-parens": true,
"no-arg": true,
"no-bitwise": true,
"no-conditional-assignment": true,
"no-consecutive-blank-lines": false
},
"jsRules": {
"max-line-length": {
"options": [
120
]
}
}
}
Add tsconfig.json
in the root of the project
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5"
},
"exclude": [
"node_modules"
],
"compileOnSave": false,
"buildOnSave": false
}
For typescript compilation, tsc could be used, but as the end game is to use React and manipulate with templates. Webpack is the way to go this time.
Setup for the web pack
npm i -D webpack webpack-cli
npm i -D html-webpack-plugin
npm i -D @babel/cli @babel/core @babel/preset-env babel-loader @babel/plugin-proposal-class-properties @babel/plugin-transform-arrow-functions
npm i -D @babel/preset-typescript
Add .babel.rc
in the root of the project
{
"presets": [
"@babel/env",
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-arrow-functions"
]
}
So far so good, now let us create a main process of Electron and say hello to Mom!
Create a folder src
Add main.ts
file in src
folder
const url = require("url");
const path = require("path");
import { app, BrowserWindow } from "electron";
let window: BrowserWindow | null;
const createWindow = () => {
window = new BrowserWindow({ width: 800, height: 600 });
window.loadURL(
url.format({
pathname: path.join(__dirname, "index.html"),
protocol: "file:",
slashes: true
})
);
window.on("closed", () => {
window = null;
});
};
app.on("ready", createWindow);
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (window === null) {
createWindow();
}
});
line 8
creating a window and loading index.html
into window aka Chromium.
Add index.html
to the src
folder
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hi mom</title>
</head>
<body>
<h1>Hi mom!</h1>
</body>
</html>
Add webpack.config.js
in the root. Instruction for the web pack how to handle Electrons main process build. Important target:"electron-main"
const path = require("path");
const HtmlWebPackPlugin = require("html-webpack-plugin");
const htmlPlugin = new HtmlWebPackPlugin({
template: "./src/index.html",
filename: "./index.html",
inject: false
});
const config = {
target: "electron-main",
devtool: "source-map",
entry: "./src/main.ts",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
},
resolve: {
extensions: [".ts", ".tsx", ".js"]
},
node: {
__dirname: false,
__filename: false
},
plugins: [htmlPlugin]
};
module.exports = (env, argv) => {
return config;
};
Adjust a bit package.json
{
"name": "electron-demo",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"build": "webpack --mode development",
"start": "electron ./dist/main.js"
},
"keywords": [],
"author": "Kristaps Vītoliņš",
"license": "MIT",
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/core": "^7.3.4",
"@babel/plugin-proposal-class-properties": "^7.3.4",
"@babel/plugin-transform-arrow-functions": "^7.2.0",
"@babel/preset-env": "^7.3.4",
"@babel/preset-typescript": "^7.3.3",
"babel-loader": "^8.0.5",
"electron": "^4.0.6",
"html-webpack-plugin": "^3.2.0",
"prettier": "^1.16.4",
"tslint": "^5.13.1",
"typescript": "^3.3.3333",
"webpack": "^4.29.6",
"webpack-cli": "^3.2.3"
}
}
Execute commands
npm run build
npm run start
And here it is. A fully functional desktop application. Which is saying hi to mom.
And this is actually the place where your front end developer skills kick in. As it is a Chromium you can use any kind of frontend technology, React, Vue, Angular, plain javascript.
React will live in Electrons renderer process for that reason we will have to create a separate web pack build configuration. And teach babel to use react loader
npm i -D @babel/preset-react
npm i -S react react-dom
npm i -D @types/react @types/react-dom
Adjust.babelrc
{
"presets": [
"@babel/env",
"@babel/preset-typescript",
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-arrow-functions"
]
}
Adjust webpack.config.js
by removing the responsibility of index.html
template generation. It is the responsibility of the renderer build process.
const path = require("path");
const config = {
target: "electron-main",
devtool: "source-map",
entry: "./src/main.ts",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
},
resolve: {
extensions: [".ts", ".tsx", ".js"]
},
node: {
__dirname: false,
__filename: false
}
};
module.exports = (env, argv) => {
return config;
};
Add new web pack config file webpack.react.config.js
. This configuration is responsible for compiling react stuff and making sure that compiled result is injected insideindex.html
const path = require("path");
const HtmlWebPackPlugin = require("html-webpack-plugin");
const htmlPlugin = new HtmlWebPackPlugin({
template: "./src/index.html",
filename: "./index.html"
});
const config = {
target: "electron-renderer",
devtool: "source-map",
entry: "./src/app/renderer.tsx",
output: {
filename: "renderer.js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
},
resolve: {
extensions: [".ts", ".tsx", ".js"]
},
plugins: [htmlPlugin]
};
module.exports = (env, argv) => {
return config;
};
Adjust index.html
so that it contains a container where React can place its component.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hi mom</title>
</head>
<body>
<div id="renderer"></div>
</body>
</html>
Create a folder app
inside src
and create renderer.tsx
import * as ReactDOM from 'react-dom';
import * as React from 'react';
import {Dashboard} from "./components/Dashboard";
ReactDOM.render(<Dashboard />, document.getElementById('renderer'));
Now let’s say Hello mom again, only now we will use a React to do so.
Create a folder components
inside app
and create Dashboard.tsx
import * as React from 'react';
export const Dashboard = () => {
return <div>Hello Mom!</div>;
};
Adjust the package.json
and add a new command so we can compile the renderer.
{
"name": "electron-demo",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"build:react": "webpack --mode development --config webpack.react.config.js",
"build": "webpack --mode development",
"start": "electron ./dist/main.js"
},
"keywords": [],
"author": "Kristaps Vītoliņš",
"license": "MIT",
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/core": "^7.3.4",
"@babel/plugin-proposal-class-properties": "^7.3.4",
"@babel/plugin-transform-arrow-functions": "^7.2.0",
"@babel/preset-env": "^7.3.4",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.3.3",
"@types/react": "^16.8.6",
"@types/react-dom": "^16.8.2",
"babel-loader": "^8.0.5",
"electron": "^4.0.6",
"html-webpack-plugin": "^3.2.0",
"prettier": "^1.16.4",
"tslint": "^5.13.1",
"typescript": "^3.3.3333",
"webpack": "^4.29.6",
"webpack-cli": "^3.2.3"
},
"dependencies": {
"react": "^16.8.3",
"react-dom": "^16.8.3"
}
}
npm run build:react
npm run build
npm run start
Now, this is something I don’t see every day. A native desktop app running React inside of it. Well maybe I do, I just don’t know it as Electron is a popular framework and is used all over the place. For example, Visual Studio code, try to guess what is powering it ;).
A blog post by Rui Figueiredo ignited my interest in Electron.
Electron using C#. That is an interesting synergy going on here. And as it is C# core, it is cross-platform as well.
Cross-platform desktop app powered by Electron using React for UI and extended functionality by C# goodness. And now not only you can use your frontend skills, but backend as well.
Install new package for npm project
npm i -D electron-cgi
Adjust main.ts
for a test run, we will send Mom to C# console app and it will return Hello Mom back. And we will console log that.
const { ConnectionBuilder } = require("electron-cgi");
...
let connection = new ConnectionBuilder()
.connectTo("dotnet", "run", "--project", "./core/Core")
.build();
connection.onDisconnect = () => {
console.log("lost");
};
connection.send("greeting", "Mom", (response: any) => {
console.log(response);
connection.close();
});
Create a simple dotnet C# core console app. Add ElectronCgi.DotNet
nuget.
using ElectronCgi.DotNet;
namespace Core
{
class Program
{
static void Main(string[] args)
{
var connection = new ConnectionBuilder()
.WithLogging()
.Build();
connection.On<string, string>("greeting", name => "Hello " + name);
connection.Listen();
}
}
}
npm run build
npm run start
Amazing isn’t it? Rui did go an extra mile and added this to C# as well
connection.OnAsync();
Now we are talking serious stuff. Imagen what possibilities this opens? Async communication to a database, to Rest API, to Queues, all nice packages for clouds, Amazon, Azure etc. All the good stuff from C# at your fingertips.
And it is not limited only to sending strings, it can be strongly typed object in C#.
Here is how
If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.> If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.> If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.> If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application.
Heavy use of stdout. Which isn’t necessarily something bad, for example,php.exe
and the frameworks which surround it ;).
I generally like the approach, clean, smart and innovating. I do endorse read a full blog post of Rui.
Let’s make this even interesting, send the message to React from C#.
For that make changes in renderer by changing the Dashboard functional component to a component with the state.
import * as React from "react";
import { ipcRenderer } from "electron";
interface IState {
message: string;
}
export class Dashboard extends React.Component<{}, IState> {
public state: IState = {
message: ""
};
public componentDidMount(): void {
ipcRenderer.on("greeting", this.onMessage);
}
public componentWillUnmount(): void {
ipcRenderer.removeAllListeners("greeting");
}
public render(): React.ReactNode {
return <div>{this.state.message}</div>;
}
private onMessage = (event: any, message: string) => {
this.setState({ message: message });
};
}
What is going there? We subscribe to the channel greeging
and upon receiving a message from the main process we put the message into the state. And from that point React takes ower, notices the state changes and renders the message.
Changes in the main process by sending received message from C# to React
const url = require("url");
const path = require("path");
const { ConnectionBuilder } = require("electron-cgi");
import { app, BrowserWindow } from "electron";
...
connection.send("greeting", "Mom from C#", (response: any) => {
window.webContents.send("greeting", response);
connection.close();
});
Add package
npm i -D electron-packager
Adjust the package.json
file by adding package-win
command and point main
to dist
folder file main.js
.
Tutorial for all platform build commands here.
{
"main": "dist/main.js",
"scripts": {
"package-win": "electron-packager . electron-demo --overwrite --asar=true --platform=win32 --arch=ia32 --icon=assets/icons/win/icon.ico --prune=true --out=release-builds --version-string.CompanyName=CE --version-string.FileDescription=CE --version-string.ProductName=\"Electron-demo\""
}
}
npm run package-win
Copy C# folder “core
” into “release-builds\electron-demo-win32-ia32
” and run the electron-demo.exe
This journey of mine from Delphi 7 to React, Electron, C# is awesome. As I said before, nothing is stuck in time, every day new technologies pop up which makes us better and breaks our personal biases.
To stay open minded is our biggest challenge.
Sourcecode of demo in GitHub
#reactjs #c-sharp #web-development