乐趣区

关于electron:Electron搭配React的history路由模式打包exe客户端

Electron 装置
装置问题

npm 或者 yarn 装置 electron 就算是配置了淘宝源还是会呈现超时。所以我的解决方案是装置 cnpm,应用 cnpm 去装置。

全局装置 cnpm

npm i cnpm -G
复制代码

新建我的项目

cnpm init // 一路 Enter 而后到最初一步输出 yes

// 装置 dev 相干依赖
cnpm i electron -D // 装置 electron
cnpm i electron-builder -D // 用来打包客户端安装包 — 须要下一步下一步装置来实现点击关上
cnpm i electron-packager -D // 用来打包客户端可执行文件 — 间接点击打包📦后的可执行文件即可运行
// 装置生产相干依赖
cnpm i electron-log // 用于调试时的 log 输入,dev 环境会间接在终端打印日志同时会在我的项目跟目录的 logs 文件夹生成 log
cnpm i electron-updater // 用户我的项目自动更新
cnpm i express // 因为应用的是 history 路由模式所以咱们应用 node 来启动前端我的项目
cnpm i http-proxy-middleware // 用于代理前端我的项目拜访服务器接口
复制代码

相干依赖的版本如下
生产
“electron-log”: “^4.4.8”,
“electron-updater”: “^5.0.5”,
“express”: “^4.18.1”,
“http-proxy-middleware”: “^2.0.6”
复制代码
开发
“electron”: “^19.0.6”,
“electron-builder”: “^23.1.0”,
“electron-packager”: “^15.5.1”
复制代码
我的项目架构详解
├── build // 用于寄存前端打包后的文件
├── desk // 用于寄存打包后的 exe 安装文件或者 dmg
├── logs // 用于寄存我的项目调试 log 文件
├── main.js // electron 的主过程文件
├── media // 我的项目的多媒体文件诸如.mp3 .mp4 .ico .icns 文件
├── node_modules // 我的项目依赖
├── package.json // 配置文件
├── preload.js
├── renderer.js
└── server // 须要打包进我的项目的后端可执行文件
复制代码
对于 preload.js 和 renders.js 的详解

话说,在传统的 electron 程序中,大量的逻辑是写在 renderer.js 文件中的。然而,起初随着 electron 的版本倒退,逐步进去了一种呼声:就是要将 node 能力从 renderer.js 中分离出来。让 renderer.js 回归传统 js 的性能。这个时候,呈现的新概念就是 preload.js。
本文的测试环境:electron@13.0.1,win10。本文探讨 preload.js 在 browserWindow 中的利用,当然,preload.js 在 webview 中也有应用到。然而临时不在本文的探讨范畴内。本文次要命题是:preload.js 的作用范畴,以及如何辨别以后作用的页面。

原文链接
我的项目启动

首先配置 package.json 文件的 main 字段为我的项目中的 main.js
配置 script 字段增加如下
“start”: “chcp 65001 && electron .”, // chcp 65001 是为了解决 Windows 平台在启动后许可的 log 中文乱码问题
“macpack”: “electron-builder build –mac”, // 用于打包 dmg 安装包
“winpack”: “electron-builder build –win” // 用于打包 exe 安装包
复制代码

在 electron 启动前端我的项目

首先须要将打包📦后的前端代码放到我的项目 build 文件夹下,留神是放到 build 文件夹根目录而不是将诸如 dist(vue 打包后)或者 build(react 打包后)文件间接拷贝到我的项目的 build 文件夹。build 文件夹下的文件目录如果是 react 就应该如下

├── asset-manifest.json
├── favicon.ico
├── files
├── index.html
├── manifest.json
├── robots.txt
└── static
复制代码
开始编写 main.js
间接贴出代码如下
const {app, BrowserWindow, Menu, dialog} = require(‘electron’);
const path = require(‘path’);
const isDev = !app.isPackaged;
const cp = require(‘child_process’);
const {createProxyMiddleware} = require(‘http-proxy-middleware’);
const express = require(‘express’);
const application = express();
const START_PORT = 50001;
const DOMAIN = ‘http://xxx’;
const enviroment = process.platform == ‘darwin’ ? ‘mac’ : ‘win’;
const log = require(‘electron-log’);
// 获取我的项目资源目录留神辨别打包前和打包后的区别
const appPath = app.isPackaged

? path.dirname(app.getPath('exe')) // 打包后
: app.getAppPath();  // 打包前

const {autoUpdater} = require(‘electron-updater’);

if (isDev) {

// 判断如果是 dev 环境就将 log 存储在我的项目根目录的 logs 文件夹
log.transports.file.resolvePath = () =>
    path.join(__dirname, `logs/${new Date().toLocaleDateString()}.log`);

}
// 设置 log 日志的格局能够去 electron-log 官网文档查看更多格式化
log.transports.file.format = ‘[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}’;

let localServer; // node 服务的实例,这里定义是为了前面不便在敞开窗口的时候杀掉它
function createWindow() {

// 主过程开启一个尺寸为 1920*1000 的窗口
const mainWindow = new BrowserWindow({
    width: 1920,
    height: 1000,
    webPreferences: {preload: path.join(__dirname, 'preload.js'),
    },
});
// 生命一个 meu
const menu = [
    {
        label: '帮忙',
        submenu: [
            {
                label: '控制台',
                click: () => {mainWindow.webContents.openDevTools({ mode: 'bottom'});
                },
            },
            {
                label: '查看更新',
                click: () => {autoUpdater.checkForUpdates();
                },
            },
            {
                label: '对于',
                click: () => {
                    dialog.showMessageBoxSync({
                        title: '对于',
                        message: `${app.getName()}V${app.getVersion()}`,
                        type: 'info',
                        icon: path.resolve(
                            __dirname,
                            'media/images/logo.png'
                        ),
                        buttons: ['好的'],
                    });
                },
            },
        ],
    },
];
// 启动一个 node 服务也就是用 node 部署打包后的文件
proxys().then((res) => {const m = Menu.buildFromTemplate(menu);
    // 设置顶部菜单
    Menu.setApplicationMenu(m);
    // 窗口显示咱们部属的前端我的项目
    mainWindow.loadURL(`http://127.0.0.1:${START_PORT}`);
    // 判断如果是 dev 环境将 devTool 关上
    isDev && mainWindow.webContents.openDevTools();});

// 启动后端服务
startServer();

}

function checkUpdate() {

if (enviroment === 'win') {
    // 本地模仿更新的端口
    autoUpdater.setFeedURL('http://127.0.0.1:9005/win32');
} else {// mac 系統更新}

autoUpdater.checkForUpdates();
// 监听 'error' 事件
autoUpdater.on('error', (err) => {logMsg(`autoUpdater 谬误 ${err}`);
});

// 监听 'update-available' 事件,发现有新版本时触发
autoUpdater.on('update-available', () => {logMsg('发现更新 -----------------------------');
});

autoUpdater.on('update-not-available', () => {
    dialog
    .showMessageBox({
        type: 'info',
        title: '利用更新',
        message: '未发现新版本'
    })
})

// 监听 'update-downloaded' 事件,新版本下载实现时触发
autoUpdater.on('update-downloaded', () => {
    // 如果有更新提醒用户并后盾下载安装
    dialog
        .showMessageBox({
            type: 'info',
            title: '利用更新',
            message: '发现新版本,是否更新?',
            buttons: ['是', '否'],
        })
        .then((buttonIndex) => {if (buttonIndex.response == 0) {
                // 抉择是,则退出程序,装置新版本
                autoUpdater.quitAndInstall();
                app.quit();}
        });
});

}
function logMsg(msg) {

log.info(msg);

}

function startServer() {

// 启动后盾打包后的可执行文件
logMsg('开始执行 -----------------------------');
let shellCode;
if (enviroment === 'win') {logMsg(` 程序安装目录: ${appPath}`);
    // serverPath = path.resolve(__dirname, 'server/python');
    const serverPathSplit = appPath.split(':');
    shellCode = `${serverPathSplit[0]}: && cd ${serverPathSplit[1]}${isDev ? '':'\\resources'}\\server\\python && ${enviroment ==='win'?'main.exe':'test'}`;
    logMsg(` 行将执行脚本:${shellCode}`);
}
// 子过程运行后端可执行文件
cp.exec(shellCode, (error, stdout, stderr) => {if (error) {logMsg(` 脚本执行谬误: ${error}`);
        return;
    }
    logMsg('执行胜利');
    logMsg(`stdout: ${stdout}`);
    log.error(`stderr: ${stderr}`);
});
logMsg('完结执行 -----------------------------');

}

function proxys() {

return new Promise((resolve, reject) => {
    application.use(
        createProxyMiddleware('/api', {
            target: DOMAIN,
            changeOrigin: true,
            secure: false,
        })
    );
    application.use(
        createProxyMiddleware('/v1', {
            target: DOMAIN,
            changeOrigin: true,
            secure: false,
        })
    );
    application.use(
        createProxyMiddleware('/icons', {
            target: DOMAIN,
            changeOrigin: true,
            secure: false,
        })
    );
    application.use(
        createProxyMiddleware('/apks', {
            target: DOMAIN,
            changeOrigin: true,
            secure: false,
        })
    );
    application.use(
        createProxyMiddleware('/zip', {
            target: DOMAIN,
            changeOrigin: true,
            secure: false,
        })
    );
    application.use(
        createProxyMiddleware('/img_avatar', {
            target: DOMAIN,
            changeOrigin: true,
            secure: false,
        })
    );
    application.use(
        createProxyMiddleware('/screenshot', {
            target: DOMAIN,
            changeOrigin: true,
            secure: false,
        })
    );
    application.use(
        createProxyMiddleware('/data', {
            target: DOMAIN,
            changeOrigin: true,
            secure: false,
        })
    );
    application.use(
        createProxyMiddleware('/android', {
            target: DOMAIN,
            changeOrigin: true,
            secure: false,
        })
    );
    application.use(
        createProxyMiddleware('/ipa_icons', {
            target: DOMAIN,
            changeOrigin: true,
            secure: false,
        })
    );
    application.use(
        createProxyMiddleware('/ipas', {
            target: DOMAIN,
            changeOrigin: true,
            secure: false,
        })
    );
    application.use(
        createProxyMiddleware('/admin', {
            target: DOMAIN,
            changeOrigin: true,
            secure: false,
        })
    );
    application.use(
        createProxyMiddleware('/ws', {
            target: DOMAIN,
            changeOrigin: true,
            secure: false,
        })
    );
    application.use(
        createProxyMiddleware('/desktop', {
            target: 'http://127.0.0.1:29096',
            changeOrigin: true,
            secure: false,
        })
    );
    // 这一步是用户前端我的项目是 history 路由比方写的相干配置
    application.use(express.static(path.resolve(__dirname, 'build')));
    application.get('*', function (request, response) {response.sendFile(path.resolve(__dirname, 'build', 'index.html'));
    });
    localServer = application.listen(START_PORT, () => {resolve();
    });
});

}
app.whenReady().then(() => {

createWindow();
// 判断窗口 ready 之后检测更新
checkUpdate();
app.on('activate', function () {if (BrowserWindow.getAllWindows().length === 0) createWindow();});

});

app.on(‘window-all-closed’, function () {

// 敞开窗口之后须要杀掉 node 启动的服务
localServer.close();
logMsg(`node 服务已停止 ---------------------`);
if (enviroment === 'win') {cp.exec(`taskkill /f /t /im main.exe`, (error, stdout, stderr) => {if (error) {logMsg(` 杀死过程执行谬误: ${error}`);
            return;
        }
        logMsg(`stdout: ${stdout}`);
        log.error(`stderr: ${stderr}`);
        logMsg('后盾服务程序已被杀死 ---------------------');
    });
}

if (process.platform !== 'darwin') app.quit();

});

复制代码
对于 package.json 的编写
因为应用的是 electron-builder 故能够去到该插件官网查看相干字段的文档。因为业务要求咱们只须要打包.exe 所以以下是对于打包 exe 利用的相干配置。
以下我用到的字段我尽量正文写进去
“build”: {

"appId": "9928c2b60725cde286468f0696df8b30",
"productName": "打包后的利用名称",
"icon": "./media/images/logo.png",  // 打包后的利用 logo
"asar": true,  // 是否应用 asar 加密源码
"nsis": {
  "oneClick": false, // 是否一键装置
  "allowElevation": true,  
  "allowToChangeInstallationDirectory": true,  // 是否能够自定义装置目录
  "installerIcon": "./media/images/app.ico", // 装置时候的 icon 图标,留神图标格局是.con
  "uninstallerIcon": "./media/images/app.ico",  // 卸载时候的 icon 图标
  "installerHeaderIcon": "./media/images/app.ico",  // 装置时候的头 icon
  "createDesktopShortcut": true,   // 是否创立桌面快捷方式
  "createStartMenuShortcut": true,  
  "shortcutName": "星源",
  "include": "script/installer.nsh"  // 装置实现执行的 nsh 脚本
},
"directories": {"output": "desk/win" // 打包实现输入的目录如果该目录不存在会帮你在以后我的项目创立},
"files": [ // 须要打包的文件
  "main.js",
  "build",
  "preload.js",
  "media",
  "script",
  "package.json",
  "server"
],
"extraResources": [ // 不须要打包的额定资源,比我我这里就寄存了后端的可执行.exe 文件
  {
    "from": "server",
    "to": "server"
  }
],
"publish": {  // 自动更新 -- 这里前面会讲到
  "provider": "generic",
  "url": "http://127.0.0.1:9005/"
},
"win": {
  "icon": "media/images/logo.png",
  "target": [
    {
      "target": "nsis",
      "arch": ["ia32"]
    }
  ]
}

}
复制代码
对于自动更新
如何编写自动更新的配置
先阐明应用到的依赖是 electron-updater 点击查看官网文档
上文中 main.js 文件中的如下代码块的作用就是用来自动更新的, 如下代码正文都写了进去
function checkUpdate() {

if (enviroment === 'win') {
    // 本地模仿更新的端口
    autoUpdater.setFeedURL('http://127.0.0.1:9005/win32');
} else {// mac 系統更新}

autoUpdater.checkForUpdates();
// 监听 'error' 事件
autoUpdater.on('error', (err) => {logMsg(`autoUpdater 谬误 ${err}`);
});

// 监听 'update-available' 事件,发现有新版本时触发
autoUpdater.on('update-available', () => {logMsg('发现更新 -----------------------------');
});

autoUpdater.on('update-not-available', () => {
    dialog
    .showMessageBox({
        type: 'info',
        title: '利用更新',
        message: '未发现新版本'
    })
})

// 监听 'update-downloaded' 事件,新版本下载实现时触发
autoUpdater.on('update-downloaded', () => {
    // 如果有更新提醒用户并后盾下载安装
    dialog
        .showMessageBox({
            type: 'info',
            title: '利用更新',
            message: '发现新版本,是否更新?',
            buttons: ['是', '否'],
        })
        .then((buttonIndex) => {if (buttonIndex.response == 0) {
                // 抉择是,则退出程序,装置新版本
                autoUpdater.quitAndInstall();
                app.quit();}
        });
});

}
复制代码
搭建本地公布平台
我本人搭建的一个本地更新服务应用 node 写的 仓库我的项目地址
该代码的应用如下

首先在我的项目根目录创立 static 文件夹, 实践上该目录下📁内容如下
├── builder-debug.yml
├── builder-effective-config.yaml
├── latest.yml
├── win-ia32-unpacked
├── �\230\237�\220�\214�\235��\211\210\ Setup\ 1.0.0.exe
└── �\230\237�\220�\214�\235��\211\210\ Setup\ 1.0.0.exe.blockmap
复制代码

将打包后的 exe 以及一堆相干配置文件丢到该目录

启动我的项目 npm run start

Electron 我的项目的 package.json 配置如下
“publish”: {// 自动更新 – 这里前面会讲到

  "provider": "generic",
  "url": "http://127.0.0.1:9005/"
},

复制代码
遇到的问题

前端我的项目 dev 环境启动能够失常看,然而打包之后始终报 css/js 门路加载问题。打包后的代码的门路指定

    // 这一步是用户前端我的项目是 history 路由比方写的相干配置
    application.use(express.static(path.resolve(__dirname, 'build'))); // 这里肯定要应用 path 来 resole 到以后打包目录的根目录要不然会呈现资源加载问题
    application.get('*', function (request, response) {response.sendFile(path.resolve(__dirname, 'build', 'index.html'));
    });

复制代码

打包进去会呈现有些包找不到。解决方案是如果你确定你在打包后须要用到的包,在应用 cnpm 装置的时候不要加 - D 后缀,即便该包变成我的项目依赖而非开发环境依赖。因为打包会打包 dependencies 而不会打包 devDependencies
打包的时候会呈现打包出错,记得认真查看终端谬误日志。我遇到的就是 icon 的格局不对。留神以下三个字段的文件格式是 ico 而非 png

“installerIcon”: “./media/images/app.ico”,
“uninstallerIcon”: “./media/images/app.ico”,
“installerHeaderIcon”: “./media/images/app.ico”,
复制代码

启动后盾给到的可执行文件实现不联网本地数据入库。

“extraResources”: [

  {
    "from": "server",
    "to": "server"
  }
],

复制代码
如上我将后端给到的可执行文件放在我的项目的 server 目录,而后应用 extraResources 字段将打包后的文件放到了 server 目录。
在本地和打包后的门路会有很大出入。应用 app.isPackaged 判断是否是打包后。如下来获取该目录正确地址来执行后端打包后的可执行文件。
const appPath = app.isPackaged

? path.dirname(app.getPath('exe')) // 打包后
: app.getAppPath();  // 打包前
退出移动版