电子 + django(第2部分),将其包装到生产中
#python #django #typescript #electron

1.简介和POC

我们如何将电子应用包装与django打包?如果您已经完成阅读"Electron + Django, desktop app integrate JavaScript & Python"

,您可能会更渴望知道答案。

在此博客中,我想详细解释包装过程,并讨论您在此过程中可能需要注意的内容。 ð

让我们首先向您展示最终结果:
6.-Package_output.mp4.gif

在视频中,我们可以看到:

  • 通过单击EXE文件打开应用
  • 测试应用程序是否有效
  • 在任务管理器中,您可以看到有5个运行 image.png

顶部的一个是python应用程序,剩余是电子应用

2.先决条件

我们将基于此example,以显示包装应用程序的步骤。您可以关注README首先构建项目。

3.软件包django应用程序

我们将使用pyinstaller包装Django应用程序。

  • 激活环境并安装pyinstaller
pip install pyinstaller
  • edtwExample下添加settings文件夹
cd python\edtwExample\edtwExample
mkdir settings
  • settings.py文件复制到文件夹中,然后将重命名为dev.pyprod.py,删除原始index.ts
copy settings.py settings\dev.py
copy settings.py settings\prod.py
del settings.py
  • 更改prod.py中的以下配置6
    • debug,True> False
    • washe_host,[]> [ '127.0.0.1', 'localhost' ]
    • INSTALLED_APPS中添加'edtwExampleAPI'
    • 数据库,BASE_DIR / 'db.sqlite3'> BASE_DIR.parent / 'db.sqlite3'
## Django production configuration
DEBUG = False

## Only allow localhost to connect to Django apps
ALLOWED_HOSTS = [ '127.0.0.1', 'localhost' ]

## Adding edtwExampleAPI in INSTALLED_APPS to acknowledge pyinstaller to include it during the build
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'edtwExampleAPI',
]
## Since we move the setting file into the folder,
## sqlite db file is one level higher than BASE_DIR
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR.parent / 'db.sqlite3',
    }
}

(我们将使用生产配置包装我们的应用程序)

  • 基于docs,在settings文件夹中创建__init__.py

  • __init__.py中粘贴以下代码

from .prod import *

文件夹结构看起来像这样:

image.png

  • 返回edtwExample并使用以下命令构建Django应用程序:
cd ../..
pyinstaller --name=edtwExample edtwExample\manage.py --noconfirm

image.png

  • 如果构建成功完成,将显示该消息:
    image.png

  • 转到dist\edtwExample并运行以下命令

cd dist\edtwExample
edtwExample.exe runserver --settings=edtwExample.settings.prod --noreload

(似乎Edtwexample.exe结束了整个Python虚拟环境和Manage.py)

如果一切都很好,它将显示以下内容:
image.png

请确保该应用使用edtwExample.settings.prod

然后我们完成了第一步。 ð

3.1。评论

我想在此步骤中指出的事情很少。

3.1.1。服务器错误(500)

完成第一步并尝试浏览API(http://127.0.0.1:8000/edtwExampleAPI/get_val_from/?input=AAAAcccc),您可能会看到以下内容:
image.png

这个不意味着有任何错误。通过pyinstaller构建Django应用程序后,EXE应用程序将阻止浏览器访问API。

如果您想测试API,请使用Postman或其他API工具

image.png

3.1.2。 templatedoesnotexit:debug = true

如果我们设置DEBUG = True并构建应用程序,当我们浏览URL http://127.0.0.1:8000/edtwExampleAPI/get_val_from/?input=AAAAcccc时,可能会显示以下错误:

image.png

Pyinstaller假设DEBUG = False,并且不包括任何HTML,CSS或JS文件到EXE App

3.1.3。 ModulenotFoundError:无模块名为XXX

构建应用程序并运行EXE文件后,可能发生错误ModuleNotFoundError

ModuleNotFoundError: No module named 'edtwExampleAPI'

的原因之一是,您不得在配置下的INSTALLED_APPS中包含所需的模块。

3.1.4。 Pyinstaller而不是复制Virutalenv

您可以问为什么我们不只是将虚拟环境复制到新PC而不是使用pyinstaller

我尝试了一下,但是当我激活环境时,它表明没有安装包装。 ð¢这是一个相当明确的解释。

image.png

链接:Create a copy of virtualenv locally without pip install

所以我决定使用pyinstaller

4.包装电子应用

生成Django Exe应用后,我们将包装电子应用

  • 回到基本文件夹并运行以下内容。
cd ..\..\..
npm run package

成功输出将如下显示。

image.png

  • 转到out\edtwexample-win32-x64并运行edtwexample.exe
    image.png

  • 测试应用程序时,您可能会看到以下错误:
    image.png

    这是因为我们没有将Django Exe应用程序复制到包装夹,我们将在下一步中修复此错误。

5.将Django Exe包括在电子包中

5.1。将Django EXE应用程序复制到电子包装文件夹

  • 基于此link,在package.json中,添加以下afterExtract配置
"afterExtract": [
    "./src/build/afterExtract.js"
]

package.json中,

  "license": "MIT",
  "config": {
    "forge": {
      "packagerConfig": {
            "afterExtract": [
                "./src/build/afterExtract.js"
            ]
      },
  • 创建一个文件./src/build/afterExtract.js并将以下代码粘贴到文件中
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs-extra');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');

module.exports = function( extractPath, electronVersion, platform, arch, done )
{
    console.log({ extractPath });
    fs.copy('./python/dist/edtwExample', path.join( extractPath, 'python' ), () => {

        console.log('Finished Copy Python Folder');
        done();
    } );
 }

该功能是将我们的Django EXE应用程序复制到电子包装文件夹。

5.2。在电子启动过程中启动Django EXE应用程序。

  • index.ts中,首先定义可变的DJANGO_CHILD_PROCESS
let DJANGO_CHILD_PROCESS: ChildProcessWithoutNullStreams = null;
  • 创建2个函数spawnDjangoisDevelopmentEnv
const spawnDjango = () =>
{
    if ( isDevelopmentEnv() )
    {
        return spawn(`python\\edtwExampleEnv\\Scripts\\python.exe`,
        ['python\\edtwExample\\manage.py', 'runserver', '--noreload'], {
            shell: true,
        });
    }
    return spawn(`cd python && edtwExample.exe runserver --settings=edtwExample.settings.prod --noreload`,  {
        shell: true,
    });
}

const isDevelopmentEnv = () => {
    console.log( `NODE_ENV=${ process.env.NODE_ENV }` )
    return process.env.NODE_ENV == 'development'
}
  • 在函数startDjangoServer中调用spawnDjango,然后将其更改如下:
const startDjangoServer = () =>
{
    DJANGO_CHILD_PROCESS = spawnDjango();
    DJANGO_CHILD_PROCESS.stdout.on('data', data =>
    {
        console.log(`stdout:\n${data}`);
    });
    DJANGO_CHILD_PROCESS.stderr.on('data', data =>
    {
        console.log(`stderr: ${data}`);
    });
    DJANGO_CHILD_PROCESS.on('error', (error) =>
    {
        console.log(`error: ${error.message}`);
    });
    DJANGO_CHILD_PROCESS.on('close', (code) =>
    {
        console.log(`child process exited with code ${code}`);
    });
    DJANGO_CHILD_PROCESS.on('message', (message) =>
    {
        console.log(`stdout:\n${message}`);
    });
    return DJANGO_CHILD_PROCESS;
}

我们只需要在生产中启动Django EXE应用程序,而不是在开发中。

5.3。跳过生产中的开放开发工具

  • 创建以下新功能。
const openDevTools = ( mainWindow : BrowserWindow ) => {

    if ( isDevelopmentEnv() )
    {
        mainWindow.webContents.openDevTools();
    }
}
  • createWindow方法中称其为。
const createWindow = (): void => {

    ...
    // Open the DevTools.
    openDevTools( mainWindow );
};

5.4。完整的忽略

这是完整的index.ts

import { app, BrowserWindow } from 'electron';
import { spawn, ChildProcessWithoutNullStreams } from 'child_process';

declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
let DJANGO_CHILD_PROCESS: ChildProcessWithoutNullStreams = null;

if (require('electron-squirrel-startup')) {
    // eslint-disable-line global-require
    app.quit();
}

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.
    openDevTools(mainWindow);
};

const UpsertKeyValue = (obj: any, keyToChange: string, value: string[]) =>
{
    const keyToChangeLower = keyToChange.toLowerCase();
    for (const key of Object.keys(obj)) {
        if (key.toLowerCase() === keyToChangeLower) {
            obj[key] = value;
            return;
        }
    }
    obj[keyToChange] = value;
}

const startDjangoServer = () =>
{
    DJANGO_CHILD_PROCESS = spawnDjango();
    DJANGO_CHILD_PROCESS.stdout.on('data', data =>
    {
        console.log(`stdout:\n${data}`);
    });
    DJANGO_CHILD_PROCESS.stderr.on('data', data =>
    {
        console.log(`stderr: ${data}`);
    });
    DJANGO_CHILD_PROCESS.on('error', (error) =>
    {
        console.log(`error: ${error.message}`);
    });
    DJANGO_CHILD_PROCESS.on('close', (code) =>
    {
        console.log(`child process exited with code ${code}`);
    });
    DJANGO_CHILD_PROCESS.on('message', (message) =>
    {
        console.log(`stdout:\n${message}`);
    });
    return DJANGO_CHILD_PROCESS;
}

const spawnDjango = () =>
{
    if (isDevelopmentEnv()) {
        return spawn(`python\\edtwExampleEnv\\Scripts\\python.exe`,
            ['python\\edtwExample\\manage.py', 'runserver', '--noreload'], {
            shell: true,
        });
    }
    return spawn(`cd python && edtwExample.exe runserver --settings=edtwExample.settings.prod --noreload`, {
        shell: true,
    });
}

const openDevTools = (mainWindow: BrowserWindow) =>
{

    if (isDevelopmentEnv()) {
        mainWindow.webContents.openDevTools();
    }
}

const isDevelopmentEnv = () =>
{
    console.log(`NODE_ENV=${process.env.NODE_ENV}`)
    return process.env.NODE_ENV == 'development'
}

app.on('ready', createWindow);

app.on('window-all-closed', () =>
{
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

app.on('activate', () =>
{
    if (BrowserWindow.getAllWindows().length === 0) {
        createWindow();
    }
});

5.5。包裹并运行

包装电子应用程序并再次运行。您应该看到该应用程序顺利运行。 ð

npm run package
cd out\edtwexample-win32-x64\
edtwexample.exe

image.png

6.关闭窗口时关闭Django EXE应用程序

您可能会注意到Django Exe进程仍在运行,即使您关闭了应用程序Windows

image.png

我们需要告诉应用程序一旦窗口接近一旦窗口杀死该过程。

  • 首先,我们将安装tree-kill软件包
npm install tree-kill
  • 然后在index.ts中添加以下代码
app.on('before-quit', async function ()
{
    // Kill python process when the window is closed
    kill( DJANGO_CHILD_PROCESS.pid );
});
  • window-all-closed中也添加kill( DJANGO_CHILD_PROCESS.pid )
app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit();
    }
    kill( DJANGO_CHILD_PROCESS.pid );
});
  • 再次包装电子应用程序,问题应解决。 ðð

6.1。背后的原因

6.1.1。 django exe由壳牌产生

django exe进程是用shell: true选项产生的,这意味着该过程是由cmd而不是直接启动的。

index.ts

spawn(`cd python && edtwExample.exe runserver --settings=edtwExample.settings.prod --noreload`, {
        shell: true,
    });

当我们关闭窗口时,我们只关闭外壳,但是该过程仍在运行。

结果,我们需要在关闭Windows事件侦听器中杀死该过程。

解释和解决方案:link

6.1.2。杀死过程2事件听众

我们需要在下面 事件听众中杀死该过程。

  • window-all-closed
  • before-quit

我尝试将此行kill( DJANGO_CHILD_PROCESS.pid )仅在任何一个事件中包括在内,即使关闭了应用程序窗口,也不会杀死Django进程。

7.源代码

6.-Package_Electron_n_django_app

8.撰写此博客的原因

写了博客"Electron + Django, desktop app integrate JavaScript & Python"后,我认为使用Django应用程序包装电子应用是一个简单的任务,即可通过运行一个或两个命令,但是我错了ð¢。

当我打包应用程序时,我进行了很多Google搜索来解决此过程中出现的问题,这对我来说很难。

另外,在搜索过程中,我注意到缺乏有组织的方法来解释整个包装过程,这就是我写这个博客的原因。