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();  // 打包前

写在最终

由于是第一次运用Electron来打包客户端软件都是一步一步谷歌出来的。希望大家相互交流学习,遇到什么问题能够留言,一同前进!