Electron 企业级应用开发实战(一)

lxf2023-03-11 18:03:01

万丈高楼平地起,功能再强大、逻辑再复杂的应用,也是从零搭建出来的,本文就为大家讲解如何从头开始打造一款 Electron 企业级跨平台桌面应用。

项目搭建

现在很多脚手架可以快速创建项目,虽然方便,但是很容易让初学者一下子陷入很多配置文件当中,不利于理解项目背后的原理,因此本文不使用任何脚手架,从零开始搭建项目,一步步创建目录和文件。首先执行以下命令:

$ mkdir electron-desktop
$ cd electron-desktop
$ yarn init -y
$ yarn add electron electron-packager --dev

上面的命令会创建一个空白的项目,包含一个 package.json 文件和 node_modules 目录下安装的依赖,接下来我们创建一个 src 目录,里面再分别创建两个子目录 main 和 renderer,此时目录结构如下 :

src
├── main
└── renderer

在 Electron 中有主进程渲染进程两个重要的概念,我们在主进程里面用 Node.js 代码调用 Electron 封装好的 API 来创建窗口,管理应用整个生命周期,而在渲染进程里面加载传统的 Web 界面。因此 main 目录用于存放跟主进程相关的代码,renderer 目录用于存放跟渲染进程相关的代码。

整个桌面应用的入口在主进程里面,接下来在 main 目录中创建 index.js 作为入口,为了让 Electron 知晓该入口,我们需要在 package.json 中做以下指定:

"main": "src/main/index.js",
"scripts": {
  "start": "electron ."
},

到这里,一个基础的 Electron 项目就搭建完毕了,一切准备就绪。

创建窗口

在 src/main/index.js 入口文件中,我们增加以下代码:

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

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

function createWindow() {
  const mainWindow = new BrowserWindow({ width: 800, height: 600 })
  mainWindow.loadURL('https://www.Admin.net')
}

然后 npm start启动项目,不出意外的话,你会看到一个加载AdminJS官网的窗口弹出来了::

Electron 企业级应用开发实战(一)

是不是很简单呢?这里需要注意的是,创建窗口的调用必须在 app 的 ready 事件之后,否则会报错。在 Electron 中,大部分的 API 都需要在 ready 之后进行调用,因为程序启动需要做很多初始化的事情,例如运行环境检测、相关依赖加载等,在此之前很多 API 还没有准备好。

打包应用

是的,我们已经可以打包应用了,只需要执行下面的命令:

$ npx electron-packager . --overwrite

如果你是 Mac 系统,会生成 electron-desktop-darwin-x64 目录,里面有个 electron-desktop.app,双击打开这个 app 就能直接运行。如果是 Windows 系统,会生成 electron-desktop-win32-x64 目录,里面有 electron-desktop.exe,双击也能直接运行。

electron-packager 生成的目录名的规则是:应用名-平台-架构(appName-platform-arch),如果你是苹果 M1 的电脑,目录名会是 electron-desktop-darwin-arm64

为了后续打包更方便,我们可以把这个命令也封装到 package.json 的 scripts 里面:

"scripts": {
  "start": "electron .",
  "pack": "electron-packager . --overwrite"
},

不过需要注意的是:electron-packager 打出来的这种包是绿色包,不能直接分发给用户使用。在 Mac 平台上一般要做成 dmg 或者 pkg 包,在 Windows 平台上要做成 exe 安装包,这部分的内容会在后续详细讲解。

实例运行

开发一款企业级应用之前,从产品侧要定义好该应用是否支持用户打开多个实例。默认情况下,Electron 应用就是多实例的,例如在 Windows 上,用户每双击一次 exe 就会开启一次应用,而在 Mac 系统上面,如果应用已经启动,再次双击并不会重新启动应用,而是唤起当前正在运行的实例,不过用户可以通过下面的方式开启多实例:

  1. 右键 electron-desktop 应用,点击「显示包内容」

Electron 企业级应用开发实战(一)

  1. 双击 Contents/MacOS/electron-desktop 可执行文件

Electron 企业级应用开发实战(一)

你会发现,每双击一次就会启动一个新的实例。而对于绝大多数桌面应用来说,单实例就够了,可以用下面的方法限制不允许启动多个实例:

const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
  app.quit()
}

requestSingleInstanceLock 方法用于抢占实例运行锁,只有第一个启动的实例(第一实例)才返回 true,而一旦锁被强占之后,后续启动的其他实例(第二实例)再调用这个方法就会返回 false,从而进入到 if 语句,将其强制退出,从而确保只有一个实例运行。

当第二实例被强制退出的时候,一般需要把第一实例的窗口显示到前台,这样给用户的感觉就像唤起了应用程序,否则用户可能并没有意识到之前已经启动过该程序了,只会纳闷为啥当前的程序启动不了,体验上会非常不友好。因此 Electron 也提供了 second-instance 事件用于在「第一实例」里面监听「第二实例」启动的行为,来驱动「第一实例」对该行为做出合适的反馈,例如:如果窗口在后台就显示出来,如果窗口最小化就恢复窗口等。最终的代码如下:

const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
  app.quit()
} else {
  app.on('second-instance', (event, argv, workingDirectory) => {
    mainWindow.restore() // 从最小化窗口恢复
    mainWindow.show() // 从后台显示
  })
}

自定义协议

企业级桌面应用一般都会定义自己的专属协议,我们可能都遇到过这样的场景:

  • 网页上点击聊天按钮,自动打开 QQ 软件
  • 在百度网盘点击下载,自动打开百度网盘软件

Electron 企业级应用开发实战(一)

这是怎么做到的呢?就是通过自定义协议。所谓自定义协议,其实就是给应用起个独一无二的名称,然后注册到操作系统里面,凡是通过这个协议名就能唤起这个软件了,在 Electron 中注册协议只需要一行代码:

app.setAsDefaultProtocolClient('electron-desktop')

注册之后,当在浏览器中输入 electron-desktop:// 之后,会发现弹出跳转提示,点击同意就能启动并跳转到桌面应用了,通过这种协议唤起应用被称为 scheme 唤起,而且在唤起的时候还可以带上一些参数,例如:

electron-desktop://width=800&height=600

scheme 唤起的行为是操作系统默认支持的,操作系统也提供了 API 来监听唤起事件并拿到唤起参数。

关于自定义协议相关的资料:

  • Mac 端:developer.apple.com/documentati…
  • Windows 端:learn.microsoft.com/en-us/windo…

获取协议参数

自定义协议之后,可以用 scheme 唤起桌面应用,这是非常重要的能力,这里面最关键的是需要拿到协议唤起参数,否则唤起 QQ 之后不知道要跟谁聊天,唤起百度网盘之后不知道要下载哪款资料。

在 Mac 和 Windows 上获取协议唤起参数是不一样的,这是由于平台策略不同导致的,所以需要单独讲解。

Mac 端协议唤起

在 Mac 上面通过监听 open-url 事件,可以拿到唤起的 scheme 参数:

app.on('open-url', (event, url) => {
  console.log(url) // 打印 electron-desktop://width=800&height=600
})

url 里面就是 scheme 唤起的完整地址字符串,除了开头的 electron-desktop:// 前缀之外,后面的内容是完全交给用户自定义的,例如:

  • electron-desktop://hello-juejin
  • electron-desktop://1+1=2

这些都可以唤起,上面之所以用 width=800&height=600完全是因为模仿 http 地址栏的 query 参数的格式,有现成的 API 方便解析参数而已。下面给出完整的示例,把 open-url 的回调获取到的 scheme 参数解析出来放到全局变量 urlParams 里面:

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

const protocol = 'electron-desktop'
app.setAsDefaultProtocolClient(protocol)

let urlParams = {}

app.on('open-url', (event, url) => {
  const scheme = `${protocol}://`
  const urlParams = new URLSearchParams(url.slice(scheme.length))
  urlParams = Object.fromEntries(urlParams.entries())
})

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

function createWindow() {
  const mainWindow = new BrowserWindow({ width: 800, height: 600 })
  mainWindow.loadURL('https://www.Admin.net')
}

协议唤起在 Mac 平台上有两点需要注意:

  • open-url 要在 ready 事件之前注册,因为有些场景是需要拿到参数之后再决定如何创建窗口的,如果放在 ready 回调里面,createWindow 可能会拿不到该参数了。
  • 在应用支持多实例场景下
    • 如果程序未启动,会立即启动应用,在 open-url 中获取到唤起参数
    • 如果存在正在运行的实例(可能有多个),会激活(其中一个)已经运行的程序,而不会开启新的实例,被激活的实例可以通过 open-url 回调获取唤起参数

Windows 端协议唤起

Windows 平台上没有提供 open-url 事件,而是会把 scheme 作为启动参数传递给应用程序,在代码里面可以用 process.argv 拿到所有参数,它是一个数组,格式如下:

["electron-desktop.exe", "--allow-file-access-from-files", "electron-desktop://width=400&height=300"]

第一个参数是应用程序的路径,后面的就是其他的启动参数,如果是 scheme 唤起的,也会在里面,所以可以用下面的代码进行判断:

const url = process.argv.find(v => v.startsWith(scheme))
if (url) { // 如果发现 electron-desktop:// 前缀,说明是通过 scheme 唤起
	console.log(url)
}

如果程序支持多示例,每次都会启动新的程序,上面的代码就够用了。但如果是单实例的场景,情况就稍稍不同了,因为本质上还是会打开新的程序,只不过程序里判断单实例锁被占用,从而则立即退出,所以必须要有办法在 scheme 唤起的时候,能够通知到当前正在运行的那个实例。这里用到的仍然是 second-instance 事件:

const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
  app.quit()
} else {
  app.on('second-instance', (event, argv, workingDirectory) => {
    // Mac 平台只需要展示窗口即可
    mainWindow.restore()
    mainWindow.show()

    // Windows 平台上需要判断新的实例是否被 scheme 唤起
    const url = argv.find(v => v.startsWith(scheme))
    if (url) { // 如果发现 electron-desktop:// 前缀,说明是通过 scheme 唤起
    	console.log(url)
    }
  })
}

关键在于第二个参数 argv,如果是通过 scheme 唤起的话,argv 里面会包含 scheme 协议,与 process.argv 类似,格式是一个数组,第一项就是 electron-desktop.exe 的位置,后面是一些参数,例如:

["electron-desktop.exe", "--allow-file-access-from-files", "electron-desktop://width=400&height=300", "C:\Windows\system32"]

「桌面AdminJS」实战

一篇文章大而全不利于知识的理解和吸收,今天讲到了很多知识点都非常重要,接下来通过一个实战案例把上面讲到的内容串起来:做一个「桌面AdminJS」,需求是:

  • 可以打包成 juejin.appjuejin.exe桌面应用

  • 打开应用后立即进入AdminJS首页

  • 限制桌面AdminJS为单实例运行

  • 支持用 juejin:// 这个 scheme 唤起应用

  • 支持用 juejin://width=500&heigh=300这个 scheme 指定窗口大小

我们把传统的 Web 页面通过用 Electron 加载出来的方式叫做「套壳桌面应用」,这是将网站做成桌面软件的最快速的方式。

最终效果如下:

Electron 企业级应用开发实战(一)

附上完整代码:

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

let mainWindow

const protocol = 'juejin'
const scheme = `${protocol}://`
app.setAsDefaultProtocolClient(protocol)

let urlParams = {}

handleSchemeWakeup(process.argv)

const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
  app.quit()
} else {
  app.on('second-instance', (event, argv) => {
    mainWindow.restore()
    mainWindow.show()
    handleSchemeWakeup(argv)
  })
}

app.on('open-url', (event, url) => handleSchemeWakeup(url))

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

function createWindow() {
  const width = parseInt(urlParams.width) || 800
  const height = parseInt(urlParams.height) || 600
  if (mainWindow) {
    mainWindow.setSize(width, height)
  } else {
    mainWindow = new BrowserWindow({ width, height })
    mainWindow.loadURL('https://www.Admin.net')
  }
}

function handleSchemeWakeup(argv) {
  const url = [].concat(argv).find((v) => v.startsWith(scheme))
  if (!url) return
  const searchParams = new URLSearchParams(url.slice(scheme.length))
  urlParams = Object.fromEntries(searchParams.entries())
  if (app.isReady()) createWindow()
}

本文正在参加「 . 」