第四章 Electron开发进阶

1,077 阅读33分钟

本文所有源码均在:github.com/Sunny-117/e…

本文收录在《Electron桌面客户端应用程序开发入门到原理》掘金专栏

本文介绍

这一章的内容通常和业务逻辑关系不大,更多的是关于应用的健壮性以及一些周边功能的介绍。

  • 应用打包
  • 应用更新
  • 单元测试
  • 应用安全性
  • 异常处理
  • 日志处理
    • 日志的记录
    • 日志的上传

打包macOS应用

在 Electron 中,要对应用进行打包,可用方案有好几套:

  • Electron Packager
  • Electron Builder
  • Electron Forge

Electron Packager

Electron Packager 是一个用于 Electron 应用的命令行工具,能够帮助我们将 Electron 应用打包成各个平台(Windows, macOS, Linux)的可分发格式。它提供了很多自定义选项,比如设置图标、应用名称、版本号等。结合其他的工具,然后再使用 Electron Packager 可以很方便地为应用生成不同平台的安装包。

特点:

  • 它是一个简单、灵活的工具,适合于快速将Electron应用打包成可执行文件。
  • 支持多平台打包,包括Windows、macOS和Linux。
  • 允许自定义打包选项,如应用图标、版本号、应用名称等。
  • 不内置生成安装程序的功能,但你可以结合其他工具(如NSIS、DMG)来创建安装包。

适用场景:

  • 适合于小型或中型项目,对打包过程的要求不是特别复杂。
  • 当你只需要简单地将应用打包成可执行文件,不需要额外的安装程序或更新机制时。

Electron Builder

Electron Builder 针对大多数构建任务重新编写了自己的内部逻辑,提供了丰富的功能,包括代码签名、发布支持、文件配置、多种目标构建等。Electron Builder 不限制使用的框架和打包工具,使得可以更加灵活地进行配置和打包。

特点:

  • 提供了一套全面的解决方案,包括打包、创建安装程序、自动更新等。
  • 支持广泛的安装包格式,如NSIS、AppImage、DMG、Snap等,适用于不同平台。
  • 高度可配置,可以通过 electron-builder.yml(或 json、toml)文件详细控制打包和分发过程。
  • 内置自动更新机制,与GitHub、S3等服务紧密集成,方便应用发布和更新。

适用场景:

  • 适合于需要复杂打包和分发流程的中大型项目。
  • 当你需要创建专业的安装程序,并实现自动更新功能时。

Electron Forge

Electron Forge 因为是官方维护的产品,所以当 Electron 支持新的应用程序构建功能时,它会立即集成这些新的能力。另外,Electron Forge 专注于将现有的工具组合成一个单一的构建流程,因此更易于跟踪代码的流程和扩展。

特点:

  • 是一个综合性的工具,提供了开发到打包的全流程支持。
  • 集成了Webpack、Electron Packager和Electron Builder的部分功能,提供了一站式的开发体验。
  • 通过插件系统扩展功能,支持自定义Webpack配置、React、Vue等前端框架。
  • 简化了开发和打包流程,通过简单的命令即可启动开发环境、打包和创建安装程序。

适用场景:

  • 适合于所有规模的项目,特别是那些希望通过一套工具管理整个Electron应用生命周期的项目。
  • 当你需要一个简单而全面的解决方案,不仅包括打包,还包括开发过程中的实时重新加载、打包优化等。

针对上面三个工具,简单总结一下:

  • Electron Pakcager:简单,灵活,适合于打包的基础需求。
  • Electron Builder:全面、功能丰富,支持各种各样的配置,适合于需要复杂打包流程和高度定制的项目。
  • Electron Forge:官方出品,集成度高,可以一站式管理 Electron 应用的生命周期。

图标

关于图标这一块儿,我们又不是 UI,所以可以去找一些现成的免费的 icon 来用。

Icon Generator:icongenerator.net/

在该网站找到一个合适的图标,下载之后放置于你的项目对应的目录里面即可。

另外,在打包 macOS 应用的时候,需要的不仅仅是一个图标,而是**一组图标**,一组不同尺寸大小的图标,方便应用在不同地方显示合适尺寸的图标。

这里我们可以借助 electron-icon-builder 这个插件,可以快速的基于我们所提供的图标模板生成一套不同尺寸的图标。

npm install electron-icon-builder -D

之后就可以在 package.json 里面配置一条脚本命令:

"scripts": {
  "build-icon": "electron-icon-builder --input=./assets/markdown.png --flatten"
}

进行打包配置

主要是配置一个名为 build 的配置项,主要需要配置的内容如下:

  • appId:这个是我们应用的唯一标识符,一般会采用反向域名的格式。(假设我们应用的官网对应的地址:markdown.duyi.com,那么这里 appId 就是 com.duyi.markdown)

    • 来看一些有名的应用在 appId 上面的示例
    • Visual Studio Code: com.microsoft.vscode
    • Slack: com.tinyspeck.slackmacgap
    • WhatsApp: com.whatsapp.desktop
    • Skype: com.skype.skype
    • Discord: com.hnc.Discord
  • mac 配置

    • category:你的应用在 macOS 上面的一些类别,例如我们的 markdown 是属于工具类应用,那么在 macOS 平台,就有一个分类,名为 public.app-category.utilities
      • 这里来看一下其他有名的应用的分类填写示例
      • public.app-category.developer-tools: 开发工具,示例:Visual Studio Code, Sublime Text, Atom
      • public.app-category.utilities: 实用工具,示例:Alfred, CleanMyMac, DaisyDisk
      • public.app-category.social-networking: 社交网络,示例:Slack, WhatsApp Desktop, Telegram
      • public.app-category.music: 音乐,示例:Spotify, Apple Music
      • public.app-category.productivity: 生产力,示例:Microsoft Office Suite, Notion, Evernote
    • target:对应的是要打包的目标格式,值为一个数组,数组里面的值经常填写的为 dmg 和 pkg
  • dmg 格式相关配置

    • title:打包成 dmg 格式时,磁盘映像对应的标题
    • icon:对应的就是一组 icon 的目录
    • background:dmg 窗口的背景图路径
    • window:dmg 窗口的大小,通过 width 和 height 来进行指定
    • contents:指定 dmg 窗口里面,应用和目录具体显示的位置。
  • pkg 格式相关配置

    • installLocation:指定 pkg 安装包在进行安装的时候,将应用安装到的具体位置,一般也是 /Applications 这个位置

更多详细的关于 electron-builder 的配置信息,可以参阅:www.electron.build/index.html

下面是我们针对此次项目打包所做的配置信息,如下:

"build": {
  "appId": "com.duyi.markdown",
  "productName": "Markdown Editor",
  "mac": {
    "category": "public.app-category.utilities",
    "target": [
      "dmg",
      "pkg"
    ]
  },
  "dmg": {
    "title": "Markdown Editor",
    "icon": "./icons",
    "background": "./assets/background.jpeg",
    "window": {
      "width": 660,
      "height": 400
    },
    "contents": [
      {
        "x": 180,
        "y": 170
      },
      {
        "x": 480,
        "y": 170,
        "type": "link",
        "path": "/Applications"
      }
    ]
  },
  "pkg": {
    "installLocation": "/Applications"
  }
}

build 这个配置完成之后,我们又可以在 package.json 中添加一条脚本命令:

"scripts": {
  "build": "electron-builder"
}

记得要安装一下 electron-builder

npm install electron-builder -D

接下来运行 npm run build 就可以成功打包,注意打包的时候,有两个信息值得注意:

skipped macOS application code signing  reason=cannot find valid "Developer ID Application" identity or custom non-Apple code signing certificate, it could cause some undefined behaviour, e.g. macOS localized description not visible, see https://electron.build/code-signing allIdentities=     0 identities found

这里的提示信息表示在打包的过程中,跳过了代码签名的验证。

要解决这个问题,你需要有一个有效的 Apple 开发者证书,然后需要执行如下的步骤:

  1. 加入苹果开发者计划:如果还没有,你需要加入苹果开发者计划。这通常涉及到一些费用。
  2. 创建并下载证书:登录到你的苹果开发者账户,然后在证书、标识符和配置文件部分创建一个新的“Developer ID Application”证书。创建并下载这个证书到你的电脑。
  3. 安装证书到钥匙串:双击下载的证书文件,它会自动添加到你的钥匙串访问中。这样,electron-builder 就能在打包应用程序时使用这个证书了。
  4. 在 electron-builder 配置中指定证书:在你的 electron-builder 配置文件中(通常是 package.json 中的 build 部分),确保正确设置了代码签名的配置。例如,你可以在配置中指定证书的名称或位置。
"mac": {
  "category": "public.app-category.utilities",
  "target": ["dmg", "zip"],
  "identity": "Developer ID Application: [你的开发者名]"
}
  1. 重新打包应用程序:完成上述步骤后,再次使用 electron-builder 打包你的应用程序。这次应该不会出现之前的提示,因为 electron-builder 现在能找到并使用你的开发者 ID 证书进行代码签名了。
Detected arm64 process, HFS+ is unavailable. Creating dmg with APFS - supports Mac OSX 10.12+

这个不是错误,这个仅仅是一个提示信息,告诉你在新的 arm 芯片的 macOS 里面,不再支持 HFS+ 这种文件格式系统。

打包生成文件说明

  • Markdown Editor-1.0.0-arm64.dmg.blockmap:这个文件是和 dmg 文件相关的 map 文件,该文件主要的作用是为了支持增量更新。
  • com.duyi.markdown.plist:这是一个属性列表文件,通常用于 macOS 程序存储一些配置信息,例如应用程序的标识符、版本信息、安全权限等。
  • builder-debug.yml:通常是记录 electron-builder 详细的构建过程的日志信息。
  • builder-effective-config.yaml:该文件包含了在使用 electron-builder进行打包的时候,实际所使用的配置信息。也就是说,electron-builder 有一个默认的基础配置,然后结合我们所给的 build 配置,最终所生成的,实际所用的配置。

打包windows应用

打包windows应用基本上和上节课介绍的打包 macOS应用大同小异,但是有一些注意点:

  1. 在打包 windows 应用的时候,需要填写 author 字段。
  2. 关于图标,对应的是一个具体的 ico 格式的文件,而非一组文件
  3. nsis 配置打包出来就是 exe 文件,对应的常见配置项:
    • oneClick:false
      • 表示是否需要一键式安装程序,当你设置为 true 的时候,安装包在进行安装的时候,就不会给用户提供相应的选项(用户组的选择、安装路径的选择),全部按照默认配置去安装。
    • allowElevation:true
      • 安装程序是否在需要的时候,能够请求提升权限
        • true:表示安装包在进行安装的时候,如果遇到权限不足的情况,那么会向用户请求提升权限。
        • false:表示安装包在进行安装的时候,如果遇到权限不足的场景,直接安装失败。
    • allowToChangeInstallationDirectory:true
      • 布尔类型,如果是 true,表示允许用户在安装的过程中修改安装路径。
    • createDesktopShortcut:true
      • 是否创建桌面快捷方式
    • createStartMenuShortcut:true
      • 是否在 windows 系统的开始菜单创建快捷方式
    • shortcutName:string
      • 快捷方式显示的名称

asar文件

asar基本的介绍

asar 英语全称 Atom Shell Archive。翻译成中文“Atom层文件归档”。这个其实就是一种 Electron 自定义的文件格式而已。在 Electron 应用进行构建的时候,会把所有的源代码以及相关的资源文件都打包到这个文件里面。

asar 文件开头有一段 JSON,类似于如下的结构:

{
  "files": {
    "default_app.js": { "size": 38848, "unpacked": true },
    "icon.png": { "size": 1023, "offset": "0" },
    "index.html": { "size": 52792, "unpacked": true },
    "index.css": { "size": 21, "offset": "1023", "executable": true },
    "pickle.js": { "size": 4626, "offset": "1044" },
    "main.js": { "size": 593, "offset": "5670" }
    ...
  }
}

在开头,有一个 files,其对应的值就是被打包进来的文件的名称、大小以及该文件在 asar 文件内部的一个偏移值,这个偏移值(offset)非常重要,回头 Electron可以通过该偏移值在 asar 文件中找到具体的文件内容,从而加载文件内容。

Electron 之所以使用自定义的 asar 文件来存储源代码,有两个原因:

  • 性能优化:asar 文件是一个归档文件,这意味着将原本项目中成百上千的小文件合并成了一个单文件。那么操作系统在加载文件的时候,也就只需要加载一个大文件,而非数千个小文件。在某些操作系统中,可以显著的提高读取速度和应用启动的速度。
  • 避免文件路径的限制:例如在 windows 操作系统中,默认最长的资源路径的长度为 256 位字符串,那么打包为 asar 归档文件之后,使用的是虚拟路径,绕开了外部文件系统的限制。

制作asar

Electron 官方是提供了相应的工具,帮助我们制作 asar

npm install -g @electron/asar

安装完成后,就可以使用命令:

asar pack ./项目名 <asar文件名>.asar

注意,在使用 asar 打包的时候,切换到项目所在的父目录。

打包完成后,我们可以通过 asar list 的命令来查看打包了哪些文件进去。

asar list app.asar

使用asar文件

首先我们新创建一个项目,然后在主进程书写如下的代码:

const { app, BrowserWindow } = require("electron");
const path = require("path");
require(path.join(__dirname, "app.asar", "menu.js"));
const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
    },
  });

  win.loadFile(path.join(__dirname, "app.asar", "window/index.html"));
};

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

这样就可以将我们之前所写的项目跑起来了。

asar文件的意义

实际上像 electron-builder 这样的打包工具,一般在默认情况下也是将项目里面的所有文件进行 asar 归档操作,也方便我们后期介绍诸如像差分升级、electron-builder原理一类的知识。

应用更新

关于更新,我们这里存在两个的方面的准备工作:

  • 应用本身要有检查更新的能力
  • 准备一个提供资源的服务器

应用本身要有检查更新的能力

这里需要使用一个依赖库 electron-updater

npm install electron-updater

接下来,在主进程中添加检查更新的逻辑即可:

const { autoUpdater } = require("electron-updater");

autoUpdater.autoDownload = false; // 关闭自动下载更新,防止下载失败

// 接下来监听和更新相关的一系列事件,不同的事件做不同的事情

// 有更新可用的情况下会触发该事件
autoUpdater.on("update-available", async () => {
  const result = await dialog.showMessageBox({
    type: "info",
    title: "发现新版本",
    message: "发现新版本,是否立即更新?",
    buttons: ["是", "否"],
  });
  if (result.response === 0) {
    // 说明用户点击了是
    autoUpdater.downloadUpdate(); // 开始下载更新
  }
});

// 出错的时候会触发 error 事件
autoUpdater.on("error", (err) => {
  win.webContents.send("error", err.message);
});

// 监听下载进度
// 每次下载进度更新的时候,该事件就会触发
autoUpdater.on("download-progress", (progressObj) => {
  // 拼接一个下载进度的日志信息
  let log_message = "Download speed: " + progressObj.bytesPerSecond;
  log_message = log_message + " - Downloaded " + progressObj.percent + "%";
  log_message =
    log_message +
    " (" +
    progressObj.transferred +
    "/" +
    progressObj.total +
    ")";
  win.webContents.send("download-progress", log_message);
});

// 监听更新下载完成事件
autoUpdater.on("update-downloaded", () => {
  // 下载完成后,也给用户一个提示,询问是否立即更新
  dialog
    .showMessageBox({
      type: "info",
      title: "安装更新",
      message: "更新下载完成,应用将重启并安装更新",
      buttons: ["是", "否"],
    })
    .then((result) => {
      if (result.response === 0) {
        // 退出应用并安装更新
        autoUpdater.quitAndInstall();
      }
    });
});

// 需要在加载完成后检查更新
win.once("ready-to-show", () => {
  autoUpdater.checkForUpdatesAndNotify();
});

接下来我们需要去更新 package.json

"publish": [
  {
    "provider": "generic",
    "url": "http://127.0.0.1:3000/"
  }
],

generic 代表的意思是提供更新资源服务的服务器是 HTTP/HTTPS 的服务器,也就是我们自己的服务器,而非某个特定云服务提供商所提供的服务。

另外,需要将 mac 配置项里面的 target 值去掉,这样 target 所对应的值就是 default 默认,到时候进行打包的时候,就会生成 zip 和 dmg 格式的文件。

"mac": {
  "category": "public.app-category.utilities",
},

准备一个提供资源的服务器

这里选择 http-server 这个第三方库来作为我们的服务器。

首先安装 http-server

npm install http-server

然后修改 pacakge.json:

"scripts": {
  "start": "http-server static/ -p 3000"
},

最后在项目根目录下创建一个 static 目录,将更新的文件放入到该目录下即可。

单元测试

测试的种类:

  • 静态测试
  • 单元测试
  • 集成测试
  • E2E测试

静态测试

静态测试不会涉及到运行代码,而是在编写代码期间对代码进行检查和分析,捕获写代码时的错别字和类型错误。

对于前端开发人员来讲,静态测试更倾向于使用 TypeScript 以及 ESLint 等静态检查工具来找出代码问题。

单元测试

单元测试用于验证单独隔离的部分是否正常工作。它对软件中的最小可测试单元进行测试,通常是单个函数或方法。

单元测试的目的是确保每个单元在独立运行时都能正常工作。单元测试通常由开发人员编写,可以使用各种自动化测试工具进行。

集成测试

所谓集成测试,就是将多个单元组装在一起进行测试,集成测试可以帮助检测各个单元之间的接口问题,并且可以提前发现系统级别的问题。因此集成测试的目的是确保整个系统中的各个部分在连接起来的时候能够正常工作。

例如在前面的单元测试中,我们一般会尽可能的屏蔽连接数据库、发送网络请求等功能,这一块儿会使用 mock 的实现,但是到了集成测试的时候,就会连接真实的数据库、发送真实的网络请求,确保它们在协作时能够正常工作。

E2E测试

英语全称为 End To End,翻译成中文就是端到端测试。

这种测试会测试整个软件系统的完整性和功能性,包括用户界面、数据库、网络连接等各个方面。

一般来说,它会跑完整个应用(前端 + 后端),E2E 会利用一个很像用户行为的机器人来和你的应用进行交互,这样的测试就会像真实用户那样和应用进行交互。

vitest配置

export default {
  test: {
    globals: true,
    environment: "jsdom",
  },
};
  • test:对应的值为一个对象,包含了所有和测试相关的配置项目。

    • globals:会在全局作用域自动注入 Vitest 测试相关的函数,比如 describe、test、it、expect,这样的话我们就不需要在每个测试文件中单独导入这些方法了。
    • environment:我们设置的值为 jsdom,表示测试运行在一个模拟的浏览器环境中。我们安装了 jsdom 这个依赖,该依赖的作用是使用纯 JavaScript 来模拟浏览器浏览器环境,这样可以让我们在不启动浏览器的情况下,测试一些涉及 DOM 操作的代码。

    应用安全性

如果你构建的 Electron 应用目的主要显示本地内容,所有代码都是本地受信任的,即使有远程内容也是受信任的、安全的内容,那么你可以不用太在意这部分的安全性内容。

但如果你需要加载第三方不受信任来源的网站内容并且还要为这些网站提供可以访问、操作文件系统,用户等能力和权限,那么可能会造成重大的安全风险。

Electron 最大的优势是同时融入了 Node.js 和 Chromium,但这也就同时意味着 Electron 要面对来自 Web 和 Node.js 两方面的安全性问题。

Web方面安全性

先来看一下 Web 方面的安全性问题

  • XSS:全称是跨站脚本攻击(Cross-Site Scripting),是一种在web应用中常见的安全漏洞。它允许攻击者将恶意脚本注入到原本是可信的网站上。用户的浏览器无法判断这些脚本是否可信,因此会执行这些脚本。这可能导致用户信息被窃取、会话被劫持、网站被篡改或是被迫执行不安全的操作。

    • 存储型XSS:恶意脚本被永久地存储在目标服务器上(如数据库、消息论坛、访客留言等),当用户访问这个存储了恶意脚本的页面时,脚本会被执行。
    • 反射型XSS:恶意脚本在URL中被发送给用户,当用户点击这个链接时,服务器将恶意脚本作为页面的一部分返回,然后在用户浏览器上执行。这种攻击通常通过钓鱼邮件等方式实现。
    • 基于DOM的XSS:这种攻击通过在客户端运行的脚本在DOM(文档对象模型)中动态添加恶意代码来实现。它不涉及向服务器发送恶意代码,而是直接在用户的浏览器中执行。
  • CSRF:全称是跨站请求伪造(Cross-Site Request Forgery),是一种常见的网络攻击方式。在CSRF攻击中,攻击者诱导受害者访问一个第三方网站,在受害者不知情的情况下,这个第三方网站利用受害者当前的登录状态发起一个跨站请求。这种请求可以是任何形式,如请求转账、修改密码、发邮件等,且对受害者来说是完全透明的。因为请求是在用户已经通过身份验证的会话中发起的,服务器无法区分该请求是用户自愿发起的还是被CSRF攻击所诱导的。

Node.js方面安全性

接下来我们来看一下 Node.js 方面所带来的安全性问题。

在 Electron v5 版本之前,Electorn 的架构是这样的:

image-20240205104852366

其中,渲染进程和主进程之间的主要沟通桥梁是 remote 模块以及 nodeIntegration,但是在渲染进程中集成了本来只能在 Node 中使用的能力的时候,就给了攻击者一定的发挥空间,渲染进程中的恶意代码可以利用 remote 模块调用主进程的任意代码,从而控制整个应用和底层操作系统,这种安全问题是非常严重的,这在前面第一章,我们介绍 preload 的时候也有提到过。

举个例子:比如我们通过 Electron 的 BrowserWindow 模块加载了一个三方网站,然后这个网站中存在着这样的一段代码:

<img onerror="require('child_process').exec('rm -rf *')" />

这种第三方网站不受信任的代码就会造成对计算机的伤害。所以如何防止这样问题的发生,那就是不要授予这些网站直接操作 node 的能力,也就意味着遵循最小权限原则,只赋予应用程序所需的最低限度权限。

所以,从 Electron v5 开始,Electron 默认关闭这些不安全的选项,并默认选择更安全的选项。渲染器和主进程之间的通信被解耦,变得更加安全:

image-20240205112412154

IPC 可用于在主进程和渲染进程之间通信,而 preload 脚本可以扩展渲染进程的功能,提供必要的操作权限,这种责任分离使我们能够应用最小权限原则。

常见措施

1. 使用preload.js和上下文隔离

在 Electron 最新版本中,默认都是关闭了渲染进程对 Node.js API 的集成,那么如果渲染进程需要使用某一些 Node.js 的 API,通过 preload.js 的方式暴露出去。

另外,新版的 Electron 还会开启一个上下文隔离,所谓上下文隔离,指的是渲染进程(网页)对应的 JS 执行环境和 Electron API 的执行环境是隔离开的,这意味着网页的 JS 是无法直接访问 Electron API 的,如果想要使用某些 API,必须通过 preload.js 暴露,然后才能使用,这也就是所谓的最小权限原则。

平时授课的时候,仅仅是为了方面,所以打开了 nodeIntegration,关闭了 contextIsolation,但是在实际开发中,同学们一定要记得通过 preload.js 去最小程度的暴露 API。

2. 开启沙盒模式

从 Electron20 版本开始,默认就会开启沙盒模式。沙盒模式的主要目录也就是起到一个主进程和渲染进程之间隔离的作用。

同样作为隔离,上下文隔离和沙盒模式的隔离会有所不同:

  • sandbox:通过创建一个限制环境来隔离渲染进程,属于进程级别的隔离,相当于进程都不一样了。
  • contextIsolation:分离网页内容的 JS 和 Electron 代码的执行上下文,属于上下文级别的隔离。也就是说,是属于同一个进程内容,但是通过隔离上下文的方式来防止不安全的交互。

两者之间有一个明显的区别,如果仅仅是上下文隔离,那么在 preload.js 中是能够访问 Node.js 的 API 的。

但是如果开启了沙盒模式,那么在 preload.js 里面都无法访问 Node.js 的 API 了。

另外注意,在沙盒模式下,preload.js 中仍然可以使用一部分以 polyfill 形式所提供的 Node.js API 的子集。

  • events
  • timers
  • url
  • ipcRenderer

例如在 preload.js 中:

const events = require("events");
const timers = require("timers");
const url = require("url");
console.log(events);
console.log(timers);
console.log(url);

我们可以使用这几个模块。

在沙盒模式下,如果 preload.js 里面想要使用某些 Node.js 的 API,这些 API 又不属于 polyfill 里面的,那么就需要再做一次封装,由主进程来提供相关的方法。

3. 开启webSecurity

开启该配置项之后,会启动浏览器的同源策略,一些跨域资源请求就会被拦截。

webSecurity一般也是默认开启的。

4. 限制网页的跳转

如果你的应用存在自动跳转的行为,那么我们最好将导航严格限制在特定范围内。

可以通过在 will-navigate 生命周期方法中阻止默认事件,然后再做一个白名单的校验:

const { URL } = require('url')
const { app } = require('electron')

app.on('web-contents-created', (event, contents) => {
  // will-navigate 是在跳转之前会触发
  contents.on('will-navigate', (event, navigationUrl) => {
    const parsedUrl = new URL(navigationUrl)
		// 这里进行一个白名单的校验,如果跳转的地址和我预期不符合,那么我们就阻止默认事件
    if (parsedUrl.origin !== 'https://example.com') {
      event.preventDefault()
    }
  })
})

最后,做一个总结,Electron 安全性的原则是基于最小化权限为前提的,也就是说,我们只为渲染进程提供最必要的权限,其他的相关操作的 API 通通不暴露出去。

另外在 Electron 官网,也总结了关于提升应用安全性的一些措施:www.electronjs.org/zh/docs/lat…

异常处理

在 Node.js 中,如果存在未捕获的异常,那么就会导致程序退出。

const invalidJSON = "{name: 'Front-End Wizard', age: 25;}";
JSON.parse(invalidJSON);
console.log("后续代码...");

在上面的代码中,JSON.parse 进行解析的时候会产生异常,抛出 SytaxError 的错误,程序也就终止了,后面的代码不会再被执行。

对于基于 Node.js 的 Electron 来讲,也同样存在这样不稳定的因素。

因此,在编写代码的过程红,对异常的处理就非常重要。

常见的异常处理分为两大类:

  • 局部异常处理
  • 全局异常处理

局部异常处理

所谓局部异常处理,指的就是开发人员在编写代码的过程中,意识到某一处可能会产生异常,于是有意识的捕获和处理异常。

const invalidJSON = "{name: 'Front-End Wizard', age: 25;}";
try {
  JSON.parse(invalidJSON);
} catch (e) {
  console.error("解析 JSON 时发生错误:", e.message);
}
console.log("后续代码...");

在上面的代码中,我们使用 try 包括 JSON.parse 这一段解析逻辑,当产生异常的时候,会进入到 catch,catch 里面会对异常进行处理,这样子的话即便产生了异常,程序也能够继续往后面执行,不会被中断。

常见需要捕获异常的地方:

  • 数据库连接
  • 网络请求
  • ...

不过,局部异常处理,会存在两个问题:

  1. 和业务逻辑有很大的相关性
  2. 非常依赖于开发人员的代码质量意识

在真实项目,往往会遇到很多本来应该去捕获异常的地方,忘记去捕获了,因此在这种时候,我们就需要第二种异常处理机制:全局异常处理。

全局异常处理

无论是 Node.js 环境还是 Chromium 环境,都提供了相应的全局事件,我们可以通过这些事件来捕获异常。

  • Node.js 环境:对应的事件注册对象为 process
    • 事件名:uncaughtException
    • 事件名:unhandledRejection
  • Chromium 环境:对应的事件注册对象为 window
    • 事件名:error
    • 事件名:unhandledRejection

因此,我们可以在我们的项目中,单独书写一个模块,进行全局的异常捕获。

let isInited = false; // 检查是否已经初始化

function initErrorHandler() {
  if (isInited) return;
  if (!isInited) {
    // 进行初始化操作
    isInited = true;
    if (process.type === "renderer") {
      // 进入此分支,说明是来自渲染进程的错误
      window.addEventListener("error", (e) => {
        e.preventDefault();
        console.log("这是来自于渲染进程的 error 类型的异常");
        console.log(e.error);
      });
      window.addEventListener("unhandledRejection", (e) => {
        e.preventDefault();
        console.log("这是来自于渲染进程的 unhandledRejection 类型的异常");
        console.log(e.reason);
      });
    } else {
      // 进入此分支,说明是来自主进程的错误
      process.on("uncaughtException", (error) => {
        console.log("这是来自于主进程的 uncaughtException 类型的异常");
        console.log(error);
      });
      process.on("unhandledRejection", (error) => {
        console.log("这是来自于主进程的 unhandledRejection 类型的异常");
        console.log(error);
      });
    }
  }
}

module.exports = initErrorHandler;

关于同步异常和异步异常

在上面,虽然我们添加了全局的异常捕获处理模块,但是这是针对异步的全局错误进行的捕获,例如:

  • 正常该捕获但是忘了捕获的异常
  • 未处理的 promise 拒绝

也就是说,这里的全局异常捕获模块,是作为 最后一道防线

一般来讲,针对同步的异常,例如上面的 JSON.parse 这一类的异常,仍然是应该通过 try ... catch 去捕获的,除非开发人员实在是忘了进行同步捕获,那么我们就在全局异常捕获模块(最后一道防线)将其捕获到。

const initErrorHandler = require("../errorHandler");
initErrorHandler();

const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
  //   throw new Error("渲染进程主动抛出错误");
  //   new promise((resolve, reject) => {
  //     reject("手动抛出一个错误");
  //   });
  const invalidJSON = "{name: 'Front-End Wizard', age: 25;}";
  try {
    JSON.parse(invalidJSON);
  } catch (e) {
    console.error("解析 JSON 时发生错误:", e.message);
  }
  console.log("后续代码...");
});

最后,我们可以对我们的全局异常处理模块再次进行封装,使其同时支持捕获同步异常和异步异常,以及全局异常处理模块:

let isInited = false; // 检查是否已经初始化
module.exports = {
  // 初始化全局异常处理模块
  initGlobalErrorHandler: function () {
    if (isInited) return;
    if (!isInited) {
      // 进行初始化操作
      isInited = true;
      if (process.type === "renderer") {
        // 进入此分支,说明是来自渲染进程的错误
        window.addEventListener("error", (e) => {
          e.preventDefault();
          console.log("这是来自于渲染进程的 error 类型的异常");
          console.log(e.error);
        });
        window.addEventListener("unhandledRejection", (e) => {
          e.preventDefault();
          console.log("这是来自于渲染进程的 unhandledRejection 类型的异常");
          console.log(e.reason);
        });
      } else {
        // 进入此分支,说明是来自主进程的错误
        process.on("uncaughtException", (error) => {
          console.log("这是来自于主进程的 uncaughtException 类型的异常");
          console.log(error);
        });
        process.on("unhandledRejection", (error) => {
          console.log("这是来自于主进程的 unhandledRejection 类型的异常");
          console.log(error);
        });
      }
    }
  },
  // 用于捕获同步代码的异常
  captureSyncErrors: function (func) {
    try {
      func();
    } catch (error) {
      console.error("捕获到同步代码的异常:", error);
      // 这里往往会添加后续的逻辑,比如将错误信息发送到服务器,记录到错误日志中等
    }
  },
  // 用于捕获异步代码的异常
  captureAsyncErrors: async function (asyncFunc) {
    try {
      await asyncFunc();
    } catch (error) {
      console.error("捕获到异步代码的异常:", error);
      // 这里往往会添加后续的逻辑,比如将错误信息发送到服务器,记录到错误日志中等
    }
  },
};

日志记录

mkdirp

mkdirp 是一个在 Node.js 环境下非常实用的小工具,其主要功能是允许你以递归的方式创建目录。

这意味着你可以一次性创建多层嵌套的目录,而不需要手动地一层一层地检查和创建。这在处理文件系统时特别有用,尤其是当你的应用需要在运行时生成一系列复杂的目录结构时。

主要特性

  • 递归创建目录:最大的特点就是能够递归创建目录,无需担心中间目录是否存在。
  • 简单易用:API 简单,易于集成和使用。
  • 兼容性:兼容各种版本的 Node.js。

使用方法

  1. 安装

通过 npm 或 yarn 可以很容易地安装mkdirp:

npm install mkdirp
# 或者
yarn add mkdirp
  1. 示例代码

使用 mkdirp 创建一个嵌套目录结构的示例代码如下:

const mkdirp = require('mkdirp');

// 创建多层嵌套目录
mkdirp('/tmp/foo/bar/baz')
  .then(made => console.log(`目录已创建于 ${made}`))
  .catch(error => console.error(`创建目录时出错: ${error}`));

在这个示例中,/tmp/foo/bar/baz目录(以及所有中间目录)将被创建。如果目录已经存在,则不会执行任何操作。

在 Node.js 版本 10.12.0 及以上,fs 模块已经原生支持了 mkdir 的 { recursive: true } 选项,使得 mkdirp 的功能可以通过原生 fs 直接实现。不过,对于旧版本的 Node.js 或者需要mkdirp提供的特殊功能,mkdirp 仍然是一个非常有用的库。

log4js

log4js是一个在Node.js环境中使用的流行日志管理工具,灵感来源于Apache的log4j库。它提供了一个功能强大、灵活的日志记录解决方案,让开发者能够控制日志信息的输出位置和输出级别。这对于开发大型应用和系统时进行问题追踪和性能监控尤其重要。

主要特性

  • 多种日志记录级别:支持标准的日志记录级别,如TRACE、DEBUG、INFO、WARN、ERROR、FATAL,使得开发者可以根据需要输出不同重要程度的信息。
  • 灵活的日志输出:可以配置将日志输出到不同的地方,比如控制台、文件、远程服务器等。
  • 日志分割:支持按文件大小或日期自动分割日志文件,便于管理和维护。
  • 自定义布局:允许自定义日志信息的格式,使得日志信息更加符合项目需求。
  • 性能:设计上考虑到了性能,尽量减少日志记录对应用性能的影响。

配置和使用

  1. 安装

首先,在Node.js项目中安装log4js。通过npm或yarn即可安装:

npm install log4js --save
# 或者
yarn add log4js
  1. 简单配置

安装完毕后,你就可以在项目中引入并使用 log4js 了。下面是一个基本的配置示例:

const log4js = require('log4js');

log4js.configure({
  appenders: {
    out: { type: 'stdout' },
    app: { type: 'file', filename: 'application.log' }
  },
  categories: {
    default: { appenders: ['out', 'app'], level: 'debug' }
  }
});

const logger = log4js.getLogger();
logger.debug("Some debug messages");

在这个例子中,我们配置了两个 appender:

  • 一个是输出到控制台(stdout)
  • 另一个是输出到名为application.log的文件。

我们还设置了默认的日志级别为 debug,这意味着所有 debug 及以上级别的日志都会被记录。

  1. 进阶配置

log4js 的配置非常灵活,支持更多高级特性,比如日志分割、自定义日志格式等。

这里是一个配置日志分割的例子:

log4js.configure({
  appenders: {
    everything: { type: 'dateFile', filename: 'all-the-logs.log', pattern: '.yyyy-MM-dd', compress: true }
  },
  categories: {
    default: { appenders: ['everything'], level: 'debug' }
  }
});

这个配置会将日志输出到一个文件中,并且每天自动创建一个新的文件,旧的日志文件会被压缩保存。

Sentry

Sentry 是一个开放源代码的错误追踪工具,它可以帮助开发者实时监控和修复应用程序中的错误。Sentry 支持多种编程语言和框架,包括 JavaScript、Python、PHP、Ruby、Java、.NET、iOS 和 Android 等,非常适用于跨平台应用程序的错误监控。通过在应用程序中集成 Sentry,开发者可以获得详细的错误报告,包括错误发生的时间、环境、导致错误的代码行以及影响的用户等信息。

Sentry 对应的官网:sentry.io/welcome/

Sentry 的主要特点

  • 实时错误追踪:Sentry 提供实时错误追踪功能,当应用程序出现错误时,能够立即通知开发者,帮助快速定位问题。
  • 跨平台支持:支持广泛的编程语言和平台,使得几乎所有类型的应用程序都能利用 Sentry 进行错误追踪。
  • 详细的错误报告:错误报告包括堆栈跟踪、影响的用户、错误发生的环境和上下文信息,使得开发者可以更容易地理解和解决问题。
  • 问题分组和聚合:Sentry 自动将相似的错误报告分组,便于管理和分析大量的错误。
  • 性能监控:除了错误追踪,Sentry 还提供性能监控功能,帮助开发者了解应用程序的性能瓶颈。
  • 集成和通知:可以与常用的开发工具和通讯平台(如 Slack、GitHub、Jira 等)集成,使得错误处理流程更加高效。

如何使用 Sentry

使用 Sentry 通常包括以下几个步骤:

  1. 注册并创建项目:在 Sentry 网站注册账户,并为你的应用程序创建一个新项目。
  2. 集成 SDK:根据你的应用程序所使用的编程语言或平台,选择合适的 Sentry SDK 并集成到你的应用程序中。
  3. 配置和定制:根据需要配置 Sentry SDK,例如设置错误捕获的级别、自定义错误报告信息等。
  4. 监控和解决错误:部署应用程序后,通过 Sentry 的控制台监控错误,使用提供的详细信息快速定位和解决问题。

注册了 Sentry 之后,官方会为我们提供一组 SDK

接下来回到我们的项目,我们需要在我们的项目中安装 Sentry 相关的依赖:

npm install --save @sentry/electron

安装完成后,在我们自己的项目里面配置相关的 SDK 即可。

const { init } = require("@sentry/electron");
init({
  dsn: "你自己的SDK",
});

本文所有源码均在:github.com/Sunny-117/e…

「❤️ 感谢大家」

如果你觉得这篇内容对你挺有有帮助的话: 点赞支持下吧,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。觉得不错的话,也可以阅读 Sunny 近期梳理的文章(感谢掘友的鼓励与支持 🌹🌹🌹):

我的博客:

Github:https://github.com/sunny-117/

前端八股文题库:sunny-117.github.io/blog/

前端面试手写题库:github.com/Sunny-117/j…

手写前端库源码教程:sunny-117.github.io/mini-anythi…

热门文章

专栏