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 再整整

相关推荐
雾散声声慢3 分钟前
前端开发中怎么把链接转为二维码并展示?
前端
熊的猫4 分钟前
DOM 规范 — MutationObserver 接口
前端·javascript·chrome·webpack·前端框架·node.js·ecmascript
天农学子5 分钟前
Easyui ComboBox 数据加载完成之后过滤数据
前端·javascript·easyui
mez_Blog5 分钟前
Vue之插槽(slot)
前端·javascript·vue.js·前端框架·插槽
爱睡D小猪8 分钟前
vue文本高亮处理
前端·javascript·vue.js
开心工作室_kaic11 分钟前
ssm102“魅力”繁峙宣传网站的设计与实现+vue(论文+源码)_kaic
前端·javascript·vue.js
放逐者-保持本心,方可放逐11 分钟前
vue3 中那些常用 靠copy 的内置函数
前端·javascript·vue.js·前端框架
IT古董12 分钟前
【前端】vue 如何完全销毁一个组件
前端·javascript·vue.js
Henry_Wu00114 分钟前
从swagger直接转 vue的api
前端·javascript·vue.js
SameX24 分钟前
初识 HarmonyOS Next 的分布式管理:设备发现与认证
前端·harmonyos