electron 入门☞🀁

electron 入门指南

Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 ChromiumNode.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在能在各PC端平台上运行的跨平台应用 macOS和Linux(不需要本地开发原生的经验)。腾讯在22年都把 QQ 用 electron 重构了,实现了 Linux、MacOS、Linux 的三端统一,此外例如 vscode、postman、Skype、WhatsApp 这些大家耳熟能详的应用都是用 electron 构建的。下文会从背景到electron的流程再到代码示例展示,如果背景只是都十分了解了,可以直接跳转到项目搭建的例子中。

跨端技术

在软件开发中,通常不同的平台开发与语言都是不一样的,出包的方式也不一样,如PC端(Windows, Mac, Linux),移动端(安卓, iOS,鸿蒙),web端,IoT设备等。因此就会出现两种开发思路(各端独立开发和跨端开发),接下来就分析一下各种法案的优缺点。

各端独立进行原生开发

优点:

  • 开发人员能更深入的对所在系统进行控制
  • 性能更优,速度更快
  • 生态完整,社区活跃度高
  • 对同端的不用版本的兼容性好

缺点

  • 开发和维护成本极高,一个端一批人,代码完全不能复用
  • 有新功能只能发版,迭代周期长

跨端开发

优点

  • 开发效率高
  • 维护成本低
  • 多端一致性高
  • 学习成本低,无需关注底层复杂的api,跨端框架都会帮你封装好

缺点

  • 生态和社区都没有原生的活跃度高,因为跨端技术仍不是主流的方案
  • 能力受限,因为框架要把底层功能封装成一个接口,一些新功能新特性,跨端框架响应没那么及时,封装出来的接口可能也不会像原生那么全
  • 无法保证原生体验,渲染效果会差一些,性能一般般

本文将主要着重在宏观意义的桌面端进行拓展(使用electron框架),因为大部分IoT设备都是基于Linux,都可以用electron打包出来。

electron

注:electron无论是渲染进程还是主进程都是使用 nodejs 进行开发的,前端开发可放心食用。

官方文档传送门:🚪

多进程模型

Electron 是集成了 Chromium 的多进程架构,为什么不是单一进程呢?网页浏览器是个极其复杂的应用程序。 除了显示网页内容的主要能力之外,他们还有许多次要的职责,例如:管理众多窗口 ( 或 标签页 ) 和加载第三方扩展。

在早期,浏览器通常使用单个进程来处理所有这些功能。 虽然这种模式意味着您打开每个标签页的开销较少,但也同时意味着一个网站的崩溃或无响应会影响到整个浏览器。

Electron主要分为主进程和渲染进行,与Chrome浏览器是类似的,此外electron还有一个效率进程,可以类比浏览器的WebWorker,主要用于用于托管,例如:不受信任的服务, CPU 密集型任务或以前容易崩溃的组件

主进程

每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。

主进程的主要目的是使用 BrowserWindow 模块创建和管理应用程序窗口。

BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 您可从主进程用 window 的 webContent 对象与网页内容进行交互。

js 复制代码
const { BrowserWindow } = require('electron')

const win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('https://github.com')

const contents = win.webContents
console.log(contents)

注意:渲染器进程也是为 web embeds 而被创建的,例如 BrowserView 模块。 嵌入式网页内容也可访问 webContents 对象。

由于 BrowserWindow 模块是一个 EventEmitter, 所以您也可以为各种用户事件 ( 例如,最小化 或 最大化您的窗口 ) 添加处理程序。

当一个 BrowserWindow 实例被销毁时,与其相应的渲染器进程也会被终止。

应用程序生命周期

主进程还能通过 Electron 的 app 模块来控制您应用程序的生命周期。别的生命周期可以去官网了解一下,太多了,这里就不赘述了,开发中遇到就去翻文档就完事了

js 复制代码
// quitting the app when no windows are open on non-macOS platforms
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit()
})

渲染进程

每个 Electron 应用都会为每个打开的 BrowserWindow ( 与每个网页嵌入 ) 生成一个单独的渲染器进程。 洽如其名,渲染器负责 渲染 网页内容。 所以实际上,运行于渲染器进程中的代码是须遵照网页标准的 (至少就目前使用的 Chromium 而言是如此) 。

因此,一个浏览器窗口中的所有的用户界面和应用功能,都应与您在网页开发上使用相同的工具和规范来进行攥写。

此外,这也意味着渲染器无权直接访问 require 或其他 Node.js API。 为了在渲染器中直接包含 NPM 模块,您必须使用与在 web 开发时相同的打包工具 (例如 webpackparcel)

效率进程

每个Electron应用程序都可以使用主进程生成多个子进程UtilityProcess API。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。 效率进程可用于托管,例如:不受信任的服务, CPU 密集型任务或以前容易崩溃的组件 托管在主进程或使用Node.jschild_process.fork API 生成的进程中。 效率进程和 Node 生成的进程之间的主要区别.js child_process模块是实用程序进程可以建立通信 通道与使用MessagePort的渲染器进程。 当需要从主进程派生一个子进程时,Electron 应用程序可以总是优先使用 效率进程 API 而不是Node.js child_process.fork API。

进程间通信

进程间通信 (IPC) 是在 Electron 中构建功能丰富的桌面应用程序的关键部分之一。 由于主进程和渲染器进程在 Electron 的进程模型具有不同的职责,因此 IPC 是执行许多常见任务的唯一方法,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容的更改。

IPC通道

在 Electron 中,进程使用 ipcMainipcRenderer 模块,通过开发人员定义的"通道"传递消息来进行通信。 这些通道是 任意 (您可以随意命名它们)和 双向 (您可以在两个模块中使用相同的通道名称)的。

在本指南中,我们将介绍一些基本的 IPC 模式,并提供具体的示例。您可以将这些示例作为您应用程序代码的参考。

了解上下文隔离进程

渲染器进程到主进程(单向)

要将单向 IPC 消息从渲染器进程发送到主进程,您可以使用 ipcRenderer.send API 发送消息 ,然后使用 ipcMain.on API 接收。

js 复制代码
// 渲染进程中
ipcRenderer.send('xxx' , params)
// 主进程中
ipcMain.on('xxx', (event, parmas) => { /*...*/ })

渲染器进程到主进程(双向)

双向 IPC 的一个常见应用是从渲染器进程代码调用主进程模块并等待结果 。 这可以通过将 ipcRenderer.invokeipcMain.handle 搭配使用来完成。

js 复制代码
// 渲染进程中
const res = await ipcRenderer.invoke('xxx', params)
// 主进程中
ipcMain.handle('xxx', (event, params) => { /*...*/ return 'abc' })

主进程到渲染器进程

将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息。 消息需要通过其 WebContents 实例发送到渲染器进程。 此 WebContents 实例包含一个 send 方法,其使用方式与 ipcRenderer.send 相同。

js 复制代码
// 渲染进程中
ipcRenderer.on('xxx', (event, parmas) => { /*...*/ })
// 主进程中
ipcMain.send('xxx' , params)
// 注意,ipcRenderer没有handle所以不能说让主进程等待执行,需要使用Electron的消息端口

渲染器进程到渲染器进程

没有直接的方法可以使用 ipcMainipcRenderer 模块在 Electron 中的渲染器进程之间发送消息。 为此,您有两种选择:

  • 将主进程作为渲染器之间的消息代理。 这需要将消息从一个渲染器发送到主进程,然后主进程将消息转发到另一个渲染器。
  • 从主进程将一个 MessagePort 传递到两个渲染器。 这将允许在初始设置后渲染器之间直接进行通信。www.electronjs.org/zh/docs/lat...

Electron中的消息端口

MessagePort是一个允许在不同上下文之间传递消息的Web功能。 就像 window.postMessage, 但是在不同的通道上。

主进程中的 MessagePorts

在渲染器中, MessagePort 类的行为与它在 web 上的行为完全一样。 但是,主进程不是网页(它没有 Blink 集成),因此它没有 MessagePortMessageChannel 类。 为了在主进程中处理 MessagePorts 并与之交互,Electron 添加了两个新类: MessagePortMainMessageChannelMain。 这些行为 类似于渲染器中 analogous 类。

MessagePort 对象可以在渲染器或主 进程中创建,并使用 ipcRenderer.postMessageWebContents.postMessage 方法互相传递。 请注意,通常的 IPC 方法,例如 sendinvoke 不能用来传输 MessagePort, 只有 postMessage 方法可以传输 MessagePort

通过主进程传递 MessagePort,就可以连接两个可能无法通信的页面 (例如,由于同源限制) 。以下是最简单的例子

js 复制代码
// 渲染进程
// 消息端口是成对创建的。 连接的一对消息端口
// 被称为通道。
const channel = new MessageChannel()

// port1 和 port2 之间唯一的不同是你如何使用它们。 消息
// 发送到port1 将被port2 接收,反之亦然。
const port1 = channel.port1
const port2 = channel.port2

// 允许在另一端还没有注册监听器的情况下就通过通道向其发送消息
// 消息将排队等待,直到一个监听器注册为止。
port2.postMessage({ answer: 42 })

// 这次我们通过 ipc 向主进程发送 port1 对象。 类似的,
// 我们也可以发送 MessagePorts 到其他 frames, 或发送到 Web Workers, 等.
ipcRenderer.postMessage('port', null, [port1])

// -------------------------------

// 主进程
// 在主进程中,我们接收端口对象。
ipcMain.on('port', (event) => {
  // 当我们在主进程中接收到 MessagePort 对象, 它就成为了
  // MessagePortMain.
  const port = event.ports[0]

  port.on('message', (event) => {
    // 收到的数据是: { answer: 42 }
    const data = event.data
  })

  // MessagePortMain 阻塞消息直到 .start() 方法被调用
  port.start()
})

再来一个例子,这个例子是上文提到渲染进程和渲染进程通信的例子

js 复制代码
// 主进程
const { BrowserWindow, app, ipcMain, MessageChannelMain } = require('electron')

app.whenReady().then(async () => {
  // Worker 进程是一个隐藏的 BrowserWindow
  // 它具有访问完整的Blink上下文(包括例如 canvas、音频、fetch()等)的权限
  const worker = new BrowserWindow({
    show: false,
    webPreferences: { nodeIntegration: true }
  })
  await worker.loadFile('worker.html')

  // main window 将发送内容给 worker process 同时通过 MessagePort 接收返回值
  const mainWindow = new BrowserWindow({
    webPreferences: { nodeIntegration: true }
  })
  mainWindow.loadFile('app.html')

  // 在这里我们不能使用 ipcMain.handle() , 因为回复需要传输
  // MessagePort.
  // 监听从顶级 frame 发来的消息
  mainWindow.webContents.mainFrame.ipc.on('request-worker-channel', (event) => {
    // 建立新通道  ...
    const { port1, port2 } = new MessageChannelMain()
    // ... 将其中一个端口发送给 Worker ...
    worker.webContents.postMessage('new-client', null, [port1])
    // ... 将另一个端口发送给主窗口
    event.senderFrame.postMessage('provide-worker-channel', null, [port2])
    // 现在主窗口和工作进程可以直接相互通信,无需经过主进程!
  })
})

// -------------

// worker进程
const { ipcRenderer } = require('electron')

const doWork = (input) => {
  // 一些对CPU要求较高的任务
  return input * 2
}

// 我们可能会得到多个 clients, 比如有多个 windows,
// 或者假如 main window 重新加载了.
ipcRenderer.on('new-client', (event) => {
  const [ port ] = event.ports
  port.onmessage = (event) => {
    // 事件数据可以是任何可序列化的对象 (事件甚至可以
    // 携带其他 MessagePorts 对象!)
    const result = doWork(event.data)
    port.postMessage(result)
  }
})

// ------------
// 其他渲染进程
const { ipcRenderer } = require('electron')

// 我们请求主进程向我们发送一个通道
// 以便我们可以用它与 Worker 进程建立通信
ipcRenderer.send('request-worker-channel')

ipcRenderer.once('provide-worker-channel', (event) => {
  // 一旦收到回复, 我们可以这样做...
  const [ port ] = event.ports
  // ... 注册一个接收结果处理器 ...
  port.onmessage = (event) => {
    console.log('received result:', event.data)
  }
  // ... 并开始发送消息给 work!
  port.postMessage(21)
})

回复流

Electron的内置IPC方法只支持两种模式:即发即弃(例如, send),或请求-响应(例如, invoke)。 使用MessageChannels,你可以实现一个"响应流",其中单个请求可以返回一串数据。

js 复制代码
// 渲染进程
const makeStreamingRequest = (element, callback) => {
  // MessageChannels 是轻量的
  // 为每个请求创建一个新的 MessageChannel 带来的开销并不大
  const { port1, port2 } = new MessageChannel()

  // 我们将端口的一端发送给主进程 ...
  ipcRenderer.postMessage(
    'give-me-a-stream',
    { element, count: 10 },
    [port2]
  )

  // ... 保留另一端。 主进程将向其端口发送消息
  // 并在完成后关闭它
  port1.onmessage = (event) => {
    callback(event.data)
  }
  port1.onclose = () => {
    console.log('stream ended')
  }
}

makeStreamingRequest(42, (data) => {
  console.log('got response data:', data)
})
// 我们会看到 "got response data: 42" 出现了10次

// ---------

// 主进程
ipcMain.on('give-me-a-stream', (event, msg) => {
  // 渲染进程向我们发送了一个 MessagePort
  // 并期望得到响应
  const [replyPort] = event.ports

  // 在这里,我们同步发送消息
  // 我们也可以将端口存储在某个地方,异步发送消息
  for (let i = 0; i < msg.count; i++) {
    replyPort.postMessage(msg.element)
  }

  // 当我们处理完成后,关闭端口以通知另一端
  // 我们不会再发送任何消息 这并不是严格要求的
  // 如果我们没有显式地关闭端口,它最终会被垃圾回收
  // 这也会触发渲染进程中的'close'事件
  replyPort.close()
})

此外还有上下文隔离、进程沙盒化这些基本概念,篇幅有限,这里就不赘述了,感兴趣的可以去官网看看,了解完上述的几个electron流程后,就可以动手尝试做一个跨端应用了。

从 0 搭建 electron 项目例子

Talk is Cheap, show me the code!

第一步:安装electron

找一个前端项目,没有的话就建一个(下文是以create-react-app建的一个新项目),首先安装electron

shell 复制代码
npm install electron --save-dev

如果人在国内,🪜又不给力的话,通常安装的会很慢,此处不是npm安装的慢,是npm在下完electron后,electron会调用自己的安装脚本,脚本下载的慢,因此在设置npm源外,如果太慢了顶不住了的话,还需要设置一下electron源

shell 复制代码
npm config set ELECTRON_MIRROR http://npm.taobao.org/mirrors/electron/

这样就ok了,electron在安装的时候会去找ELECTRON_MIRROR这个变量,有就会修改下载源的,亲测有效

第二步:编写main.js文件

编写入口文件,首先在package.json文件中加个字段"main": "main.js" 让 electron 知道改去读哪个入口文件,再建一个main.js文件就可以了,以下是一个简单的例子

js 复制代码
const { BrowserWindow, ipcMain } = require('electron')
const { app, dialog } = require('electron')
const path = require('path')
// const remote = require('@electron/remote/main') 这个是干嘛的,后续文章会说明
// const ElectronStore = require('electron-store') 后续会说明这个东东

const isDevelopment = process.env.NODE_ENV === 'development'
let mainWindow = null

function createMainWindow() {
    mainWindow = new BrowserWindow({
        width: 1160,
        height: 752,
        minHeight: 632,
        minWidth: 960,
        show: false,
        // frame: false, 是否使用无边框模式,(应用程序关闭最小化那一条东西)
        title: 'Markdown Editor',
        webPreferences: {
            nodeIntegration: true, // 如果想在渲染进制中使用nodejs相关的功能需要设置一下
            contextIsolation: false // 是否隔离主进程和渲染进程,如果隔离的话,相关的通信方式都需要在preload的前置脚本中把需要与主进程通信的函数、挂载在window中给渲染进程使用,具体可看下官网的例子
          // preload: path.resolve(__dirname, '.preload.js') 可以写一些前置脚本
        },
        icon: path.join(__dirname, './assets/appdmg.png')
    })

    if (isDevelopment) {
        // 为什么electron可以直接装直接用,因为它的窗口可以通过url进行渲染
        mainWindow.loadURL('http://localhost:3000/')
    } else {
        const entryPath = path.join(__dirname, './build/index.html')
        mainWindow.loadFile(entryPath)
    }

    mainWindow.once('ready-to-show', () => {
        mainWindow.show()
    })
    // remote.initialize()
    // remote.enable(mainWindow.webContents)
    if (isDevelopment) mainWindow.webContents.openDevTools()
}

app.on('ready', () => {
    createMainWindow()
    // ElectronStore.initRenderer()
    // ipcMain.handle('choose-file', async (event, args) => {
    //     const res = await dialog.showOpenDialog(args)
    //     return res
    // })
})

第三步:配置启动命令

在 package.json 文件配置的script中配置启动命令

json 复制代码
{
  "scripts": {
    "dev": "concurrently \"wait-on http://localhost:3000 && cross-env NODE_ENV=development electron .\" \"npm start\"",
  }
}

大家可以仔细看看这条启动命令,其中还用到了concurrently和wait-on这两个工具,因此还要装一下

shell 复制代码
npm insatll --save-dev concurrently wait-on

这两个命令是干嘛的呢?我们在看第二部的时候可以发现,一个 BrowserWindow 需要 LoadURL 或者 LoadFile,因此在启动 electron 之前得需要先启一个前端页面服务或者把前端项目打包好。我们启动命令只需要启项目开发服务,这样开发才有热更,因此我们想开发一个 electron 应用需要启动 frontend 服务以及 electron 进程。同时启两个服务就要用到 concurrently 这个工具了。

我们再来考虑服务启动先后的问题,要先启动 electron,得先把 frontend 服务启动好才行,不然开发中一开始打开electron,里面什么都没有,这不是我们想要的效果,应该electron打开的时候页面已经加载好了才行,因此就需要用到 wait-on 这个工具,这个工具就是用来等待某个套接字这里可以理解为等服务启动好了,才执行后面的命令。

最后:出包

走完前三步,我们其实已经可以进行愉快的跨端开发了,在开发完之后我们就可以出包了。

这里我们可以用 electron-builder 这个工具帮我们打包 electron 应用(传送门),首先我们可以先安装它。

shell 复制代码
npm install --save-dev electron-builder

配置相关的打包配置,我们直接配置在package.json里就可以了,除了打包配置写在build里外pakage.json还要写好name、author、version,不然安装不了

json 复制代码
{
  "name": "切图仔的妈宕编辑器", // 如果用window安装的话,安装出来的目录名就叫这个
  "author": "切图仔",
  "version": "9.9.9",
  "description": "看心情写描述信息,不是必须项",
  "build": {
    "appId": "madang-editor",
    "productName": "妈宕编辑器",
    "copyright": "Copyright © 2024 切图仔",
    "extends": null,
    "files": [ // 要打包进去应用里的文件
      "build/**/*",
      "node_modules/**/*",
      "package.json",
      "main.js"
    ],
    "directories": {
      "buildResources": "assets" // 静态资源的目录,应用的icon什么的
    },
    "mac": {
      "category": "public.app-category.productivity",
      "artifactName": "${productName}-${version}-${arch}.${ext}"// 安装包的名字
    },
    "dmg": {// macOS安装包
      "background": "assets/appdmg.png",
      "icon": "assets/icon.icns",
      "iconSize": 100,
      "contents": [
        {
          "x": 380,
          "y": 280,
          "type": "link",
          "path": "/Applications"
        },
        {
          "x": 110,
          "y": 280,
          "type": "file"
        }
      ],
      "window": {
        "width": 500,
        "height": 500
      }
    },
    "win": {// windows 安装包
      "target": [
        "msi", // 我在写这篇文章的时候在高版本的macOS出msi包会有些问题,如果报错了去掉msi安装包只出nsis也可以,window 也能安装
        "nsis"
      ],
      "icon": "assets/icon.ico", // 高版本的ico有大小限制,最小尺寸是256×256,某些垃圾线上转ico的工具装出来有问题,可以试试这个 https://redketchup.io/icon-converter
      "artifactName": "${productName}-Setup-${version}.${ext}",// 安装包的名字
      "publisherName": "Jamsdfn"
    },
    "nsis": {
      "allowToChangeInstallationDirectory": true,
      "oneClick": false,
      "perMachine": false
    }
  }
}

配置好上面的配置之后就可以配置一下打包命令了,也是package.json,不过懒得配的话也可以直接跑命令,不用npm启

json 复制代码
{
	"script": {
    "pack": "node scripts/build.js && electron-builder --dir",
    "pack-mac": "node scripts/build.js && electron-builder --dir --mac",
    "pack-win": "node scripts/build.js && electron-builder --dir --win",
    "dist": "node scripts/build.js && electron-builder",
    "dist-mac": "node scripts/build.js && electron-builder --mac",
    "dist-win": "node scripts/build.js && electron-builder --win"
	}
}

附上我自己写的一个小项目地址,可以clone到本地玩一玩,一个markdown编辑器:github.com/Jamsdfn/mar...

挖坑

最近另一个很火的跨端框架tauri:tauri.app/zh-cn/ 写这篇东西的时候已经74.9k个star了,electron 111k个,快赶上了,2.0版本快出了,号称可以跨所有端,但是主进程是基于 Rust 的,还没学,等学完 Rust 再整整

相关推荐
腾讯TNTWeb前端团队3 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰6 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪7 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪7 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy7 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom8 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom8 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom8 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom8 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom8 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试