5

背景

最近新项目的需求是开发桌面应用,因此需要引入Electron。而桌面应用与浏览器web服务的主要区别就在于代码是不是在浏览器中跑的。所以我们仍然沿用web开发时的主流开发框架angular。

于是,新项目前端技术为electron + angular。

Electron + angular初始化

首先,非常重要的一点是:node版本必须高于18.16.1,因为低于这个版本的node无法支持electron。

然后,先初始化一个angular项目(初始化后如图,这个不再赘述)。
image.png

接着,我们需要安装electron,在angular项目的根目录下执行

ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/ npm install --save-dev electron 

安装成功后可以看到package.json里有了electron的依赖:
image.png

为了给我们的angular项目套上electron,我们还需要去重新设置一下入口文件:
image.png

然后在根目录下新建main.js,按照electron的方式配置启动路径,指向angular项目。

// 根目录下的main.js文件
const {app, BrowserWindow} = require('electron');
const url = require('url');
const path = require('path');

function onReady () {
  win = new BrowserWindow({width: 1600, height: 1000})
  loadUrl();

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

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

function loadUrl() {
  win.loadURL(url.format({
    pathname: path.join(
      __dirname,
      'dist/angular-electron-app/index.html'),
    protocol: 'file:',
    slashes: true
  }))
}

//在ready事件里
app.on('ready', async () => {
  onReady();
})


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

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

去到src/index.html,将

<base href="/">

改为

<base href="./">

最后在脚本中加入启动命令:
image.png

根目录下终端运行命令

npm run electron

即可启动成功:
image.png

配置快捷键调起控制台

electron窗口毕竟不是浏览器,该窗口中的控制台是由main.js中的 openDevTools()方法控制的,前文的main.js中配置了在窗口打开时控制台打开。如果在开发过程中不小心把控制台给关了,是无法通过在electron窗口中的操作调起控制台的。想要再打开就只能去到终端ctrl + c后再执行一次electron启动命令。

搞开发的都知道,没有控制台的开发就像断了一条腿一样难受。

所以我们还需要对控制台的快捷调起进行一番配置。

electron框架提供的与计算机交互的接口中有一个 globalShortcut,官方对它的描述是:
image.png

globalShortcut 可以检测键盘事件,还可以向操作系统注册/注销全局快捷键。有了它,我们就可以实现我们想要的功能了。

首先在main.js中,引入 globalShortcut

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

然后将

//在ready事件里
app.on('ready', async () => {
  onReady();
})

换成

//在ready事件里
app.on('ready', async () => {
  // Ctrl + Shift + i 快捷键调起/关闭控制台
  globalShortcut.register('CommandOrControl+Shift+i', function () {
    // 判断现在控制台是否处于打开状态
    if (win.webContents.isDevToolsOpened()) {
      // 如果被打开,则关闭
      win.webContents.closeDevTools()
    } else {
      // 如果没有被打开,则调起
      win.webContents.openDevTools()
    }
  })
  onReady();
})

这里使用的是Ctrl + Shift + i 快捷键调起控制台,当然也可以换成别的。

配置热加载

熟悉angular的朋友们都知道,angular的一个很人性化的点就是热加载,当代码有所变动时浏览器中会自动检测到并刷新,而不用手动刷新。因此,使用angular从web开发转为桌面程序开发时自然也是希望开发中保持这个特点。所以还需要配置一下热加载。

我们先回顾一下前文配置:

// main.js中配置的指向angular项目的启动路径
function loadUrl() {
  win.loadURL(url.format({
    pathname: path.join(
      __dirname,
      'dist/angular-electron-app/index.html'),
    protocol: 'file:',
    slashes: true
  }))
}

由于指向的路径是ng build后的dist文件夹下的angular项目,所以这样自然是无法实现热加载的,因为我们不可能改一点代码就ng build一次,这不现实。

网上查到的资料来看,大多数的做法及原理都很简单,将electron加载路径由dist/下的angular项目改为 http://localhost:4200 ,即将上述配置改为:

function loadUrl() {
  win.loadURL('http://localhost:4200');
}

然后在启动electron窗口之前先启动angular,即在一个终端中先 ng s,再在另一个终端中 npm run electron

这样做的确可以实现electron窗口中angular项目的热加载。

但存在一个很重要且必须要注意的问题,就是electron打包时不能就这样打包。如果就这样打包发给用户的话,用户双击启动后只会看到一个空白的窗口,因为angular服务并没有启动但electron却加载了angular服务的端口。所以打包时加载路径仍然应该配置dist/下的angular项目。

这也就意味着,我们在打包时必须去手动更换路径的配置,这是很难受的一件事。

还有一点,居然要开两个终端,执行两条启动命令,总觉得很不爽。

那么,有没有办法解决上述两个痛点,使得我们的项目更加完美呢?答案当然是有的。

1.用wait-on、&& 与 & 以合并两条启动命令为一个脚本

我们知道,在命令行中可以用 && 和 & 来合并多条命令使其一起执行。

command1 && command2 先执行command1,等待command1完成后,才执行command2。

command1 & command2 先执行command1,不用等待command1完成,直接执行command2。

如此一来,是不是将 ng snpm run electron 用 && 或 & 连接成一个脚本就可以了呢?很遗憾,经过尝试都不行,并非那么简单。

为什么 ng s && npm run electron 不行?因为 ng s 并不会完成,除非手动ctrl + c取消,否则会在终端中一直运行,根本就不会执行后面的 npm run electron

为什么 ng s & npm run electron 不行?这么写electron的确如此能够启动起来,但是由于angular项目的启动需要时间的,且需要的时间大于electron启动的时间,所以当electron启动时,angular还未启动完成,此时 http://localhost:4200是什么都没有的,所以electron启动起来后窗口中只能看到空白。

所以我们还需要一个东西:wait-on。根目录下执行安装命令:

npm install wait-on --save-dev

安装成功后,继续优化我们的脚本命令:

"scripts": {
    "ng": "ng",
    "start": "ng serve",
     ...
    "electron": "npm start & wait-on http-get://localhost:4200/ && electron ."
  },

上述electron脚本中加入 wait-on http-get://localhost:4200/ 的作用就是检测 localhost:4200 端口是否有服务,只有被启动成功才会执行后面的 electron . 去启动electron。

这样做,我们就成功地将两个命令合并成了一个脚本,启动项目时只需在一个终端窗口中执行 npm run electron 即可。

2.用环境变量实现开发和打包时应该加载不同路径的自动选择

前文提到,开发时和打包时的加载路径是不一样的,这就意味着项目在开发环境与生产(打包)环境间切换时,都需要我们手动去更换路径。这也是难以让人接受的。所以我们还应该增加一些配置实现不同情况下加载不同路径的自动选择。

有一个可以在命令行中设置且能够在electron的主进程中捕获到的东西,叫作环境变量。我们可以在脚本中设置环境变量,以区分现是在开发还是在打包,从而自动选择其对应的路径。

首先在我们的启动脚本中设置环境变量:BUILD_TYPE=develop

"scripts": {
    "electron": "npm start & wait-on http-get://localhost:4200/ && BUILD_TYPE=develop electron ."
  },

然后在主进程(main.js)中的 onReady() 方法中去捕获 BUILD_TYPE

function onReady () {
  ...
  // 获取脚本(命令行)中配置的环境变量
  const buildType = process.env.BUILD_TYPE;
  if (buildType === 'develop') {
    // 加载开发时的路径
    console.log('加载开发时的路径');
    loadUrlWhenDevelop();
  } else {
    // 加载打包时的路径
    console.log('加载打包时的路径');
    loadUrlWhenPackage();
  }
  ...
}

这样配置就可以做到不同情况时自动加载不同路径了。

最后再补充一点,关于如何打包甚至是跨平台打包的内容,请点击这里

完整main.js

const {app, BrowserWindow, globalShortcut} = require('electron');
const url = require('url');
const path = require('path');

function onReady () {
  win = new BrowserWindow({width: 1600, height: 1000})
  // 获取脚本(命令行)中配置的环境变量
  const buildType = process.env.BUILD_TYPE;
  if (buildType === 'develop') {
    // 加载开发时的路径
    console.log('加载开发时的路径');
    loadUrlWhenDevelop();
  } else {
    // 加载打包时的路径
    console.log('加载打包时的路径');
    loadUrlWhenPackage();
  }

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

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

function loadUrlWhenPackage() {
  // 打包时的启动路径为angular项目ng build后的dist/下的项目文件
  win.loadURL(url.format({
    pathname: path.join(
      __dirname,
      'dist/line-data-record/index.html'),
    protocol: 'file:',
    slashes: true
  }))
}

function loadUrlWhenDevelop() {
  // 开发时为了保留angular热加载特性,所以启动路径指向localhost:4200
  win.loadURL('http://localhost:4200');
}

//在ready事件里
app.on('ready', async () => {
  // Ctrl + Shift + i 快捷键调起/关闭控制台
  globalShortcut.register('CommandOrControl+Shift+i', function () {
    if (win.webContents.isDevToolsOpened()) {
      win.webContents.closeDevTools()
    } else {
      win.webContents.openDevTools()
    }
  })
  onReady();
})


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

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

完整package.json

{
  "name": "angular-electron-app",
  "version": "0.0.0",
  "main": "main.js",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "watch": "ng build --watch --configuration development",
    "test": "ng test",
    "electron": "npm start & wait-on http-get://localhost:4200/ && BUILD_TYPE=develop electron .",
    "package": "npm run build && electron-forge package",
    "make": "npm run build && electron-forge make"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^15.2.0",
    "@angular/common": "^15.2.0",
    "@angular/compiler": "^15.2.0",
    "@angular/core": "^15.2.0",
    "@angular/forms": "^15.2.0",
    "@angular/platform-browser": "^15.2.0",
    "@angular/platform-browser-dynamic": "^15.2.0",
    "@angular/router": "^15.2.0",
    "electron-squirrel-startup": "^1.0.0",
    "rxjs": "~7.8.0",
    "tslib": "^2.3.0",
    "zone.js": "~0.12.0"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^15.2.9",
    "@angular/cli": "~15.2.9",
    "@angular/compiler-cli": "^15.2.0",
    "@electron-forge/cli": "^6.4.2",
    "@electron-forge/maker-deb": "^6.4.2",
    "@electron-forge/maker-rpm": "^6.4.2",
    "@electron-forge/maker-squirrel": "^6.4.2",
    "@electron-forge/maker-zip": "^6.4.2",
    "@electron-forge/plugin-auto-unpack-natives": "^6.4.2",
    "@types/jasmine": "~4.3.0",
    "electron": "^26.2.1",
    "jasmine-core": "~4.5.0",
    "karma": "~6.4.0",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage": "~2.2.0",
    "karma-jasmine": "~5.1.0",
    "karma-jasmine-html-reporter": "~2.0.0",
    "typescript": "~4.9.4",
    "wait-on": "^7.0.1"
  }
}

仓库地址

https://github.com/HHepan/angular-electron-app

参考资料

https://www.logicflow.ai/blog/angular-desktop-applications-wi...

https://zhuanlan.zhihu.com/p/298530109

https://blog.csdn.net/qq_40282732/article/details/...


HHepan
164 声望13 粉丝

河北工业大学