Electron + Django(第 2 部分),将其打包到生产环境中
1.简介 & POC
我们如何用 django 打包电子应用程序?看完《Electron + Django (Part 1),桌面应用集成JavaScript&Python》,你可能更想知道答案。
在这篇博客中,我想详细解释打包过程,并讨论您在此过程中可能需要注意的事项。 😎
先给大家看看最终结果:
在视频中,我们可以看到:
-
点击exe文件打开app
-
测试应用看它是否工作
-
在任务管理器中可以看到有5个进程在运行

最上面的一个是 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-
文件夹结构如下所示: 
- 回到
edtwExample并使用以下命令构建 Django 应用程序:
光盘../..
pyinstaller --nameu003dedtwExample edtwExample\manage.py --noconfirm

-
如果构建成功完成,将显示消息:

-
进入
dist\edtwExample,运行如下命令
cd dist dtw示例
edtwExample.exe 运行服务器 --settingsu003dedtwExample.settings.prod --noreload
(似乎 edtwExample.exe 包装了整个 python 虚拟环境和manage.py)
如果一切正常,它将显示以下内容: 
请确保应用程序正在使用edtwExample.settings.prod
然后我们就完成了第一步。 😊
3.1.评论
在此步骤中,我想指出几件事。
3.1.1.服务器错误 (500)
完成第一步并尝试浏览 API (127.0.0.1:8000/edtwExampleAPI/get_val_from/..) 后,您可能会看到以下内容: 
这不意味着有任何错误。pyinstaller构建django应用后,exe应用会阻止浏览器访问API。
如果您想测试 API,请使用 Postman 或其他 API 工具

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

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?
我试过了,但是当我激活环境时,它显示没有安装包。 😢 这是一个相当清晰的解释。

链接:在本地创建一个virtualenv的副本,无需pip install
所以我决定改用pyinstaller。
4.打包电子应用程序
生成 Django exe 应用程序后,我们将打包 Electron 应用程序
- 回到基本文件夹并运行以下命令。
cd ..\..\..
npm 运行包
成功输出将如下所示。 
-
转到
out\edtwexample-win32-x64并运行edtwexample.exe
-
测试应用时,可能会看到如下错误:

这是因为我们没有将 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个函数
spawnDjango和isDevelopmentEnv
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

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

一旦窗口关闭,我们需要告诉应用程序终止进程。
- 首先,我们将安装
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 应用只是运行一两个命令的简单任务,但我错了😢。
当我打包应用程序时,我做了很多谷歌搜索来解决这个过程中出现的问题,这对我来说很难😑。
另外,在搜索过程中,我注意到缺乏一个有组织的方法来解释整个打包过程,这就是我写这篇博客的原因。
更多推荐

所有评论(0)