从0到1构建electron桌面应用

1,333 阅读6分钟

什么是electron

说起跨平台桌面应用 可能第一反应是QT 由于比较底层,其性能极高 但是由于用C++ 开发 对于前端来说成本比较大

然后就是NW前几年还是很活跃的 毕竟 钉钉 微信web开发工具都是基于它开发的,但是其所有窗口共享一个node.js环境所以耦合行比较高,后面基本半死不活了。

而electron就是一款相比于QT更容易上手,比NW更加全面,社区更加活跃的一款一个跨平台的、基于 Web 前端技术的桌面 GUI 应用程序开发框架。下面看看3者的对比图: 正如官方文档所说:Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js , Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发 经验。 www.electronjs.org/

安装并运行:

1.首先安装electron,和正常安装依赖包一样就行,由于文件相对较大,所以最好使用淘宝镜像或者yarn;

2.然后要在最外层目录创建main.js作为程序的主进程;

3.然后添加package.json中的 "main": "main.js";

4.最后为应用添加启动命令 "electron-start": "electron ." 正常情况下先启动项目,在运行启动命令就可以了。但是为了方便可以设置一条命令达到目的: 由于我是用的umi构建项目所以直接监听8000端口

"electron-dev":  "concurrently \" wait-on http://localhost:8000 && electron .\" \"npm start\" ",

知道了启动的命令后,我得在main.js中写入主进程相关的逻辑:

主进程可以做些什么

ps:只列举一部分

监听electron初始化 设置主窗口参数 BrowserWindow

1.创建浏览器窗口

const { app, BrowserWindow} = require('electron');
function createWindow() {
    //创建浏览器窗口,宽高自定义具体大小你开心就好
    mainWindow = new BrowserWindow({
        width: 1150,
        height: 750,
        minWidth: 1150,
        minHeight: 750,
        frame: true,
        resizable: false,//禁止改变主窗口尺寸
        thickFrame: true,
        webPreferences: {
            javascript: true,
            plugins: true,
            nodeIntegration: true, // 不集成 Nodejs
            webSecurity: false,
            enableRemoteModule: true,//使用Remote
            contextIsolation:false//隔离上下文
        }
    })
    const ipc = require('electron').ipcMain;
    ipc.on('close', function () { //'close 是自定义的命令 ,只要与页面发过来的命令名字统一就可以
        //接收到消息后的执行程序
        // mainWindow.destroy()
        //   app.quit();
        //   app.quit()
        //   mainWindow.hide()
    })
    mainWindow.webContents.openDevTools()
    if (isDevelopment) {
        // 加载应用
        // 打开开发者工具,默认不打开
        mainWindow.loadURL('http://localhost:8000/')
    } else {
        //正式版本
        const pathname = path.join(__dirname, 'dist/index.html')
        mainWindow.loadFile(pathname)
    }
    // 关闭window时触发下列事件.
    mainWindow.on('closed', (e) => {
        //回收BrowserWindow对象
        mainWindow = null;
    })
}

2.当electron初始化完毕,调用函数创建窗口

app.whenReady().then(createWindow)
//or
app.on('ready', createWindow)
自定义托盘
const {  Tray,Menu } = require('electron');
let trayMenuTemplate = [               
        {            
            label'BIM族库',            
            clickfunction () {
                mainWindow.show();
            }        
        },        
        {            
            label'退出',            
            clickfunction () {                 
                app.quit();                
                app.quit();//因为程序设定关闭为最小化,所以调用两次关闭,防止最大化时一次不能关闭的情况            
            }        
        }    
    ];
    //系统托盘图标目录
    let trayIcon = path.join(__dirname, '');//app是选取的目录
    appTray = new Tray(path.join(trayIcon, 'icon.ico'));//app.ico是app目录下的ico文件
     //图标的上下文菜单
     const contextMenu = Menu.buildFromTemplate(trayMenuTemplate);
      //设置此托盘图标的悬停提示内容
    appTray.setToolTip('标题'); 
    appTray.setContextMenu(contextMenu);
    //单击右下角小图标显示应用
    appTray.on('click',function(){
        mainWindow.show();
    })
自定义弹窗 由于是窗口 所以浏览器的原生弹窗要捕获也可以自定义
const { dialog } = require('electron');
function showMessage(text) {
     const options = {
        type: 'info',
        title: '提示',
        message: text,
    }
    dialog.showMessageBox(options)

};
与渲染进程通信

1.渲染进程发送消息给主进程

可以通过ipcRenderer.send发送自定义指令消息

import { ipcRenderer } from 'electron';
ipcRenderer.send('aaa'); //aaa是自定义指令

2.主进程监听渲染进程send的消息

可以通过ipc.on监听渲染进程发送的具体指令

然后可以通过mainWindow.webContents.send或者e.reply回消息给渲染进程 有点类似聊天

const ipc = require('electron').ipcMain;
ipc.on('big'(e)=>{
   mainWindow.maximize() //最大化窗口
   mainWindow.webContents.s
   end或者('tobig')
   //e.reply回消息给渲染进程('tobig',true)
})

3.渲染进程接收

和主进程类似不过主进程是ipcMain 渲染进程是ipcRenderer

import { ipcRenderer } from 'electron'
ipcRenderer.on('aaa',()=>{

})

4.或者直接在渲染进程使用remote,它可以调用主进程对象的方法

import { remote } from 'electron';
const BrowserWindow = remote.BrowserWindow;
var win = new BrowserWindow({ width: 800, height: 600 });
win.loadURL('https://www.baidu.com');

但是这里要提一点的是 es6的引入electron是会报错的 # fs.existsSync is not a function

我目前有2种比较靠谱的解决方式:

1.可以在index.html 全局引入 创建一个js文件全局引入把对象挂载在window
    renderer.js:   global.electron = require('electron');
    index.html:
    <script>
      window.electron = require('electron');
    </script>
2.webpack配置文件中加入 target('electron-renderer') 
第1种方案遇到umi就难受了还要在src下创建document.ejs来引入
所以最好交给webpack来做
chainWebpack (config,{webpack}) {
    config
      .target('electron-renderer') 
},

打包

electron-packager 和 electron-builder
1.注意点
    不是history模式 如果使用了打包后就会找不到css和js文件 访问路径不对
    项目路径不能有中文 巨坑
    electron-builder一定要安在devDependencies下
    网络不好会很难 因为要重新下载electron 配置.npmrc文件
    使用electron-packager注意文件权限和需要忽略的文件
2.使用electron-packager
    缺点: 不支持更新安装 可配置项比较少
    优点: 由于不做复杂的操作 配置项也是基础的 所以打包速度快
    
"packager-build""electron-packager . app --out BIMapp --arch=x64 --icon ./public/bim.ico  --overwrite --ignore=node_modules"
//
app是应用的名称
--out 打包完的可执行文件,放在在哪里
--arch操作系统
-- icon应用图标
--overwrite是否覆盖原有文件
ignore需要忽略的文件

image.png .exe就是打包好的应用程序 项目代码全都放在reources中

3.使用electron-builder
    打包前要先打包项目代码
 "builder": "cross-env npm run build && cross-env node electron-build/index.js",

electron-build/index.js:

const defaultConfig = {
  ia32: true,
  config: {
    'appId': appId, 
    'productName': productName, //项目名 这也是生成的exe文件的前缀名
    'copyright': 'lzg',//版权信息
    'asar': false, //是否需要签证
    'compression':normal' ,// 压缩级别  store | normal | maximum
    'extraMetadata': { // 打包时覆盖package.json
      'name': name // 默认安装目录
    },
    'directories': {//输出文件夹 带上版本信息
      'output': `app/music.test}/v${_package.version}`
    },
    'publish': {
      'provider': '', // 服务器提供商,也可以是GitHub
      'url': ''// 服务器地址
    },
    'files': [ 
      'main.js',
      'main/**/*',
      'from': 'dist/index',
      'to': 'dist',
      'filter': ['**/*', '!*.map']
    ],
    'dmg': { //ios系统
      'sign': false,//签名
      'icon':'', //图标
      // 'background': 'electron-build/resource/background.jpg' // 背景图
    },
    ...macSignConfig,
    'mac': {
      'identity': '', //身份信息
      'hardenedRuntime': true,
      'gatekeeperAssess': false,
      'entitlements': 'electron-build/entitlements.mac.plist',
      'entitlementsInherit': 'electron-build/entitlements.mac.plist',
      'target': [
        // 'dir'
        'dmg',
        'zip'
      ],
      'icon': '', //图标
      'category': 'io.bell.Education',
      'extendInfo': {
        'CFBundleURLTypes': 'codesprite',
        'NSMicrophoneUsageDescription': 'music需要您的麦克风权限',
        'NSCameraUsageDescription': 'music需要您的视像头权限'
      }
    },
    'win': { //win系统
      'artifactName': '姓名_v${version}.${ext}',//名称
      'icon': '', //图标
      'target': [
        {
          'target': 'nsis',
          'arch': [
            'x64'
          ]
        }
      ]
      // ...winSignConfig
    },
    'nsis': {//nsis相关配置,打包方式为nsis时生效
      'language': '0x0804',
      'oneClick': false,// 是否一键安装
      'allowToChangeInstallationDirectory': true,// 允许修改安装目录
      'allowElevation': true,// 允许请求提升,如果为false,则用户必须使用提升的权限重新启动安装程序。
      'createDesktopShortcut': true,// 创建桌面图标
      'createStartMenuShortcut': true,// 创建开始菜单图标
      'installerIcon':  '',// 安装图标
      'deleteAppDataOnUninstall': true//卸载清理用户数据的。
    }
  }
}
builder
  .build(defaultConfig)
  .then(results => {
  })
  .catch((err) => {
    console.log(err)
  })
        以上是我的常用打包配置信息 一些主要的说明都已经标注 

到此:你已经完成了一款electron项目的打包

image.png