Introduction & Concept
Building a desktop app from Electron and Django is actually building a web application, which uses Electron as the front-end by a local browser and Django as the backend.
Then, why do we need to build the desktop application instead of web application?🤔
Because:
- desktop app can run offline
- desktop app can access client PC low-level API (e.g. file system 📁)
POC (Proof-Of-Concept)
We will build an simple deskop application using Electron, TypeScript, Webpack and Django.
As shown above,
- User first inputs a text.
- The input is passed to Django web server and return output, which combined the input, date and time and some other text.
- Finally, the output will be shown in the window
0. Prerequisites
Assume that you have installed:
- Node js, v14.19.1
- python, 3.9.10
- virtualenv, 20.13.1
1. Setup Electron
We will use Electron Forge TypeScript + Webpack template to create our desktop application.
- Run the following command in command window:
npx create-electron-app edtwExample --template=typescript-webpack
We named tha example edtw
, whick stands for Electron, Django, TypScript, Webpack
- After running command, you should see the output of the command window:
File Structure:
- Run
npm run start
insidesedtwExample
folder and the following window should be popped up
2. Setup Django
- Create a folder called
python
inedtwExample
folder
mkdir python
cd python
- Create a virtual environment and activate it
virtualenv edtwExampleEnv
edtwExampleEnv\Scripts\activate
- Install Django and Django REST framework (with the version)
pip install django==4.0.3 djangorestframework==3.13.1
- Initiate Django project
django-admin startproject edtwExample
Here is the result file structure:
- Run Django app by the following command
python manage.py runserver
- Open 127.0.0.1:8000 in browser and you should see the following:
3. Start Django app when the electron start (using spawn)
In order to do so, we create a startDjangoServer
method in index.ts
that use spawn
to run django runserver
command
import { spawn } from 'child_process';
const startDjangoServer = () =>
{
const djangoBackend = spawn(`python\\edtwExampleEnv\\Scripts\\python.exe`,
['python\\edtwExample\\manage.py', 'runserver', '--noreload']);
djangoBackend.stdout.on('data', data =>
{
console.log(`stdout:\n${data}`);
});
djangoBackend.stderr.on('data', data =>
{
console.log(`stderr: ${data}`);
});
djangoBackend.on('error', (error) =>
{
console.log(`error: ${error.message}`);
});
djangoBackend.on('close', (code) =>
{
console.log(`child process exited with code ${code}`);
});
djangoBackend.on('message', (message) =>
{
console.log(`message:\n${message}`);
});
return djangoBackend;
}
The following script calls cmd to run a new process with the command python\edtwExampleEnv\Scripts\python.exe
with the arguments ['python\\edtwExample\\manage.py', 'runserver', '--noreload']
.
const djangoBackend = spawn(`python\\edtwExampleEnv\\Scripts\\python.exe`,
['python\\edtwExample\\manage.py', 'runserver', '--noreload']);
The following script log the output of the django process
djangoBackend.stdout.on('data', data =>
{
log.info(`stdout:\n${data}`);
});
djangoBackend.stderr.on('data', data =>
{
log.error(`stderr: ${data}`);
});
djangoBackend.on('error', (error) =>
{
log.error(`error: ${error.message}`);
});
djangoBackend.on('close', (code) =>
{
log.info(`child process exited with code ${code}`);
});
djangoBackend.on('message', (message) =>
{
log.info(`stdout:\n${message}`);
});
We call the startDjangoServer
method in the createWindow
method.
const createWindow = (): void => {
startDjangoServer();
// Create the browser window.
const mainWindow = new BrowserWindow({
height: 600,
width: 800,
});
// and load the index.html of the app.
mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
// Open the DevTools.
mainWindow.webContents.openDevTools();
};
Run
npm run start
and open the task manager, you should be able to see python process
If you close the app window, the python process will stop
Note
The argument --noreload
in ['python\\edtwExample\\manage.py', 'runserver', '--noreload']
MUST BE INCLUDED to prevent django application started twice.
If --noreload
is omitted, you will have 4 python instance running in the background.
Even you close application window, there are 2 python instance left and you can still access django site.
4. Construct API method in Django
- Add
rest_framework
inINSTALLED_APPS
insettings.py
- Running the below command in command window to create an app named
edtwExampleAPI
python manage.py startapp edtwExampleAPI
You should see the below file structure:
- Add
path('', include('edtwExampleAPI.urls')),
inedtwExample\urls.py
- Create
urls.py
under the folderedtwExampleAPI
and paste the following content there
from django.urls import include, path
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register( 'edtwExampleAPI', views.EdtwViewSet, basename='edtwExampleAPI' )
## Wire up our API using automatic URL routing.
## Additionally, we include login URLs for the browsable API.
urlpatterns = [
path('', include(router.urls)),
]
- In
views.py
, copy and paste the following content
from datetime import datetime
from rest_framework import viewsets
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
class EdtwViewSet(viewsets.ViewSet):
## Create your views here.
@action(methods=['GET'], detail=False, name='Get Value from input' )
def get_val_from( self, request ):
input = request.GET[ 'input' ]
return Response( status=status.HTTP_200_OK,
data=f"[{ datetime.now() }] input= { input }, value from Django" )
- Restart Django web server and go to
http://127.0.0.1:8000/edtwExampleAPI/get_val_from/?input=This+is+an+input+text
.
5. Call Django API from Electron
- Copy and paste the following code to
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World!</title>
</head>
<body>
<h1>💖 Hello World!</h1>
<p>Welcome to your Electron application.</p>
<input id="input_text" type="text"></body>
<button id="btn_get_val_from_django" >Get Value From Django</button>
<h2>Output</h2>
<p id="p_output"></p>
</body>
</html>
- Copy and paste the following code to
renderer.ts
import axios from 'axios';
import './index.css';
const btnGetValFromDjango = document.getElementById('btn_get_val_from_django');
btnGetValFromDjango.onclick = async () => {
const res = await axios.get("http://127.0.0.1:8000/edtwExampleAPI/get_val_from/", { params: {
input: ( document.getElementById('input_text') as HTMLInputElement ).value
} });
const result = res.data;
document.getElementById('p_output').innerHTML = result;
};
console.log('👋 This message is being logged by "renderer.js", included via webpack');
- Our logic is finished. In here, there will be 2 errors coming up. Do not worry, I will let you know how to solve it. 😊
If you test the app, it will show the following error.
The above error was due to the content security policy. We will fix it by adding devContentSecurityPolicy
in package.json
and restart the application. (See this for more info.)
"@electron-forge/plugin-webpack",
{
"mainConfig": "./webpack.main.config.js",
"devContentSecurityPolicy": "connect-src 'self' http://127.0.0.1:8000 'unsafe-eval'",
"renderer": {
"config": "./webpack.renderer.config.js",
"entryPoints": [
{
"html": "./src/index.html",
"js": "./src/renderer.ts",
"name": "main_window"
}
]
}
}
- After that, if you try the application again, there will be another error.
This is due to the common CORS policy. We choose the fix introduced in here.
The concept is to replace the header before browser check the origin.
Add the following method in index.ts
const UpsertKeyValue = (obj : any, keyToChange : string, value : string[]) => {
const keyToChangeLower = keyToChange.toLowerCase();
for (const key of Object.keys(obj)) {
if (key.toLowerCase() === keyToChangeLower) {
// Reassign old key
obj[key] = value;
// Done
return;
}
}
// Insert at end instead
obj[keyToChange] = value;
}
Change createWindow
method as follow in index.ts
const createWindow = (): void => {
startDjangoServer();
// Create the browser window.
const mainWindow = new BrowserWindow({
height: 600,
width: 800,
});
mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
(details, callback) => {
const { requestHeaders } = details;
UpsertKeyValue(requestHeaders, 'Access-Control-Allow-Origin', ['*']);
callback({ requestHeaders });
},
);
mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
const { responseHeaders } = details;
UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Origin', ['*']);
UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Headers', ['*']);
callback({
responseHeaders,
});
});
// and load the index.html of the app.
mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
// Open the DevTools.
mainWindow.webContents.openDevTools();
};
- Restart the application and Done! 🎉👏
Source code
You may check for the source code for more information.
Reason for building a Desktop app using Electron and Django
Initially, I would like to build an desktop app and focus on using Python to build backend or business logic so I first search for Python's Desktop frameworks. However, they are either
- Not very user friendly
- Lack of nice-looking UI framework
- Not free of charge
Due to the above 3 reasons, I expected I may need to spend a lot of time to develop if I choose them. (Here is a good ref.)
As I am a web developer, I asked myself, can I use as much as what I already know (e.g. JavaScript & Python) to build a Desktop app?
That is why Electron get into my sight.
-
Electron + Django is a good approach if
- You already have a web app using Django as the backend and JavaScript for the frontend and you would like to convert it to Desktop app
- You want to be more excellent on Django and develop the frontend using your favorite frontend library (e.g. React, Angular or vue e.t.c)
-
It may not be an nice approach if
-
You build a desktop app from nothing. (Electron + Django takes relative more time to set up and you need to maintain 2 programming languages).
For this case, I suggest to use Electron itself combined with TypeScript as there is already existing template to handle this. Also, Electron itself can access PC low-level API to fulfill your need
-
Top comments (0)