electron桌面应用的使用

2,781 阅读4分钟

 很多人知道Vscode这一强大的编辑应用正是electron框架写的,出于兴趣和其方便性,我就把公司的项目进行了一个优化,使用electron 来把项目搞成桌面应用,废话不多说,步骤如下:

一: 安装Electron

electron 有点大,安装起来有点久,多点耐心哦~

npm install --save-dev electron

二: 项目的启动

开发环境下,在根目录下添加main.js,并附上

const { app, BrowserWindow } = require('electron')

// 保持对window对象的全局引用,如果不这么做的话,当JavaScript对象被
// 垃圾回收的时候,window对象将会自动的关闭
let win

function createWindow () {
  // 创建浏览器窗口。
  win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true
    }
  })

  // 加载index.html文件  这里不一定是index.html, 看你目录结构,我react 项目就是'./public/index.html'
  win.loadFile('index.html')

  // 打开开发者工具
  win.webContents.openDevTools()

  // 当 window 被关闭,这个事件会被触发。
  win.on('closed', () => {
    // 取消引用 window 对象,如果你的应用支持多窗口的话,
    // 通常会把多个 window 对象存放在一个数组里面,
    // 与此同时,你应该删除相应的元素。
    win = null
  })
}

// Electron 会在初始化后并准备
// 创建浏览器窗口时,调用这个函数。
// 部分 API 在 ready 事件触发后才能使用。
app.on('ready', createWindow)

// 当全部窗口关闭时退出。
app.on('window-all-closed', () => {
  // 在 macOS 上,除非用户用 Cmd + Q 确定地退出,
  // 否则绝大部分应用及其菜单栏会保持激活。
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  // 在macOS上,当单击dock图标并且没有其他窗口打开时,
  // 通常在应用程序中重新创建一个窗口。
  if (win === null) {
    createWindow()
  }
})

// 在这个文件中,你可以续写应用剩下主进程代码。
// 也可以拆分成几个文件,然后用 require 导入。

然后在package.json 上添加命令

 "scripts": {
    "start": "electron ."
  }

此时运行npm start / yarn start   electron 应用就启起来了,是不是很简单;

三, 项目的打包

electron 应用打包有两种方式:electron-packager 和 electron-builder;

一开始我是使用electron-packager,但是后面想做自动更新的功能,就转electron-builder进行打包

"package": "electron-packager ./build/ electron-test --all --out ~/ --electron-version 1.0.0",

后面使用electron-builder  进行打包,并结合electron-updater自动化更新;

3.1 首先就是安装啦:

yarn add electron-builder --dev
yarn add electron-updater
npm install electron-updater --save

3.2 配置package.json 文件

注:nsis配置不会影响自动更新功能,但是可以优化用户体验,比如是否允许用户自定义安装位置、是否添加桌面快捷方式、安装完成是否立即启动、配置安装图标等

  "build": {    "appId": "****.app",    "copyright": "back-manage",    "productName": "back-manage",    "nsis": {      "oneClick": true,      "perMachine": true,      "allowElevation": true,      "allowToChangeInstallationDirectory": true,      "createDesktopShortcut": true,      "runAfterFinish": true,      "installerIcon": "public/favicon1.png",      "uninstallerIcon": "public/favicon1.png"    },    "publish": [      {        "provider": "generic",        "url": "https://***"      }    ],    "dmg": {      "contents": [        {          "x": 410,          "y": 150,          "type": "link",          "path": "/Applications"        },        {          "x": 130,          "y": 150,          "type": "file"        }      ]    },    "mac": {      "icon": "public/favicon1.png",      "artifactName": "${productName}_setup_${version}.${ext}",      "target": [        "dmg",        "zip"      ]    },    "win": {      "icon": "public/favicon1.png",      "target": [        "nsis",        "zip"      ]    }  },

3.2 修改主进程main.js 文件(引入 electron-updater 文件,添加自动更新检测和事件监听)

注意,我这里使用的是react,打包这里使用的主进程文件不再是main.js, 而是public下面的electron.js

warn:  public/electron.js not found. Please see https://medium.com/@kitze/%EF%B8%8F-from-react-to-an-electron-app-ready-for-production-a0468ecb1da3

我修改后的eletron.js 

// 引入electron并创建一个Browserwindowconst { app, BrowserWindow,Menu,ipcMain } = require('electron');const { autoUpdater } = require('electron-updater');const path = require('path');const url = require('url');const uploadUrl = 'https://***/'; (这个地址是自动更新会去对比这个服务器上的latest.yml,检测
最新版本)// 保持window对象的全局引用,避免JavaScript对象被垃圾回收时,窗口被自动关闭.let mainWindow;// 检测更新,在你想要检查更新的时候执行,renderer事件触发后的操作自行编写function updateHandle() {  const message = {    error: '检查更新出错',    checking: '正在检查更新……',    updateAva: '检测到新版本,正在下载……',    updateNotAva: '现在使用的就是最新版本,不用更新'  };  const os = require('os');  autoUpdater.setFeedURL(uploadUrl);  autoUpdater.on('error', error => {    sendUpdateMessage(message.error);    // sendUpdateMessage(error);  });  autoUpdater.on('checking-for-update', () => {    sendUpdateMessage(message.checking);  });  autoUpdater.on('update-available', info => {    console.log(info)    mainWindow.webContents.send('updateAvailable', '<h3>检测到新版本' + info.version + ',需要升级?</h3>' + info.releaseNotes);    // sendUpdateMessage(message.updateAva);  });  autoUpdater.on('update-not-available', info => {    sendUpdateMessage(message.updateNotAva);  });  // 更新下载进度事件  autoUpdater.on('download-progress', progressObj => {    console.log(progressObj)    const winId = BrowserWindow.getFocusedWindow().id;        let win = BrowserWindow.fromId(winId);        win.webContents.send('downloadProgress', progressObj);  });  autoUpdater.on('update-downloaded', (    event,    releaseNotes,    releaseName,    releaseDate,    updateUrl,    quitAndUpdate  ) => {    console.log(event,      releaseNotes,      releaseName,      releaseDate,      updateUrl,      quitAndUpdate)    console.log('update-downloaded');    ipcMain.on('isUpdateNow', (e, arg) => {      console.log(arguments);      console.log('开始更新');      // some code here to handle event      autoUpdater.quitAndInstall();    });    mainWindow.webContents.send('isUpdateNow');  });  ipcMain.on("isDownload", () => {    autoUpdater.downloadUpdate();})  ipcMain.on('checkForUpdate', () => {    // 执行自动更新检查    autoUpdater.checkForUpdates();  });}// 通过main进程发送事件给renderer进程,提示更新信息function sendUpdateMessage(text) {  mainWindow.webContents.send('message', text);}function createWindow() {  // 创建浏览器窗口,宽高自定义具体大小你开心就好  mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: {webSecurity: false, allowDisplayingInsecureContent: true, allowRunningInsecureContent: true,    nativeWindowOpen: true,      webSecurity: false,      nodeIntegration: true, // 是否完整的支持 node. 默认值为true      nodeIntegrationInWorker: true, // 是否在Web工作器中启用了Node集成      preload: path.join(__dirname, './renderer.js')  }});  /*    * 加载应用-----  electron-quick-start中默认的加载入口    mainWindow.loadURL(url.format({      pathname: path.join(__dirname, 'index.html'),      protocol: 'file:',      slashes: true    }))  */  // 加载应用----适用于 react 项目  //   mainWindow.loadURL('http://localhost:3000/');  // 加载应用----react 打包  mainWindow.loadURL(    url.format({      pathname: path.join(__dirname, './index.html'),      protocol: 'file:',      slashes: true    })    // 'http://192.168.0.11:8082'  );  // 打开开发者工具,默认不打开  // mainWindow.webContents.openDevTools()  // 关闭window时触发下列事件.  mainWindow.on('closed', () => {    mainWindow = null;  });  ipcMain.on('openDev', () => {    mainWindow.openDevTools();  });  updateHandle()}// 当 Electron 完成初始化并准备创建浏览器窗口时调用此方法app.on('ready', createWindow);// 所有窗口关闭时退出应用.app.on('window-all-closed', () => {  // macOS中除非用户按下 `Cmd + Q` 显式退出,否则应用与菜单栏始终处于活动状态.  if (process.platform !== 'darwin') {    app.quit();  }});app.on('activate', () => {  // macOS中点击Dock图标时没有已打开的其余应用窗口时,则通常在应用中重建一个窗口  if (mainWindow === null) {    createWindow();  }});// 你可以在这个脚本中续写或者使用require引入独立的js文件.

3.3 在项目启动app.js添加相应的检测更新的代码

因为在app.js 中是获取不到electron 的,我们需要在index.html中注入electron

window.electron = require('electron');

然后在app.js 

const ipcRenderer = window.electron && window.electron.ipcRenderer;
然后在componentDidMount 或者useEffect 函数中,添加:    const self = this;    if (ipcRenderer) {      ipcRenderer.send('checkForUpdate');      ipcRenderer.on('message', (event, message) => {        console.log(message);      });      // 注意:“downloadProgress”事件可能存在无法触发的问题,只需要限制一下下载网速就好了      ipcRenderer.on('downloadProgress', (event, progressObj) => {        console.log('下载', progressObj);        this.downloadPercent = progressObj.percent || 0;      });      ipcRenderer.on('isUpdateNow', () => {        console.log('是否现在更新');        ipcRenderer.send('isUpdateNow');      });      // 检测到新版本      ipcRenderer.on('updateAvailable', (event, message) => {        console.log(event, message);        self.$notification.open({          message: 'Notification',          description: '检测到新版本,正在更新中……',          onClick: () => {            console.log('Notification Clicked!');          }        });        ipcRenderer.send('isUpdateNow');      });    }

在componentWillUnmount 钩子函数中移除事件在componentWillUnmount 钩子函数中移除事件  componentWillUnmount() {    if (ipcRenderer) {      ipcRenderer.removeAll([        'message',        'downloadProgress',        'isUpdateNow',        'updateAvailable'      ]);    }  }

万事具备,只差一个执行命令的赶脚了有木有,走你……

但是,window 系统的包是打包好了,但是macOs 的出现了签名问题,如果没有自动更新的问题,用packager 打包出来的是不需要签名的,但是要实现自动更新,签名是必须的,

cannot find valid "Developer ID Application" identity or custom non-Apple code signing certificate


首先说一下,要处理这个问题,首先你要有个苹果开发者账号,个人或者公司的都行,我当时花了99美元买了一个个人开发者账号,需要里面的cer 证书来生成p12文件

具体可以参考链接: segmentfault.com/a/119000001…

好了,经过这一系列操作过后,你已经很够打包出一个能够自动更新的桌面应用,是不是很有成就感?

但是,问题,又来了,打包出来的应用无法进行复制黏贴,咋整?

解决:

1. 安装 electron-localshortcut

yarn add electron-localshortcut

2. 在主进程文件createWindow 函数添加如下代码:

  if (process.platform === 'darwin') {    Menu.setApplicationMenu(Menu.buildFromTemplate([]));    const contents = mainWindow.webContents;    const localShortcut = require('electron-localshortcut');    localShortcut.register(mainWindow, 'CommandOrControl+A', () => {      contents.selectAll();    });    localShortcut.register(mainWindow, 'CommandOrControl+C', () => {      contents.copy();    });    localShortcut.register(mainWindow, 'CommandOrControl+V', () => {      contents.paste();    });  }

最后附上相应的git地址

refer git address: https://github.com/catherine201/electron-example.git


多谢阅读~~