DEV Community

Cover image for Electron + Django ( Part 1 ), desktop app integrate JavaScript & Python
Ivan Yu
Ivan Yu

Posted on • Originally published at ivanyu2021.hashnode.dev

Electron + Django ( Part 1 ), desktop app integrate JavaScript & Python

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.

chart.drawio.png

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.

output.gif

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
Enter fullscreen mode Exit fullscreen mode

We named tha example edtw, whick stands for Electron, Django, TypScript, Webpack

  • After running command, you should see the output of the command window:

image.png

File Structure:

image.png

  • Run npm run start insides edtwExample folder and the following window should be popped up image.png

2. Setup Django

  • Create a folder called python in edtwExample folder
mkdir python
cd python
Enter fullscreen mode Exit fullscreen mode

image.png

  • Create a virtual environment and activate it
virtualenv edtwExampleEnv
edtwExampleEnv\Scripts\activate
Enter fullscreen mode Exit fullscreen mode
  • Install Django and Django REST framework (with the version)
pip install django==4.0.3 djangorestframework==3.13.1
Enter fullscreen mode Exit fullscreen mode
  • Initiate Django project
django-admin startproject edtwExample
Enter fullscreen mode Exit fullscreen mode

Here is the result file structure:
image.png

  • Run Django app by the following command
python manage.py runserver
Enter fullscreen mode Exit fullscreen mode
  • Open 127.0.0.1:8000 in browser and you should see the following: image.png

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;
}
Enter fullscreen mode Exit fullscreen mode

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']);
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode
  • Run npm run start and open the task manager, you should be able to see python process
    image.png

  • 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.
image.png

Even you close application window, there are 2 python instance left and you can still access django site.
image.png

4. Construct API method in Django

  • Add rest_framework in INSTALLED_APPS in settings.py

image.png

  • Running the below command in command window to create an app named edtwExampleAPI
python manage.py startapp edtwExampleAPI
Enter fullscreen mode Exit fullscreen mode

You should see the below file structure:

image.png

  • Add path('', include('edtwExampleAPI.urls')), in edtwExample\urls.py

image.png

  • Create urls.py under the folder edtwExampleAPI 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)),
]
Enter fullscreen mode Exit fullscreen mode
  • 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" )
Enter fullscreen mode Exit fullscreen mode
  • Restart Django web server and go to http://127.0.0.1:8000/edtwExampleAPI/get_val_from/?input=This+is+an+input+text.

The following will be shown:
image.png

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>
Enter fullscreen mode Exit fullscreen mode
  • 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');
Enter fullscreen mode Exit fullscreen mode
  • 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.

image.png

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"
            }
        ]
    }
}
Enter fullscreen mode Exit fullscreen mode
  • After that, if you try the application again, there will be another error.

image.png

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode
  • 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)