1.简介 & POC

我们如何用 django 打包电子应用程序?看完《Electron + Django (Part 1),桌面应用集成JavaScript&Python》,你可能更想知道答案。

在这篇博客中,我想详细解释打包过程,并讨论您在此过程中可能需要注意的事项。 😎

先给大家看看最终结果:6.-Package_output.mp4.gif

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

  • 点击exe文件打开app

  • 测试应用看它是否工作

  • 在任务管理器中可以看到有5个进程在运行image.png

最上面的一个是 python 应用程序,其余的是电子应用程序

2.先决条件

我们将基于这个示例来展示打包应用程序的步骤。您可以先按照 README 来构建项目。

3.打包 Django 应用程序

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

  • 激活环境并安装pyinstaller
点安装 pyinstaller
zoz100027`

* 在`edtwExample`下添加`settings`文件夹

cd python dtwExample dtwExample

mkdir 设置


* 将`settings.py`文件复制到文件夹中并重命名为`dev.py`和`prod.py`,去掉原来的`settings.py`

zoz100036`
复制 settings.py settings\dev.py
复制 settings.py settings\prod.py
删除设置.py
zoz100037`


* 在`prod.py`中更改如下配置
* 调试,`True`>`False`
* 允许\_HOSTS,`[]`>`[ '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',

}

}


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

* 在[文档](https://github.com/pyinstaller/pyinstaller/wiki/Recipe-Executable-From-Django#multiple-settings-modules)的基础上,在`settings`文件夹中创建`__init__.py`

* 将以下代码粘贴到`__init__.py`中

从 .prod 导入 *

祖兹 100093-

文件夹结构如下所示: image.png

  • 回到edtwExample并使用以下命令构建 Django 应用程序:
光盘../..
pyinstaller --nameu003dedtwExample edtwExample\manage.py --noconfirm

image.png

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

  • 进入dist\edtwExample,运行如下命令

cd dist dtw示例
edtwExample.exe 运行服务器 --settingsu003dedtwExample.settings.prod --noreload

(似乎 edtwExample.exe 包装了整个 python 虚拟环境和manage.py)

如果一切正常,它将显示以下内容: image.png

请确保应用程序正在使用edtwExample.settings.prod

然后我们就完成了第一步。 😊

3.1.评论

在此步骤中,我想指出几件事。

3.1.1.服务器错误 (500)

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

意味着有任何错误。pyinstaller构建django应用后,exe应用会阻止浏览器访问API。

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

image.png

3.1.2. TemplateDoesNotExit:调试 u003d True

如果我们设置DEBUG = True并构建应用程序,当我们浏览 url127.0.0.1:8000/edtwExampleAPI/get_val_from/..时,可能会出现以下错误:

image.png

pyinstaller 假定DEBUG = False并且不包含任何 html、css 或 js 文件到 exe 应用程序中

3.1.3. ModuleNotFoundError:没有名为 XXX 的模块

构建app并运行exe文件后,可能会出现错误ModuleNotFoundError

ModuleNotFoundError: No module named 'edtwExampleAPI'

原因之一是您可能没有在配置prod.py下的INSTALLED_APPS中包含所需的模块。

3.1.4. Pyinstaller 而不是复制 virtualenv

你可能会问,我们为什么不直接将虚拟环境复制到新PC,而不是使用pyinstaller?

我试过了,但是当我激活环境时,它显示没有安装包。 😢 这是一个相当清晰的解释。

image.png

链接:在本地创建一个virtualenv的副本,无需pip install

所以我决定改用pyinstaller

4.打包电子应用程序

生成 Django exe 应用程序后,我们将打包 Electron 应用程序

  • 回到基本文件夹并运行以下命令。
cd ..\..\..
npm 运行包

成功输出将如下所示。 image.png

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

  • 测试应用时,可能会看到如下错误:image.png

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

5.在 Electron 包中包含 Django exe

5.1.将 Django exe 应用程序复制到 Electron 包文件夹

  • 基于这个链接,在package.json中添加如下afterExtract配置
“提取后”:[
“./src/build/afterExtract.js”
]

package.json中,

“许可证”:“麻省理工学院”,
“配置”:{
“锻造”:{
“包装器配置”:{
“提取后”:[
“./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应用程序复制到Electron包文件夹中。

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

  • index.ts中,先定义一个变量DJANGO_CHILD_PROCESS
让 DJANGO_CHILD_PROCESS: ChildProcessWithoutNullStreams u003d 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 u003d () u003d>
{
DJANGO CHILD_PROCESS u003d 生成 Django();
DJANGO_CHILD_PROCESS.stdout.on('数据', 数据 u003d>
{
控制台.log(`stdout:\n${data}`);
});
DJANGO_CHILD_PROCESS.stderr.on('data', data u003d>
{
控制台.log(`stderr: ${data}`);
});
DJANGO_CHILD_PROCESS.on('error', (error) u003d>
{
控制台.log(`error: ${error.message}`);
});
DJANGO_CHILD_PROCESS.on('close', (code) u003d>
{
控制台.log(`child process exited with code ${code}`);
});
DJANGO_CHILD_PROCESS.on('消息', (消息) u003d>
{
控制台.log(`stdout:\n${message}`);
});
返回 DJANGO_CHILD_PROCESS;
}

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

5.3.在生产中跳过打开的开发工具

  • 创建以下新函数。
常量 openDevTools u003d ( mainWindow : BrowserWindow ) u003d> {

if ( isDevelopmentEnv() )
{
mainWindow.webContents.openDevTools();
}
}
  • createWindow方法期间调用它。
const createWindow u003d (): void u003d> {

...
// 打开开发工具。
openDevTools(主窗口);
};

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.打包并运行

打包 Electron 应用程序并再次运行。您应该看到应用程序运行顺利。 🎉

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

image.png

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

您可能会注意到,即使您关闭应用程序窗口,Django exe 进程仍在运行

image.png

一旦窗口关闭,我们需要告诉应用程序终止进程。

  • 首先,我们将安装tree-kill
npm 安装树杀
祖兹 100193

* 然后在`index.ts`中加入如下代码

app.on('before-quit', async function ()

{

// 关闭窗口时杀死python进程

杀死(DJANGO_CHILD_PROCESS.pid);

});


* 在`window-all-closed`中添加行`kill( DJANGO_CHILD_PROCESS.pid )`

app.on('window-all-close', () u003d> {

if (process.platform !u003du003d 'darwin') {

app.quit();

}

杀死(DJANGO_CHILD_PROCESS.pid);

});


* 再次打包electron app,问题应该解决了。 👏👏


## 6.1.背后的原因

### 6.1.1. Django exe 是由 shell 生成的

Django exe 进程是使用`shell: true`选项生成的,这意味着该进程是由 cmd 而不是直接由 exe 文件启动的。

在`index.ts`

spawn(cd python && edtwExample.exe runserver --settings=edtwExample.settings.prod --noreload, {

    shell: true,

});

当我们关闭窗口时,我们只关闭了 shell,但进程仍在运行。

因此,我们需要在关闭窗口事件监听器中杀死进程。

解释及解决方法:[链接](https://stackoverflow.com/a/43717477)。

### 6.1.2.杀死进程 2 事件监听器

我们需要在下面的 **BOTH** 事件侦听器中终止进程。

* `window-all-closed`
* `before-quit`

我试图将这一行`kill( DJANGO_CHILD_PROCESS.pid )`仅包含在任一事件中,即使应用程序窗口关闭,Django 进程也不会被杀死。

# 7\.源代码

[6.-Package\_Electron\_n\_django\_app](https://github.com/ivanyu199012/6.-Package_Electron_n_django_app)

# 8\.写这篇博客的原因

写完博客[“Electron + Django, desktop app integration JavaScript & Python”](https://ivanyu2021.hashnode.dev/electron-django-desktop-app-integrate-javascript-and-python)之后,我认为用 django app 打包一个 electron 应用只是运行一两个命令的简单任务,但我错了😢。

当我打包应用程序时,我做了很多谷歌搜索来解决这个过程中出现的问题,这对我来说很难😑。

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

Python社区为您提供最前沿的新闻资讯和知识内容

更多推荐