1.简介和POC
我们如何将电子应用包装与django打包?如果您已经完成阅读"Electron + Django, desktop app integrate JavaScript & Python"。
,您可能会更渴望知道答案。在此博客中,我想详细解释包装过程,并讨论您在此过程中可能需要注意的内容。 ð
在视频中,我们可以看到:
- 通过单击EXE文件打开应用
- 测试应用程序是否有效
- 在任务管理器中,您可以看到有5个运行
顶部的一个是python应用程序,剩余是电子应用
2.先决条件
我们将基于此example,以显示包装应用程序的步骤。您可以关注README首先构建项目。
3.软件包django应用程序
我们将使用pyinstaller
包装Django应用程序。
- 激活环境并安装
pyinstaller
pip install pyinstaller
- 在
edtwExample
下添加settings
文件夹
cd python\edtwExample\edtwExample
mkdir settings
- 将
settings.py
文件复制到文件夹中,然后将重命名为dev.py
和prod.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'
- debug,
## 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 *
文件夹结构看起来像这样:
- 返回
edtwExample
并使用以下命令构建Django应用程序:
cd ../..
pyinstaller --name=edtwExample edtwExample\manage.py --noconfirm
cd dist\edtwExample
edtwExample.exe runserver --settings=edtwExample.settings.prod --noreload
(似乎Edtwexample.exe结束了整个Python虚拟环境和Manage.py)
请确保该应用使用edtwExample.settings.prod
然后我们完成了第一步。 ð
3.1。评论
我想在此步骤中指出的事情很少。
3.1.1。服务器错误(500)
完成第一步并尝试浏览API(http://127.0.0.1:8000/edtwExampleAPI/get_val_from/?input=AAAAcccc),您可能会看到以下内容:
这个不意味着有任何错误。通过pyinstaller
构建Django应用程序后,EXE应用程序将阻止浏览器访问API。
如果您想测试API,请使用Postman或其他API工具
3.1.2。 templatedoesnotexit:debug = true
如果我们设置DEBUG = True
并构建应用程序,当我们浏览URL http://127.0.0.1:8000/edtwExampleAPI/get_val_from/?input=AAAAcccc时,可能会显示以下错误:
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
?
我尝试了一下,但是当我激活环境时,它表明没有安装包装。 ð¢这是一个相当明确的解释。
链接:Create a copy of virtualenv locally without pip install
所以我决定使用pyinstaller
。
4.包装电子应用
生成Django Exe应用后,我们将包装电子应用
- 回到基本文件夹并运行以下内容。
cd ..\..\..
npm run package
成功输出将如下显示。
-
这是因为我们没有将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个函数
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 = () =>
{
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
6.关闭窗口时关闭Django EXE应用程序
您可能会注意到Django Exe进程仍在运行,即使您关闭了应用程序Windows
我们需要告诉应用程序一旦窗口接近一旦窗口杀死该过程。
- 首先,我们将安装
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搜索来解决此过程中出现的问题,这对我来说很难。
另外,在搜索过程中,我注意到缺乏有组织的方法来解释整个包装过程,这就是我写这个博客的原因。