-
增量更新与全量更新的区别增量更新和全量更新在更新方式和更新范围上有明显区别。更新方式:全量更新会下载完整的新版本软件包,并覆盖安装到用户电脑上,由于是完整的安装包,里面包含了 Electron 框架,因此包体积会比较大。增量更新则是只下载有变动的部分,一般来说是 app.asar 文件,不需要把 Electron 框架都带进去,所以下载速度快,还能节约流量。更新范围:当应用升级 Electron 版本时,比如从 9.x.y 升级到 13.x.y,就需要用全量更新。而如果 Electron 版本未变,用增量更新的方式会更好。总的来说,全量更新和增量更新的区别主要体现在更新方式和更新范围上。Electron打包后的文件结构Electron 打包出来的应用主要由两部分组成:Chromium 和 Node.js。其中,Chromium 是 Electron 应用的前端运行环境,提供了 HTML、CSS 和 JavaScript 的运行环境;Node.js 是 Electron 应用的底层运行环境,提供了诸如文件操作、网络请求等原生 API 的接口。在 Electron 应用中,除了 asar 文件外,其余的文件和程序都是基于 Chromium 和 Node.js 的运行环境和功能实现的。因此,可以说 Electron 打包出来的应用除了 asar 文件外,其余的程序都是基于 Chrome 浏览器内核程序实现的。需要注意的是,虽然 Electron 应用的前端运行环境是 Chromium,但并不意味着它们只能使用 Chromium 的功能。相反,Electron 应用可以使用任何在 Node.js 环境中可用的库和模块,这就使得 Electron 应用的功能非常丰富和灵活。Electron 如何实现增量更新Electron 增量更新是一种更新应用程序的方法,它通过仅传输应用程序中已更改的部分来更新应用程序,而不是像传统全量更新那样需要下载和安装整个应用程序。这种方法的原理是基于对应用程序的版本控制,通过比较新旧版本之间的差异,只将变化的部分进行打包并更新,从而减少更新所需的资源和时间。在 Electron 中,开发者可以使用一些开源库来实现增量更新功能,例如 electron-builder 和 electron-updater。这些库可以帮助开发者在应用程序中集成增量更新功能,自动检测更新、下载并安装最新的应用程序版本,提高应用程序的更新效率。总的来说,Electron 增量更新的原理是基于版本控制和差异比较,通过只更新变化的部分来实现更高效、更快速的更新。
-
什么是asar文件asar是一种将多个文件合并成一个文件的类tar风格的归档格式。asar文件用于存储文件和文件夹,并且Electron可以无需解压整个文件,即可从其中读取任意文件内容。asar文件如果不加密会有什么后果如果不加密Electron中的asar文件,可能会面临以下风险:容易被篡改:未加密的asar文件容易被恶意用户或攻击者篡改,他们可以轻松地修改其中的文件内容,甚至添加恶意代码。数据泄露:asar文件中可能包含应用程序的敏感数据,例如配置文件、用户凭证等。如果未进行加密保护,这些数据可能会被未经授权的访问者获取,导致数据泄露。知识产权风险:未加密的asar文件可能被未经授权的第三方复制、分发或修改,从而侵犯原始开发者的知识产权。因此,为了保护应用程序的安全性、数据完整性和知识产权,建议对Electron中的asar文件进行加密处理。如何加密asar文件Electron 自身并不提供直接加密asar文件的功能,因为 asar 文件只是将多个文件打包成一个文件,其内容并没有被加密。因此,如果你想要实现 asar 文件的加密,你需要使用其他工具或库来实现。一种可能的方法是使用 Node.js 的文件系统模块(fs)和crypto模块来加密和解密asar文件。下面是一个简单的示例代码,用于加密和解密asar文件:const fs = require('fs'); const crypto = require('crypto'); // 加密函数 function encrypt(inputFile, outputFile, password) { const algorithm = 'aes-256-cbc'; const cipher = crypto.createCipher(algorithm, password); const input = fs.createReadStream(inputFile); const output = fs.createWriteStream(outputFile); input.pipe(cipher).pipe(output); output.on('finish', () => { console.log('Encryption completed.'); }); } // 解密函数 function decrypt(inputFile, outputFile, password) { const algorithm = 'aes-256-cbc'; const decipher = crypto.createDecipher(algorithm, password); const input = fs.createReadStream(inputFile); const output = fs.createWriteStream(outputFile); input.pipe(decipher).pipe(output); output.on('finish', () => { console.log('Decryption completed.'); }); } // 使用示例 encrypt('path/to/input.asar', 'path/to/output.asar', 'your_password'); decrypt('path/to/input.asar', 'path/to/output.asar', 'your_password');在这个示例中,我们使用了 Node.js 的 fs 和 crypto 模块来读取和写入文件,并使用 aes-256-cbc 加密算法来加密和解密 asar 文件。你可以通过调用 encrypt 和 decrypt 函数来加密和解密 asar 文件。在调用这些函数时,你需要提供输入文件的路径、输出文件的路径和加密密码。
-
报告安全问题有关如何正确上报 Electron 漏洞的信息,参阅 SECURITY.md.对于上游 Chromium 漏洞: Electron 用其他版本的 Chromium 来更新。 欲了解更多信息,请参阅 Electron发布时间表 文档。前言作为网络开发人员,我们通常喜欢浏览器的强大安全网,因为这样我们编写的代码风险较小。 我们的网站在沙盒中被赋予了有限的权力,我们相信我们的用户享受到的是一个由大型工程师团队打造的浏览器,它能够快速应对新发现的安全威胁。当使用 Electron 时,很重要的一点是要理解 Electron 不是一个 Web 浏览器。 它允许您使用熟悉的 Web 技术构建功能丰富的桌面应用程序,但是您的代码具有更强大的功能。 JavaScript 可以访问文件系统,用户 shell 等。 这允许您构建更高质量的本机应用程序,但是内在的安全风险会随着授予您的代码的额外权力而增加。考虑到这一点,请注意,展示任意来自不受信任源的内容都将会带来严重的安全风险,而这种风险Electron也没打算处理。 事实上,最流行的 Electron 应用程序(Atom,Slack,Visual Studio Code 等) 主要显示本地内容(即使有远程内容也是无 Node 的、受信任的、安全的内容) - 如果您的应用程序要运行在线的源代码,那么您需要确保源代码不是恶意的。一般准则安全是所有人的共同责任需要牢记的是,你的 Electron 程序安全性除了依赖于整个框架基础(Chromium、Node.js)、Electron 本身和所有相关 NPM 库的安全性,还依赖于你自己的代码安全性。 因此,你有责任遵循下列安全守则:使用最新版的 Electron 框架搭建你的程序。你最终发行的产品中会包含 Electron、Chromium 共享库和 Node.js 的组件。 这些组件存在的安全问题也可能影响你的程序安全性。 你可以通过更新Electron到最新版本来确保像是nodeIntegration绕过攻击一类的严重漏洞已经被修复因而不会影响到你的程序。 请参阅“使用当前版本的Electron”以获取更多信息。评估你的依赖项目NPM提供了五百万可重用的软件包,而你应当承担起选择可信任的第三方库。 如果你使用了受已知漏洞的过时的库,或是依赖于维护的很糟糕的代码,你的程序安全就可能面临威胁。遵循安全编码实践你的代码是你的程序安全的第一道防线。 一般的网络漏洞,例如跨站脚本攻击(Cross-Site Scripting, XSS),对Electron将造成更大的影响,因此非常建议你遵循安全软件开发最佳实践并进行安全性测试。隔离不受信任的内容每当你从不被信任的来源(如一个远程服务器)获取代码并在本地执行,其中就存在安全性问题。 例如在默认的 BrowserWindow中显示一个远程网站. 如果攻击者以某种方式设法改变所述内容 (通过直接攻击源或者通过在应用和实际目的地之间进行攻击) ,他们将能够在用户的机器上执行本地代码。:::警告无论如何,在启用Node.js集成的情况下,你都不该加载并执行远程代码。 相反,只使用本地文件(和您的应用打包在一起)来执行Node.js代码 如果你想要显示远程内容,请使用 <webview> Tag或者 BrowserView,并确保禁用 nodeIntegration 并启用 contextIsolation:::Electron 安全警告安全警告和建议被打印到开发者控制台。 只有当二进制文件的名称为Electron时,它们才会显示,这表明开发人员 当前正在查看控制台。你可以通过在process.env 或 window对象上配置ELECTRON_ENABLE_SECURITY_WARNINGS 或ELECTRON_DISABLE_SECURITY_WARNINGS来强制开启或关闭这些警告。清单:安全建议为加强程序安全性,你至少应当遵循下列规则:只加载安全的内容禁止在所有渲染器中使用Node.js集成显示远程内容在所有渲染器中启用上下文隔离启用进程沙盒化在所有加载远程内容的会话中使用 ses.setPermissionRequestHandler().不要禁用 webSecurity定义一个Content-Security-Policy并设置限制规则(如:script-src 'self')不要设置 allowRunningInsecureContent 为 true不要开启实验性功能不要使用enableBlinkFeatures<webview>:不要使用 allowpopups <webview>:验证选项与参数禁用或限制网页跳转禁用或限制新窗口创建不要对不可信的内容使用 shell.openExternal使用当前版本的 Electron验证所有 IPC 消息的 sender如果你想要自动检测错误的配置或是不安全的模式,可以使用electronegativity 关于在使用Electron进行应用程序开发中的潜在薄弱点或者bug,您可以参考开发者与审核人员指南.1. 只加载安全的内容任何不属于你的应用的资源都应该使用像HTTPS这样的安全协议来加载。 换言之, 不要使用不安全的协议 (如 HTTP)。 同理,我们建议使用WSS,避免使用WS,建议使用FTPS ,避免使用FTP,等等诸如此类的协议。为什么?HTTPS 有两个主要好处:确保数据完整性,断言数据在您的应用程序和主机之间传输时未被修改。它会加密您的用户和目标主机之间的流量,使窃听应用与主机之间发送的信息变得更加困难。怎么做?main.js (Main Process)// 不推荐browserWindow.loadURL ('http://example.com')// 推荐 browserWindow.loadURL ('https://example.com')index.html (Renderer Process)<!-- 不推荐 --><script crossorigin src="http://example.com/react.js"></script><link rel="stylesheet" href="http://example.com/style.css"><!-- 推荐 --><script crossorigin src="https://example.com/react.js"></script><link rel="stylesheet" href="https://example.com/style.css">2. 不要为远程内容启用 Node.js 集成info此建议是 Electron 自 5.0.0 以来的默认行为。加载远程内容时,不论使用的是哪一种渲染器(BrowserWindow,BrowserView 或者 <webview>),最重要的就是绝对不要启用 Node.js 集成。 其目的是限制您授予远程内容的权限, 从而使攻击者在您的网站上执行 JavaScript 时更难伤害您的用户。在此之后,你可以为指定的主机授予附加权限。 举例来说,如果你正在打开一个指向 https://example.com/ 的 BrowserWindow,那么你可以给他刚刚好足够的权限,但是绝对不要超出这个范围。为什么?如果攻击者跳过渲染进程并在用户电脑上执行恶意代码,那么这种跨站脚本(XSS) 攻击的危害是非常大的。 跨站脚本攻击很常见,通常情况下,威力仅限于执行代码的网站。 禁用Node.js集成有助于防止XSS攻击升级为“远程代码执行” (RCE) 攻击。怎么做?main.js (Main Process)// 不推荐const mainWindow = new BrowserWindow({ webPreferences: { contextIsolation: false, nodeIntegration: true, nodeIntegrationInWorker: true }})mainWindow.loadURL('https://example.com')main.js (Main Process)// 推荐const mainWindow = new BrowserWindow({ webPreferences: { preload: path.join(app.getAppPath(), 'preload.js') }})mainWindow.loadURL('https://example.com')index.html (Renderer Process)<!-- 不推荐 --><webview nodeIntegration src="page.html"></webview><!-- 推荐 --><webview src="page.html"></webview>当禁用Node.js集成时,你依然可以暴露API给你的站点以使用Node.js的模块功能或特性。 预加载脚本依然可以使用require等Node.js特性, 以使开发者可以通过contextBridge API向远程加载的内容公开自定义API。3. 上下文隔离info此建议是 Electron 自 12.0.0 以来的默认行为。上下文隔离是Electron的一个特性,它允许开发者在预加载脚本里运行代码,里面包含Electron API和专用的JavaScript上下文。 实际上,这意味全局对象如 Array.prototype.push 或 JSON.parse等无法被渲染进程里的运行脚本修改。Electron使用了和Chromium相同的Content Scripts技术来开启这个行为。即便使用了 nodeIntegration: false, 要实现真正的强隔离并且防止使用 Node.js 的功能, contextIsolation 也 必须 开启.info获取关于contextIsolation是什么以及如何使用的更多信息,请参阅我们的上下文隔离文档4. 启用进程沙盒化沙盒 是一项 Chromium 功能,它使用操作系统来显著地限制渲染器进程可以访问的内容。 您应该在所有渲染器中启用沙盒。 不建议在一个未启动沙盒的进程(包括主进程)中加载、阅读或处理任何不信任的内容。info欲了解更多有关进程沙盒的信息,以及如何启用它,请查看我们专门的 进程沙盒 文档。5. 处理来自远程内容的会话许可的请求当你使用Chrome时,也许见过这种许可请求:每当网站尝试使用某个特性时,就会弹出让用户手动确认(如网站通知)此API基于Chromium permissions API,并已实现对应的许可类型。为什么?默认情况下,Electron将自动批准所有的许可请求,除非开发者手动配置一个自定义处理函数。 尽管默认如此,有安全意识的开发者可能希望默认反着来。怎么做?main.js (Main Process)const { session } = require('electron')const { URL } = require('url')session .fromPartition('some-partition') .setPermissionRequestHandler((webContents, permission, callback) => { const parsedUrl = new URL(webContents.getURL()) if (permission === 'notifications') { // 批准权限请求 callback(true) } // 验证 URL if (parsedUrl.protocol !== 'https:' || parsedUrl.host !== 'example.com') { // 驳回权限请求 return callback(false) } })6. 不要禁用 webSecurityinfo此建议是 Electron 的默认值。在渲染进程(BrowserWindow、BrowserView 和 <webview>)中禁用 webSecurity 将导致至关重要的安全性功能被关闭。不要在生产环境中禁用webSecurity。为什么?禁用 webSecurity 将会禁止同源策略并且将 allowRunningInsecureContent 属性置 true。 换句话说,这将使得来自其他站点的非安全代码被执行。怎么做?main.js (Main Process)// 不推荐const mainWindow = new BrowserWindow({ webPreferences: { webSecurity: false }})main.js (Main Process)// 推荐const mainWindow = new BrowserWindow()index.html (Renderer Process)<!-- 不推荐 --><webview disablewebsecurity src="page.html"></webview><!-- 推荐 --><webview src="page.html"></webview>7. Content Security Policy(内容安全策略)内容安全策略(CSP) 是应对跨站脚本攻击和数据注入攻击的又一层保护措施。 我们建议任何载入到Electron的站点都要开启。为什么?CSP允许Electron通过服务端内容对指定页面的资源加载进行约束与控制。 如果你定义https://example.com这个源,所属这个源的脚本都允许被加载,反之https://evil.attacker.com不会被允许加载运行。 对于提升你的应用安全性,设置CSP是个很方便的办法。怎么做?下面的CSP设置使得Electron只能执行自身站点和来自apis.example.com的脚本。// 不推荐Content-Security-Policy: '*'// 推荐Content-Security-Policy: script-src 'self' https://apis.example.comCSP HTTP headersElectron 会处理 Content-Security-Policy HTTP 标头,它可以在 webRequest.onHeadersReceived 中进行设置:main.js (Main Process)const { session } = require('electron')session.defaultSession.webRequest.onHeadersReceived((details, callback) => { callback({ responseHeaders: { ...details.responseHeaders, 'Content-Security-Policy': ['default-src \'none\''] } })})CSP meta tagCSP 的首选传输机制是一个 HTTP 头. 但是, 使用 file:// 协议加载资源时,无法使用此方法。 在某些情况下,使用 <meta> 标记直接在标记(makeup)中对页面设置策略 很有用:index.html (Renderer Process)<meta http-equiv="Content-Security-Policy" content="default-src 'none'">8. 不要设置 allowRunningInsecureContent 为 trueinfo此建议是 Electron 的默认值。默认情况下,Electron不允许网站在HTTPS中加载或执行非安全源(HTTP) 中的脚本代码、CSS或插件。 将allowRunningInsecureContent属性设为true将禁用这种保护。当网站的初始内容通过HTTPS加载并尝试在子请求中加载HTTP的资源时,这被称为"混合内容"。为什么?通过HTTPS加载会将该资源进行加密传输,以保证其真实性和完整性。 参看只显示安全内容这节以获得更多信息。怎么做?main.js (Main Process)// 不推荐const mainWindow = new BrowserWindow({ webPreferences: { allowRunningInsecureContent: true }})main.js (Main Process)// 推荐const mainWindow = new BrowserWindow({})9. 不要开启实验性功能info此建议是 Electron 的默认值。Electron 的熟练用户可以通过 experimentalFeatures 属性来启用 Chromium 实验性功能。为什么?如名称所示,实验性功能是实验性的,尚未对所有 Chromium 用户启用。 此外,它们对整个 Electron 的影响很可能没有经过测试。尽管存在合理的使用场景,但是除非你知道你自己在干什么,否则你不应该开启这个属性。怎么做?main.js (Main Process)// 不推荐const mainWindow = new BrowserWindow({ webPreferences: { experimentalFeatures: true }})main.js (Main Process)// 推荐const mainWindow = new BrowserWindow({})10. 不要使用enableBlinkFeaturesinfo此建议是 Electron 的默认值。Blink是Chromium里的渲染引擎名称。 就像experimentalFeatures一样,enableBlinkFeatures属性将使开发者启用被默认禁用的特性。为什么?通常来说,某个特性默认不被开启肯定有其合理的原因。 针对特定特性的合理使用场景是存在的。 作为开发者,你应该非常明白你为何要开启它,有什么后果,以及对你应用安全性的影响。 在任何情况下都不应该推测性的开启特性。怎么做?main.js (Main Process)// 不推荐const mainWindow = new BrowserWindow({ webPreferences: { enableBlinkFeatures: 'ExecCommandInJavaScript' }})main.js (Main Process)// 推荐const mainWindow = new BrowserWindow()11. 不要在 WebViews 中使用 allowpopupsinfo此建议是 Electron 的默认值。如果您正在使用 <webview> ,您可能需要页面和脚本加载进您的 <webview> 标签以打开新窗口。 开启allowpopups属性将使得BrowserWindows可以通过window.open()方法创建。 否则, <webview> 标签内不允许创建新窗口。为什么?如果你不需要弹窗,最好使用默认值以关闭新BrowserWindows的创建。 以下是最低的权限要求原则:若非必要,不要再网站中创建新窗口。怎么做?index.html (Renderer Process)<!-- 不推荐 --><webview allowpopups src="page.html"></webview><!-- 推荐 --><webview src="page.html"></webview>12. 创建WebView前确认其选项通过渲染进程创建的WebView是不开启Node.js集成的,且也不能由自身开启。 但是,WebView可以通过其webPreferences属性创建一个独立的渲染进程。通过控制主进程中创建新的<webview>,并确认其webPreferences没有禁用安全相关特性是个不错的办法。为什么?由于 <webview> 存在在DOM中,因此即使Node继承被禁用,它也可以通过运行在您的 网站上的脚本创建它们。Electron 可以让开发者关闭各种控制渲染进程的安全特性。 通常情况下,开发者并不需要关闭他们中的任何一种 - 因此你不应该允许创建不同配置的<webview>标签怎么做?在 <webview>标签生效前,Electron将产生一个will-attach-webview事件到webContents中。 利用这个事件来阻止可能含有不安全选项的 webViews 创建。main.js (Main Process)app.on('web-contents-created', (event, contents) => { contents.on('will-attach-webview', (event, webPreferences, params) => { // 如果未使用,则删除预加载脚本或验证其位置是否合法 delete webPreferences.preload // 禁用 Node.js 集成 webPreferences.nodeIntegration = false // 验证正在加载的 URL if (!params.src.startsWith('https://example.com/')) { event.preventDefault() } })})同样,这个清单只是将风险降低到最低限度,但没有将其消除。 如果您的目标是展示一个网站,浏览器将是一个更安全的选择。13. 禁用或限制网页跳转如果你的应用不需要导航或只需要导航到已知页面,最好将导航完全限制在该已知范围内,不允许任何其他类型的导航。为什么?导航是一种常见的攻击媒介。 如果攻击者可以诱使你的应用导航离开其当前页面,则他们可能会强制你的应用在 Internet 上打开网站。 即使你的webContents被配置为增强安全(如禁用nodeIntegration或启用contextIsolation),让你的应用打开一个任意的网站依旧是非常简单的操作。一种常见的攻击模式是,攻击者诱导你的应用的用户与此应用进行能够使其导航到攻击者的某个页面的互动。 这通常是通过链接、插件或其他用户生成的内容完成的。怎么做?如果您的应用不需要导航,您可以在 will-navigate 处理器中调用 event.preventDefault()。 如果您知道您的应用程序可能会导航到哪些界面,请在事件处理器中检查URL,并且仅当它与您预期的URL匹配时才进行导航。我们建议您使用Node的解析器来处理URL。 简单的字符串比较有时会出错 - startsWith('https://example.com')测试会让https://example.com.attacker.com通过.main.js (Main Process)const { URL } = require('url')const { app } = require('electron')app.on('web-contents-created', (event, contents) => { contents.on('will-navigate', (event, navigationUrl) => { const parsedUrl = new URL(navigationUrl) if (parsedUrl.origin !== 'https://example.com') { event.preventDefault() } })})14. 禁用或限制新窗口创建如果您有已知的窗口组,那么限制您的应用程序创建额外的窗口是一个好主意。为什么?与导航非常相似,创建新 webContents 是 一种常见的攻击方式。 攻击者试图诱使您的应用创建新的窗口、框架、 或其他渲染过程,拥有比以前更多的权限; 或 打开之前无法打开的页面。如果您除了知道需要创建的窗口之外,还不需要创建 窗口,则禁用创建可以免费为您带来一些额外的 安全性。 对于打开一个 BrowserWindow 并且不需要在运行时打开任意数量的附加 窗口的应用来说,情况通常如此。怎么做?webContents将会在新窗口创建前传递给 打开窗口的处理函数。 除其他参数外,处理程序 将接收请求打开窗口的 url 以及用于创建窗口的选项。 我们建议您注册一个处理程序来 监视窗口的创建,并拒绝任何意外的窗口创建。main.js (Main Process)const { app, shell } = require('electron')app.on('web-contents-created', (event, contents) => { contents.setWindowOpenHandler(({ url }) => { // 在这个例子中,我们要求操作系统 // 在默认浏览器中打开此事件的URL // // 关于哪些URL应该被允许通过shell.openExternal打开, // 请参照以下项目。 if (isSafeForExternalOpen(url)) { setImmediate(() => { shell.openExternal(url) }) } return { action: 'deny' } })})15. 不要对不可信的内容使用 shell.openExternalShell 的 openExternalAPI 允许使用桌面的原生工具打开指定的协议 URI。 例如,在 macOS 上,此功能与 open 终端命令实用程序类似,将基于 URI 和文件类型关联打开特定的应用程序。为什么?错误使用 openExternal 会危害用户的主机 当 openExternal 使用内容不受信任时,它可以用来执行任意命令。怎么做?main.js (Main Process)// 不好const { shell } = require('electron')shell.openExternal(USER_CONTROLLED_DATA_HERE)main.js (Main Process)// 好const { shell } = require('electron')shell.openExternal('https://example.com/index.html')16. 使用当前版本的 Electron你应该努力始终去使用最新版本的 Electron。 每当发布新的主要版本时,你应该尝试尽快更新您的应用。为什么?一个使用 Electron、Chromium 和 Node.js 的旧版本构建的应用程序比使用这些组件的最新版本的应用程序更容易成为目标。 一般来说,较旧的 版本的 Chromium 和 Node.js 的安全问题和漏洞利用更多。Chromium 和 Node.js 都是数千名有才华的开发者建造的令人印象深刻的工程。 鉴于他们受欢迎的程度,他们的安全性都经过专业的安全研究人员仔细的测试和分析。 其中许多研究人员负责任地披露漏洞,这通常意味着研究人员会给 Chromium 和 Node.js 一些时间来修复问题,然后再发布它们。 如果你的应用程序运行的是 Electron 的最新版本 (包括 Chromium 和 Node.js),你的应用程序将更加安全,因为潜在的安全问题并不广为人知。怎么做?一次迁移您的应用一个主要版本, 并且查阅 Electron 的 Breaking Changes 文档查看是否需要更新代码。17. 验证所有 IPC 消息的 sender应始终验证传入的 IPC 消息的 sender 属性,确保 未使用不受信任的渲染器执行动作或向不受信任的渲染器发送信息。为什么?从理论上讲,所有 Web Frame 都可以将 IPC 消息发送到主进程,包括在某些情况下 iframe 和子窗口。 如果您的 IPC 消息通过 event.reply 向发件人返回 用户数据,或者执行了渲染器 无法本机执行的特权操作,则应确保您没有侦听第三方 web frame。您应该默认验证 所有 IPC 消息 sender 。怎么做?main.js (Main Process)// 不好的做法ipcMain.handle('get-secrets', () => { return getSecrets()})// 好的做法ipcMain.handle('get-secrets', (e) => { if (!validateSender(e.senderFrame)) return null return getSecrets()})function validateSender (frame) { // 使用实际的URL解析器和白名单来评估URL的主机 if ((new URL(frame.url)).host === 'electronjs.org') return true return false}
-
开发者经常询问优化 Electron 应用程序性能的策略。 软件工程师、用户和框架开发者并不总是就“性能”的含义达成单一定义。 此文档概述了Electron 维护者最喜欢的减少内存使用、 CPU 负载以及磁盘资源使用的方式。以确保您的应用程序能够响应用户输入并尽快完成操作。 此外,我们希望所有的性能策略都能保持您应用的高标准安全。关于如何使用 JavaScript构建高性能网站的技巧和方法通常也适用于Electron 应用程序。 在某种程度上,讨论如何构建高性能 Node.js 应用的方法同样也适用。但是小心理解“性能”一词的含义对于 Node.js 后端和客户端程序并不相同。文中的列表提供了一些方便,同时也需要注意,它和我们的安全性检查列表类似,并不详尽。 即使你参照了下面提到的所有步骤,依然有可能构建出来一个性能低的Electron应用。 Electron是一个强大的开发平台,可以让开发人员按照自己所想,做更多的或更少的事情。 而这种自由的代价就是开发者需要承担大部分性能上的责任。再三权衡以下列举了一些直截了当、易于实现的方式。 但是,如果你想构建性能最优秀的应用,仅仅这些是不够的。 你需要仔细检查应用中运行的所有代码,认真地进行分析和衡量。 瓶颈在哪里? 当用户点击按钮时,哪些操作的执行占用了最多的时间? 当应用程序被挂起时,哪些对象占用了最多的内存?通过多次的尝试,我们发现,构建高性能的Electron应用程序,最成功的策略是分析正在运行的代码,查找其中最耗资源的部分,然后对其进行优化。 一遍又一遍地重复这个“搬砖”的过程,将极大地提高应用程序的性能。 在大型应用程序(例如Visual Studio Code、Slack)中的实践经验证明了这是目前最可靠的性能提升策略。要了解更多关于如何分析应用程序代码的信息,请熟悉Chrome开发者工具。 若要高级分析查看多个进程,请使用 Chrome Tracing 工具。推荐阅读从分析运行时性能开始谈:“Visual Studio Code - 第一个一秒”清单:性能建议如果你尝试这些步骤,你的应用可能会略微简洁、快速,而且一般来说会更少出现资源不足的情况。谨慎地加载模块过早的加载和执行代码阻塞主进程阻塞渲染进程不必要的polyfills不必要的或者阻塞的网络请求打包你的代码1. 谨慎地加载模块在向你的应用程序添加一个 Node.js 模块之前,请检查这个模块。 这个模块包含了多少依赖? 简单的一个a require()声明中包含了什么种类的资源? 你可能发现NPM包注册的最多的或者Github上Star最多的模块实际上并不是最简单或者最小可用的模块。为什么?这一建议背后的理由最好用一个真实的例子来说明。 在Electron最开始的那些日子,可靠的检查网络连接是一个问题,导致很多应用公开的使用了一个简单的isOnline()方法。该模块通过尝试访问多个众所周知的端点,检测到您的网络连接。 至于这些资源的列表,它取决于一个完全不同的模块,这个模块也包含一个众所周知的端口。 这个依赖本身又依赖一个包含超过 100 000行端口信息的JSON文件的模块。 每当模块加载时(通常用 require('module')),它会加载所有依赖关系并最终读取并解析此 JSON 文件。 解析几千行的JSON是一个非常繁重的操作。 在性能差的机器上,它会占用整整几秒的时间。在许多服务器环境中,启动时间几乎无关紧要。 一个Node.js 服务器要求所有端口的信息可能实际上是“性能更好” 如果服务器在启动时将所有需要的信息加载到内存,这样就能更快地为响应请求。 此示例中讨论的模块不是一个“坏”模块。 然而,Electron 应用不应该将实际上不需要的信息加载、解析和存储在内存中。简而言之,一个主要为运行在Linux系统上的Node.js 服务器编写的模块,虽然看起来很好,但是对你的应用性能来说可能是个坏消息。 在这个特殊的示例中,正确的解决方案是根本不需要加载模块, 而是使用了一 个包含在以后版本的 Chromium 中的连接性检查。怎么做?当考虑一个模块时,我们建议你做以下检查:包含的依赖项的大小需要加载的(require()) 资源你所加载的资源能够执行你关心的操作可以使用命令行上的单个命令生成用于加载模块的 CPU 配置文件和堆内存配置文件 在下面的示例中,我们看一下受欢迎的模块 request。node --cpu-prof --heap-prof -e "require('request')"执行此命令将在您执行的目录下生成一个.cpuprofile和一个.heapprofile 文件。 这两个文件都可以使用 Chrome 开发者工具进行分析,分别使用 Performance 和 Memory 标签 进行分析。在这个例子里,我们看到在作者的机器上加载request 大概用了半秒钟,其中 node-fetch明显占用了极少的内存并且加载用时少于 50ms。2. 过早的加载和执行代码如果你有非常繁重的初始化操作,请考虑推迟进行。 程序启动立刻查看应用执行的全部工作。 考虑按照用户操作的顺序将它们错开执行,而不是立刻执行所有的操作。在传统的Node.js开发中,我们习惯将所有的require()语句放在代码顶部。 如果你目前正在使用相同的策略and并且使用你不需要立即加载的大型模块编写你的 Electron 应用程序,使用相同的策略并推迟到更适当的时机加载。为什么?加载模块是令人吃惊的繁重的操作,尤其是在Windows上。 当你的应用开始,不应该让用户等待当时不需要的操作。这似乎是显而易见的, 但许多应用程序在程序启动后可能会马上完成大量的 工作 - 如检查更新,正在下载稍后流程中使用的内容,或执行大型的磁盘I/O 操作。让我们把Visual Studio Code作为一个例子。 当你打开一个文件,它会立刻展示没有高亮任何代码的内容,优先实现和文本交互的功能。 一旦它完成了这项工作,它将继续让代码高亮。怎么做?让我们考虑一个示例,并假定您的应用程序正在以架空的.foo形式解析文件 。 为了做到这一点,它依赖同样架空的foo-parserver 模块。 在传统的 Node.js 开发中,你可以写代码热加载依赖:parser.jsconst fs = require('fs')const fooParser = require('foo-parser')class Parser { constructor () { this.files = fs.readdirSync('.') } getParsedFiles () { return fooParser.parse(this.files) }}const parser = new Parser()module.exports = { parser }在上面的例子中,我们做了很多工作,一旦文件加载,我们就会立即执行。 我们需要立即获取解析的文件吗? 或许我们可以晚一点再做这件事,当getParsedFiles() 真正的执行到的时候?parser.js// "fs"很可能已经被加载,所以require()调用是廉价的const fs = require('fs')class Parser { async getFiles () { // 一旦调用getFiles,立即触发磁盘读取操作,而不是提前进行 // 此外,通过使用异步方法 // 确保我们不会阻塞其他操作 this.files = this.files || await fs.promises.readdir('.') return this.files } async getParsedFiles () { // 我们假设 foo-parser 是一个庞大且耗费资源的模块 // 因此将这个工作推迟到我们真正需要解析文件时再进行 // 由于require()带有模块缓存 // require()调用只会有一次开销 // 后续对getParsedFiles()的调用将会更快 const fooParser = require('foo-parser') const files = await this.getFiles() return fooParser.parse(files) }}// 现在此操作的开销比我们之前的示例要低得多const parser = new Parser()module.exports = { parser }简而言之,只有当需要的时候才分配资源,而不是在你的应用启动时分配所有。3. 阻塞主进程Electron的主要进程(有时称为“浏览器进程”) 非常特殊:它是与你应用的所有其他进程的父进程,也是和操作系统交互的关键进程。 它负责处理窗口、交互以及应用程序内各个组件之间的通信。 它还包含了UI线程。在任何情况下你都不应阻塞此进程或者运行时间长的用户界面线程。 阻塞UI线程意味着您的整个应用程序将冻结直到主进程准备好继续处理。为什么?主进程和其UI线程实质上是你的应用程序内重要操作的控制中心。 当操作系统向你的应用程序报告鼠标点击事件时,它会经过主进程,然后才到达你的窗口。 如果您的窗口呈现黄色平滑动画, 它需要和 GPU 进程进行通信——再次穿越主进程。Electron 和 Chromium 谨慎地将大型的磁盘I/O 和 CPU绑定的操作放入新线程,以避免阻塞UI 线程。 你也应该这样做。怎么做?Electron强大的多进程架构随时准备帮助你完成你的长期任务,但其中也包含少量性能陷阱。对于需要长期占用 CPU 的繁重任务,利用worker threads,请考虑将它们移动到 BrowserWindow,或(作为最后手段)生成一个专用进程。尽可能避免使用同步 IPC 和 @electron/remote 模块。 虽然有合法的使用案例,但很容易不知情地阻塞 UI 线程。避免在主进程中使用阻塞 I/O 操作。 简而言之,每当Node.js的核心模块 (如fs 或 child_process) 提供一个同步版本或 异步版本,你更应该使用异步和非阻塞式的变量。4. 阻塞渲染进程自从 Electron 使用了当前版本的 Chrome,你可以使用Web 平台提供的最新和最优秀的功能来推迟或卸载繁重的操作,以使你的应用保持流畅和迅速的反应。为什么?你的应用可能有很多JavaScript在渲染过程中运行。 有个技巧是尽快执行操作,而不占用保持滚动平滑、响应用户输入或60帧/秒动画所需的资源。如果有用户抱怨你的应用“口吃”的时候在渲染的代码中编排操作流就显得尤其重要。怎么做?一般来说,所有用于构建现代浏览器的性能网络应用程序的建议,对于Electron 的渲染器也同样适用。 现在处理你的应用的主要两个方法是对于小的操作使用requestIdleCallback() 而长时间运行的操作使用 Web Workers。requestIdleCallback()允许开发者将函数排队为在进程进入空闲期后立刻执行。 它使你能够在不影响用户体验的情况下执行低优先级或后台执行的工作。 想要了解如何使用它的更多信息,请查看MDN上的文档。Web Workers是在单独线程上运行代码的一个好方式。 有一些注意事项需要考虑 - 请查阅 Electron 的 多线程文档 和 MDN 的 Web Workers文档。 对于长时间并且大量使用CPU的操作来说它们是一个理想的解析器。5. 不必要的polyfillsElectron的一大好处是,你准确地知道哪个引擎将解析你的 JavaScript, HTML和CSS。 如果你重新设计的代码是为整个网页编写的,请确保不会polyfill包含在Electron 中的特性。为什么?现在互联网构建网页应用程序时,最老的环境决定了你能够和不能使用的功能。 尽管Electron支持性能良好的 CSS 选择器和动画,但是较早的浏览器可能不支持。 在你可以使用WebGL的场合,你的开发者可能选择了一个资源更加匮乏的解决方案来支持旧机器。当它遇到JavaScript时, 你可能已经包含了工具包库,如DOM选择器 jQuery 或是 如regenerator-runtime支持async/await 的polyfills。基于 JavaScript 的polyfill速度比Electron 中的原生特征要快一些。 不要通过发布你自己的网络平台标准来减慢你的 Electron 应用速度。怎么做?假定当前版本的 Electron不需要使用polyfills。 如果你有所疑虑,检查 caniuse.com 以确认 是否在你的Electron版本中使用的Chromium版本 已经支持了你需要的特性.此外,仔细检查您使用的三方库。 它们是否真的必要? 例如,jQuery非常成功,它的许多功能现在都是 标准JavaScript功能设置的 的一部分。如果您正在使用 TypeScript 这样的编译器,检查它的配置并确保你的目标是Electron 支持的最新 ECMAScript 版本。6. 不必要的或者阻塞的网络请求避免从互联网中获取几乎不变化的资源,如果它可以轻松地与你的应用程序捆绑起来。为什么?许多开始使用基于Web的应用程序的Electron用户后来都使用了桌面应用。 作为网页开发者,我们习惯了从各种内容交付网站加载资源。 现在你正在发布一个桌面应用程序,尽可能地尝试“切断连接”,避免让用户等待那些从不改变且可以轻松包含在应用程序中的资源。一个典型的例子是谷歌字体。 许多开发者使用谷歌令人印象深刻的免费字体集,这些字体通过内容交付网络获取。 方法显而易见:包括几行CSS 和谷歌将处理其余部分。构建Electron应用程序时,如果你下载字体并将其包含在应用包中,你的用户将会得到更好的服务。怎么做?在理想情况下,你的应用程序不需要网络就可以运行。 要达到这个目标,你必须了解你的应用正在下载哪些资源以及这些资源的大小。要做到这一点,请打开开发者工具。 导航到 Network 选项卡,然后检查 Disable cache 选项。 然后重新加载你的页面。 除非你的应用禁止重新加载, 你通常可以在使用开发者工具时点击Cmd + R 或Ctrl + R触发重新加载。开发者工具将仔细记录所有网络请求。 第一步,评估正在下载的所有资源,首先侧重于较大的文件。 其中是否有任何图像、字体或媒体文件不会改变并且可以包含在你的包中? 如果可以,把它们打包。下一步,启用 Network Throttling。 查找当前读取Online的下拉列表,并选择较慢的速度,例如Fast 3G。 重新加载你的页面并查看你的应用程序是否有等待任何不必要的资源。 在大多数情况下,尽管实际上不需要相关的资源,应用还是会等待网络请求完成。作为一个提示, 从互联网上加载你可能想要更改的而不发送应用程序更新是一个强有力的策略。 为了进一步控制如何加载资源,请考虑使用Service Worker。7. 打包你的代码正如中已经指出的那样,"加载和运行代码太早", 调用 require() 是一项繁重的操作。 如果你能够这样做,将你的应用程序的代码打包到单个文件中。为什么?现代JavaScript开发通常涉及许多文件和模块。 对于使用Electron开发的人来说这是非常好的事情,我们强烈建议你将你的代码打包到单个文件中以确保调用require() 时只在你的应用加载花费一次开销。怎么做?有许多JavaScript打包的方法可供使用,我们知道我们最好不要因为推荐某一种工具来使得社区不满。 然而,我们的确建议您使用一个能够处理Electron独特的环境的打包程序,它需要处理Node.js 和浏览器两种环境。在撰写这篇文章时,受欢迎的选择包括Webpack, Parcel和rollup.js。8. 当你不需要默认菜单时调用 Menu.setApplicationMenu(null)Electron在启动时将设置一个默认菜单,其中包含一些标准条目。 但是你的应用程序或许希望更改默认菜单,这么做有助于提高启动性能。为什么?如果你打算构建自己的菜单或使用无帧窗口而不使用原生菜单,你应该尽早告诉 Electron 不要设置默认菜单。怎么做?在 app.on("ready") 之前调用 Menu.setApplicationMenu(null) 。 这将阻止Electron设置默认菜单。 了解相关讨论,请参阅 https://github.com/electron/electron/issues/35512。
-
MessagePort是一个允许在不同上下文之间传递消息的Web功能。 就像 window.postMessage, 但是在不同的通道上。 此文档的目标是描述 Electron 如何扩展 Channel Messaging model ,并举例说明如何在应用中使用 MessagePorts下面是 MessagePort 是什么和如何工作的一个非常简短的例子:renderer.js (Renderer Process)// 消息端口是成对创建的。 连接的一对消息端口// 被称为通道。const channel = new MessageChannel()// port1 和 port2 之间唯一的不同是你如何使用它们。 消息// 发送到port1 将被port2 接收,反之亦然。const port1 = channel.port1const port2 = channel.port2// 允许在另一端还没有注册监听器的情况下就通过通道向其发送消息// 消息将排队等待,直到一个监听器注册为止。port2.postMessage({ answer: 42 })// 这次我们通过 ipc 向主进程发送 port1 对象。 类似的,// 我们也可以发送 MessagePorts 到其他 frames, 或发送到 Web Workers, 等.ipcRenderer.postMessage('port', null, [port1])main.js (Main Process)// 在主进程中,我们接收端口对象。ipcMain.on('port', (event) => { // 当我们在主进程中接收到 MessagePort 对象, 它就成为了 // MessagePortMain. const port = event.ports[0] // MessagePortMain 使用了 Node.js 风格的事件 API, 而不是 // web 风格的事件 API. 因此使用 .on('message', ...) 而不是 .onmessage = ... port.on('message', (event) => { // 收到的数据是: { answer: 42 } const data = event.data }) // MessagePortMain 阻塞消息直到 .start() 方法被调用 port.start()})关于 channel 消息接口的使用文档详见 Channel Messaging API主进程中的 MessagePorts在渲染器中, MessagePort 类的行为与它在 web 上的行为完全一样。 但是,主进程不是网页(它没有 Blink 集成),因此它没有 MessagePort 或 MessageChannel 类。 为了在主进程中处理 MessagePorts 并与之交互,Electron 添加了两个新类: MessagePortMain 和 MessageChannelMain。 这些行为 类似于渲染器中 analogous 类。MessagePort 对象可以在渲染器或主 进程中创建,并使用 ipcRenderer.postMessage 和 WebContents.postMessage 方法互相传递。 请注意,通常的 IPC 方法,例如 send 和 invoke 不能用来传输 MessagePort, 只有 postMessage 方法可以传输 MessagePort。通过主进程传递 MessagePort,就可以连接两个可能无法通信的页面 (例如,由于同源限制) 。扩展: close 事件Electron在 MessagePort 添加了一个在Web上本不存在的功能,以使MessagePort更加好用。 这个功能就是 close 事件, 在通道的另一端关闭时会触发该事件。 端口也可以通过垃圾回收而隐式关闭。在渲染进程中,你可以通过将事件分配给port.onclose 或调用 port.addEventListener('close', ...) 来监听 close 事件。 在主进程中,你可以通过调用 port.on('close', ...) 来监听 close 事件。实例使用在两个渲染进程之间建立 MessageChannel在这个示例中,主进程设置了一个MessageChannel,然后将每个端口发送给不同的渲染进程。 这样可以让渲染进程彼此之间发送消息,而无需使用主进程作为中转。main.js (Main Process)const { BrowserWindow, app, MessageChannelMain } = require('electron')app.whenReady().then(async () => { // 创建窗口 const mainWindow = new BrowserWindow({ show: false, webPreferences: { contextIsolation: false, preload: 'preloadMain.js' } }) const secondaryWindow = new BrowserWindow({ show: false, webPreferences: { contextIsolation: false, preload: 'preloadSecondary.js' } }) // 建立通道 const { port1, port2 } = new MessageChannelMain() // webContents准备就绪后,使用postMessage向每个webContents发送一个端口。 mainWindow.once('ready-to-show', () => { mainWindow.webContents.postMessage('port', null, [port1]) }) secondaryWindow.once('ready-to-show', () => { secondaryWindow.webContents.postMessage('port', null, [port2]) })})接下来,在你的预加载脚本中通过IPC接收端口,并设置相应的监听器。preloadMain.js and preloadSecondary.js (Preload scripts)const { ipcRenderer } = require('electron')ipcRenderer.on('port', e => { // 接收到端口,使其全局可用。 window.electronMessagePort = e.ports[0] window.electronMessagePort.onmessage = messageEvent => { // 处理消息 }})在这个示例中,messagePort 直接绑定到了 window 对象上。 更好的方法是使用 contextIsolation,并为每个预期的消息设置特定的 contextBridge 调用, 但为了示例简洁,这里没有这样做。 你可以在本页面下方的 直接在上下文隔离页面的主进程和主世界之间进行通信部分找到一个上下文隔离的示例。这意味着 window.electronMessagePort 在全局范围内可用,你可以在应用程序的任何地方调用postMessage 方法,以便向另一个渲染进程发送消息。renderer.js (Renderer Process)// elsewhere in your code to send a message to the other renderers message handlerwindow.electronMessagePort.postMessage('ping')Worker进程在这个示例中,你的应用程序有一个作为隐藏窗口存在的 Worker 进程。 你希望应用程序页面能够直接与 Worker 进程通信,而不需要通过主进程进行中继,以避免性能开销。main.js (Main Process)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.html<script>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) }})</script>app.html<script>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)})</script>回复流Electron的内置IPC方法只支持两种模式:即发即弃(例如, send),或请求-响应(例如, invoke)。 使用MessageChannels,你可以实现一个“响应流”,其中单个请求可以返回一串数据。renderer.js (Renderer Process)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次main.js (Main Process)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()})直接在上下文隔离页面的主进程和主世界之间进行通信当 [context isolation][] 已启用。 IPC 消息从主进程发送到渲染器是发送到隔离的世界,而不是发送到主世界。 有时候你希望不通过隔离的世界,直接向主世界发送消息。main.js (Main Process)const { BrowserWindow, app, MessageChannelMain } = require('electron')const path = require('path')app.whenReady().then(async () => { // 创建一个启用了 contextIsolation 的 BrowserWindow const bw = new BrowserWindow({ webPreferences: { contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }) bw.loadURL('index.html') // We'll be sending one end of this channel to the main world of the // context-isolated page. const { port1, port2 } = new MessageChannelMain()// 允许在另一端还没有注册监听器的情况下就通过通道向其发送消息 消息将排队等待,直到有一个监听器注册为止。 port2.postMessage({ test: 21 }) // 我们也可以接收来自渲染器主进程的消息。 port2.on('message', (event) => { console.log('from renderer main world:', event.data) }) port2.start() // 预加载脚本将接收此 IPC 消息并将端口 // 传输到主进程。 bw.webContents.postMessage('main-world-port', null, [port1])})preload.js (Preload Script)const { ipcRenderer } = require('electron')// 在发送端口之前,我们需要等待主窗口准备好接收消息 我们在预加载时创建此 promise ,以此保证// 在触发 load 事件之前注册 onload 侦听器。const windowLoaded = new Promise(resolve => { window.onload = resolve})ipcRenderer.on('main-world-port', async (event) => { await windowLoaded // 我们使用 window.postMessage 将端口 // 发送到主进程 window.postMessage('main-world-port', '*', event.ports)})index.html<script>window.onmessage = (event) => { // event.source === window 意味着消息来自预加载脚本 // 而不是来自iframe或其他来源 if (event.source === window && event.data === 'main-world-port') { const [ port ] = event.ports // 一旦我们有了这个端口,我们就可以直接与主进程通信 port.onmessage = (event) => { console.log('from main process:', event.data) port.postMessage(event.data * 2) } }}</script>
-
Chromium的一个关键安全特性是,进程可以在沙盒中执行。 沙盒通过限制对大多数系统资源的访问来减少恶意代码可能造成的伤害 — 沙盒化的进程只能自由使用CPU周期和内存。 为了执行需要额外权限的操作,沙盒处的进程通过专用通信渠道将任务下放给更大权限的进程。在Chromium中,沙盒化应用于主进程以外的大多数进程。 其中包括渲染器进程,以及功能性进程,如音频服务、GPU 服务和网络服务。查阅Chromium的 沙箱设计文档 了解更多信息。从 Electron 20 开始,渲染进程默认启用了沙盒,无需进一步配置。 如果你想禁用某个进程的沙盒,请参阅为单个进程禁用沙盒部分。Electron 中的沙盒行为在 Electron 中沙盒进程 大部分地 表现都与 Chromium 差不多, 但因为介面是 Node.js 的关系 Electron 有一些额外的概念需要考虑。渲染器进程当 Electron 中的渲染进程被沙盒化时,它们的行为与常规 Chrome 渲染器一样。 一个沙盒化的渲染器不会有一个 Node.js 环境。因此,在沙盒中,渲染进程只能透过 进程间通讯 (inter-process communication, IPC) 委派任务给主进程的方式, 来执行需权限的任务 (例如:文件系统交互,对系统进行更改或生成子进程) 。note想要了解更多关于进程间通信的信息,请参阅我们的 IPC 指南。Preload 脚本为了让渲染进程能与主进程通信,附属于沙盒化的渲染进程的 preload 脚本中仍可使用一部分以 Polyfill 形式实现的 Node.js API。 有一个与 Node 中类似的 require 函数提供了出来,但只能载入 Electron 和 Node 内置模块的一个子集:electron (以下是渲染进程的模块: contextBridge, crashReporter, ipcRenderer, nativeImage, webFrame)事件timersurlNode. js 中的import方法也是被支持的:eventstimersurl此外,以下 Node.js 基础对象也填充到了 preload 脚本的全局上下文中:BufferprocessclearImmediatesetImmediaterequire 函数只是一个功能有限的 Ployfill 实现,并不支持把 preload 脚本拆成多个文件然后作为 CommonJS 模块 来加载。 若需要拆分 preload 脚本的代码,可以使用 webpack 或 Parcel 等打包工具。注意,因为 preload 脚本的运行环境本质上比沙盒化渲染进程的拥有更高的特权,除非开启了 contextIsolation,否则高特权的 API 仍有可能被泄漏给渲染进程中的不信任代码。配置沙盒对于大多数应用程序来说,沙盒是最佳选择。 在某些与沙盒不兼容的使用情况下(例如,在渲染器中使用原生的 Node.js 模块时),可以禁用特定进程的沙盒。 但这会带来安全风险,特别是当未受信任的代码或内容存在于未沙盒化的进程中时。为单个进程禁用沙盒在 Electron 中,可通过在 BrowserWindow 构造函数中使用 sandbox: false选项来针对每个进程禁用渲染器沙盒。main.jsapp.whenReady().then(() => { const win = new BrowserWindow({ webPreferences: { sandbox: false } }) win.loadURL('https://google.com')})在渲染器中启用 nodeIntegration 时,沙盒也会被禁用。 可以通过在 BrowserWindow 构造函数中添加 nodeIntegration: true 标志的来实现。main.jsapp.whenReady().then(() => { const win = new BrowserWindow({ webPreferences: { nodeIntegration: true } }) win.loadURL('https://google.com')})全局启用沙盒你也可以调用 app.enableSandbox API 来强制沙盒化所有渲染器。 注意,此 API 必须在应用的 ready 事件之前调用。main.jsapp.enableSandbox()app.whenReady().then(() => } // 因为调用了app.enableSandbox(),所以任何sandbox:false的调用都会被覆盖。 const win = new BrowserWindow() win.loadURL('https://google.com')})禁用 Chromium 的沙盒(仅测试)你也可以指定 --no-sandbox 命令行参数来完全禁用 Chromium 的沙盒功能,这会使沙盒对所有进程失效(包括工具进程)。 我们强烈建议你只针对测试用途开启此标志,并且 永远 不要用于生产环境。注意,sandbox: true 选项也会同时禁用渲染进程中的 Node.js 环境。渲染不可信内容的注意事项尽管已经有一些成功案例(例如 Beaker 浏览器),但在 Electron 中渲染不受信任的内容仍有未知的风险。 我们的目标是尽可能达到与 Chrome 中沙盒化的内容一样的安全性,但由于一些现实因素还没法做到:我们不像 Chromium 团队那样在产品安全方面有专属的资源与专业知识。 虽然已经尽可能地继承 Chromium 中的一切,并且尽快响应安全问题,但缺少 Chromium 可调动的那些资源,我们做不到和它一样安全。Chrome 的一些安全特性(例如安全浏览和证书透明度)依赖于中心化授权和专属服务器,这些都超出了 Electron 项目的目标。 因此我们在 Electron 中禁用了它们,同时也损失了它们带来的安全性。Chromium 只有一个,但基于 Electron 构建的应用却成千上万,并且千差万别。 这些差异带来了太多的可能性,很难在各种特殊的应用场景下都保证平台的安全。我们没法向终端用户直接推送安全更新,只能靠应用供应商更新依赖的 Electron 版本,来让更新覆盖到用户。虽然我们会尽可能将 Chromium 的安全修复应用到老版本的 Electron 中,但没法保证每一个修复都能移植过去。 为了保证安全,最好的办法还是始终使用最新的稳定版 Electron。
-
进程间通信 (IPC) 是在 Electron 中构建功能丰富的桌面应用程序的关键部分之一。 由于主进程和渲染器进程在 Electron 的进程模型具有不同的职责,因此 IPC 是执行许多常见任务的唯一方法,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容的更改。IPC 通道在 Electron 中,进程使用 ipcMain 和 ipcRenderer 模块,通过开发人员定义的“通道”传递消息来进行通信。 这些通道是 任意 (您可以随意命名它们)和 双向 (您可以在两个模块中使用相同的通道名称)的。在本指南中,我们将介绍一些基本的 IPC 模式,并提供具体的示例。您可以将这些示例作为您应用程序代码的参考。了解上下文隔离进程在开始实现细节之前,您应该熟悉使用 预加载脚本 在上下文隔离渲染器进程中导入 Node.js 和 Electron 模块的概念。有关 Electron 进程模型的完整概述,您可以阅读 进程模型文档。有关使用 contextBridge 模块从预加载脚本暴露 API 的入门知识,请查看 上下文隔离教程。模式 1:渲染器进程到主进程(单向)要将单向 IPC 消息从渲染器进程发送到主进程,您可以使用 ipcRenderer.send API 发送消息,然后使用 ipcMain.on API 接收。通常使用此模式从 Web 内容调用主进程 API。 我们将通过创建一个简单的应用来演示此模式,可以通过编程方式更改它的窗口标题。对于此演示,您需要将代码添加到主进程、渲染器进程和预加载脚本。 完整代码如下,我们将在后续章节中对每个文件进行单独解释。docs/fiddles/ipc/pattern-1 (26.1.0)main.jspreload.jsindex.htmlrenderer.jsconst { app, BrowserWindow, ipcMain } = require('electron')const path = require('path')function createWindow () { const mainWindow = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, 'preload.js') } }) ipcMain.on('set-title', (event, title) => { const webContents = event.sender const win = BrowserWindow.fromWebContents(webContents) win.setTitle(title) }) mainWindow.loadFile('index.html')}app.whenReady().then(() => { createWindow() app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow() })})app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit()})1. Listen for events with ipcMain.onIn the main process, set an IPC listener on the set-title channel with the ipcMain.on API:main.js (Main Process)const { app, BrowserWindow, ipcMain } = require('electron')const path = require('path')// ...function handleSetTitle (event, title) { const webContents = event.sender const win = BrowserWindow.fromWebContents(webContents) win.setTitle(title)}function createWindow () { const mainWindow = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, 'preload.js') } }) mainWindow.loadFile('index.html')}app.whenReady().then(() => { ipcMain.on('set-title', handleSetTitle) createWindow()})// ...上面的 handleSetTitle 回调函数有两个参数:一个 IpcMainEvent 结构和一个 title 字符串。 每当消息通过 set-title 通道传入时,此函数找到附加到消息发送方的 BrowserWindow 实例,并在该实例上使用 win.setTitle API。info请确保您为以下步骤加载了 index.html 和 preload.js 入口点!2. 通过预加载脚本暴露 ipcRenderer.send要将消息发送到上面创建的监听器,您可以使用 ipcRenderer.send API。 默认情况下,渲染器进程没有权限访问 Node.js 和 Electron 模块。 作为应用开发者,您需要使用 contextBridge API 来选择要从预加载脚本中暴露哪些 API。在您的预加载脚本中添加以下代码,向渲染器进程暴露一个全局的 window.electronAPI 变量。preload.js (Preload Script)const { contextBridge, ipcRenderer } = require('electron')contextBridge.exposeInMainWorld('electronAPI', { setTitle: (title) => ipcRenderer.send('set-title', title)})此时,您将能够在渲染器进程中使用 window.electronAPI.setTitle() 函数。安全警告出于 安全原因,我们不会直接暴露整个 ipcRenderer.send API。 确保尽可能限制渲染器对 Electron API 的访问。3. 构建渲染器进程 UI在 BrowserWindow 加载的我们的 HTML 文件中,添加一个由文本输入框和按钮组成的基本用户界面:index.html<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"> <title>Hello World!</title> </head> <body> Title: <input id="title"/> <button id="btn" type="button">Set</button> <script src="./renderer.js"></script> </body></html>为了使这些元素具有交互性,我们将在导入的 renderer.js 文件中添加几行代码,以利用从预加载脚本中暴露的 window.electronAPI 功能:renderer.js (Renderer Process)const setButton = document.getElementById('btn')const titleInput = document.getElementById('title')setButton.addEventListener('click', () => { const title = titleInput.value window.electronAPI.setTitle(title)})此时,您的演示应用应该已经功能齐全。 尝试使用输入框,看看 BrowserWindow 的标题会发生什么变化!模式 2:渲染器进程到主进程(双向)双向 IPC 的一个常见应用是从渲染器进程代码调用主进程模块并等待结果。 这可以通过将 ipcRenderer.invoke 与 ipcMain.handle 搭配使用来完成。在下面的示例中,我们将从渲染器进程打开一个原生的文件对话框,并返回所选文件的路径。对于此演示,您需要将代码添加到主进程、渲染器进程和预加载脚本。 完整代码如下,我们将在后续章节中对每个文件进行单独解释。docs/fiddles/ipc/pattern-2 (26.1.0)main.jspreload.jsindex.htmlrenderer.jsconst { app, BrowserWindow, ipcMain, dialog } = require('electron')const path = require('path')async function handleFileOpen () { const { canceled, filePaths } = await dialog.showOpenDialog() if (!canceled) { return filePaths[0] }}function createWindow () { const mainWindow = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, 'preload.js') } }) mainWindow.loadFile('index.html')}app.whenReady().then(() => { ipcMain.handle('dialog:openFile', handleFileOpen) createWindow() app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow() })})app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit()})1. 使用 ipcMain.handle 监听事件在主进程中,我们将创建一个 handleFileOpen() 函数,它调用 dialog.showOpenDialog 并返回用户选择的文件路径值。 每当渲染器进程通过 dialog:openFile 通道发送 ipcRender.invoke 消息时,此函数被用作一个回调。 然后,返回值将作为一个 Promise 返回到最初的 invoke 调用。关于错误处理在主进程中通过 handle 引发的错误是不透明的,因为它们被序列化了,并且只有原始错误的 message 属性会提供给渲染器进程。 详情请参阅 [#24427](https://github.com/electron/electron/issues/24427)。main.js (Main Process)const { app, BrowserWindow, dialog, ipcMain } = require('electron')const path = require('path')// ...async function handleFileOpen () { const { canceled, filePaths } = await dialog.showOpenDialog({}) if (!canceled) { return filePaths[0] }}function createWindow () { const mainWindow = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, 'preload.js') } }) mainWindow.loadFile('index.html')}app.whenReady().then(() => { ipcMain.handle('dialog:openFile', handleFileOpen) createWindow()})// ...关于通道名称IPC 通道名称上的 dialog: 前缀对代码没有影响。 它仅用作命名空间以帮助提高代码的可读性。info请确保您为以下步骤加载了 index.html 和 preload.js 入口点!2. 通过预加载脚本暴露 ipcRenderer.invoke在预加载脚本中,我们暴露了一个单行的 openFile 函数,它调用并返回 ipcRenderer.invoke('dialog:openFile') 的值。 我们将在下一步中使用此 API 从渲染器的用户界面调用原生对话框。preload.js (Preload Script)const { contextBridge, ipcRenderer } = require('electron')contextBridge.exposeInMainWorld('electronAPI', { openFile: () => ipcRenderer.invoke('dialog:openFile')})安全警告出于 安全原因,我们不会直接暴露整个 ipcRenderer.invoke API。 确保尽可能限制渲染器对 Electron API 的访问。3. 构建渲染器进程 UI最后,让我们构建加载到 BrowserWindow 中的 HTML 文件。index.html<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"> <title>Dialog</title> </head> <body> <button type="button" id="btn">Open a File</button> File path: <strong id="filePath"></strong> <script src='./renderer.js'></script> </body></html>用户界面包含一个 #btn 按钮元素,将用于触发我们的预加载 API,以及一个 #filePath 元素,将用于显示所选文件的路径。 要使这些部分起作用,需要在渲染器进程脚本中编写几行代码:renderer.js (Renderer Process)const btn = document.getElementById('btn')const filePathElement = document.getElementById('filePath')btn.addEventListener('click', async () => { const filePath = await window.electronAPI.openFile() filePathElement.innerText = filePath})在上面的代码片段中,我们监听 #btn 按钮的点击,并调用 window.electronAPI.openFile() API 来激活原生的打开文件对话框。 然后我们在 #filePath 元素中显示选中文件的路径。注意:对于旧方法ipcRenderer.invoke API 是在 Electron 7 中添加的,作为处理渲染器进程中双向 IPC 的一种开发人员友好的方式。 但这种 IPC 模式存在几种替代方法。如果可能,请避免使用旧方法我们建议尽可能使用 ipcRenderer.invoke 。 出于保留历史的目地,记录了下面双向地渲染器到主进程模式。info对于以下示例,我们将直接从预加载脚本调用 ipcRenderer,以保持代码示例短小。使用 ipcRenderer.send我们用于单向通信的 ipcRenderer.send API 也可用于双向通信。 这是在 Electron 7 之前通过 IPC 进行异步双向通信的推荐方式。preload.js (Preload Script)// 您也可以使用 `contextBridge` API// 将这段代码暴露给渲染器进程const { ipcRenderer } = require('electron')ipcRenderer.on('asynchronous-reply', (_event, arg) => { console.log(arg) // 在 DevTools 控制台中打印“pong”})ipcRenderer.send('asynchronous-message', 'ping')main.js (Main Process)ipcMain.on('asynchronous-message', (event, arg) => { console.log(arg) // 在 Node 控制台中打印“ping” // 作用如同 `send`,但返回一个消息 // 到发送原始消息的渲染器 event.reply('asynchronous-reply', 'pong')})这种方法有几个缺点:您需要设置第二个 ipcRenderer.on 监听器来处理渲染器进程中的响应。 使用 invoke,您将获得作为 Promise 返回到原始 API 调用的响应值。没有显而易见的方法可以将 asynchronous-reply 消息与原始的 asynchronous-message 消息配对。 如果您通过这些通道非常频繁地来回传递消息,则需要添加其他应用代码来单独跟踪每个调用和响应。使用 ipcRenderer.sendSyncipcRenderer.sendSync API 向主进程发送消息,并 同步 等待响应。main.js (Main Process)const { ipcMain } = require('electron')ipcMain.on('synchronous-message', (event, arg) => { console.log(arg) // 在 Node 控制台中打印“ping” event.returnValue = 'pong'})preload.js (Preload Script)// 您也可以使用 `contextBridge` API// 将这段代码暴露给渲染器进程const { ipcRenderer } = require('electron')const result = ipcRenderer.sendSync('synchronous-message', 'ping')console.log(result) // 在 DevTools 控制台中打印“pong”这份代码的结构与 invoke 模型非常相似,但出于性能原因,我们建议避免使用此 API。 它的同步特性意味着它将阻塞渲染器进程,直到收到回复为止。模式 3:主进程到渲染器进程将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息。 消息需要通过其 WebContents 实例发送到渲染器进程。 此 WebContents 实例包含一个 send 方法,其使用方式与 ipcRenderer.send 相同。为了演示此模式,我们将构建一个由原生操作系统菜单控制的数字计数器。对于此演示,您需要将代码添加到主进程、渲染器进程和预加载脚本。 完整代码如下,我们将在后续章节中对每个文件进行单独解释。docs/fiddles/ipc/pattern-3 (26.1.0)main.jspreload.jsindex.htmlrenderer.jsconst { app, BrowserWindow, Menu, ipcMain } = require('electron')const path = require('path')function createWindow () { const mainWindow = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, 'preload.js') } }) const menu = Menu.buildFromTemplate([ { label: app.name, submenu: [ { click: () => mainWindow.webContents.send('update-counter', 1), label: 'Increment' }, { click: () => mainWindow.webContents.send('update-counter', -1), label: 'Decrement' } ] } ]) Menu.setApplicationMenu(menu) mainWindow.loadFile('index.html') // Open the DevTools. mainWindow.webContents.openDevTools()}app.whenReady().then(() => { ipcMain.on('counter-value', (_event, value) => { console.log(value) // will print value to Node console }) createWindow() app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow() })})app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit()})1. 使用 webContents 模块发送消息对于此演示,我们需要首先使用 Electron 的 Menu 模块在主进程中构建一个自定义菜单,该模块使用 webContents.send API 将 IPC 消息从主进程发送到目标渲染器。main.js (Main Process)const { app, BrowserWindow, Menu, ipcMain } = require('electron')const path = require('path')function createWindow () { const mainWindow = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, 'preload.js') } }) const menu = Menu.buildFromTemplate([ { label: app.name, submenu: [ { click: () => mainWindow.webContents.send('update-counter', 1), label: 'Increment' }, { click: () => mainWindow.webContents.send('update-counter', -1), label: 'Decrement' } ] } ]) Menu.setApplicationMenu(menu) mainWindow.loadFile('index.html')}// ...出于本教程的目的,请务必注意, click 处理函数通过 update-counter 通道向渲染器进程发送消息(1 或 -1)。click: () => mainWindow.webContents.send('update-counter', -1)info请确保您为以下步骤加载了 index.html 和 preload.js 入口点!2. 通过预加载脚本暴露 ipcRenderer.on与前面的渲染器到主进程的示例一样,我们使用预加载脚本中的 contextBridge 和 ipcRenderer 模块向渲染器进程暴露 IPC 功能:preload.js (Preload Script)const { contextBridge, ipcRenderer } = require('electron')contextBridge.exposeInMainWorld('electronAPI', { onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)})加载预加载脚本后,渲染器进程应有权访问 window.electronAPI.onUpdateCounter() 监听器函数。安全警告出于 安全原因,我们不会直接暴露整个 ipcRenderer.on API。 确保尽可能限制渲染器对 Electron API 的访问。info在这个最小示例中,您可以直接在预加载脚本中调用 ipcRenderer.on ,而不是通过 context bridge 暴露它。preload.js (Preload Script)const { ipcRenderer } = require('electron')window.addEventListener('DOMContentLoaded', () => { const counter = document.getElementById('counter') ipcRenderer.on('update-counter', (_event, value) => { const oldValue = Number(counter.innerText) const newValue = oldValue + value counter.innerText = newValue })})但是,与通过 context bridge 暴露预加载 API 相比,此方法的灵活性有限,因为监听器无法直接与渲染器代码交互。3. 构建渲染器进程 UI为了将它们联系在一起,我们将在加载的 HTML 文件中创建一个接口,其中包含一个 #counter 元素,我们将使用该元素来显示值:index.html<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"> <title>Menu Counter</title> </head> <body> Current value: <strong id="counter">0</strong> <script src="./renderer.js"></script> </body></html>最后,为了更新 HTML 文档中的值,我们将添加几行 DOM 操作的代码,以便在每次触发 update-counter 事件时更新 #counter 元素的值。renderer.js (Renderer Process)const counter = document.getElementById('counter')window.electronAPI.onUpdateCounter((_event, value) => { const oldValue = Number(counter.innerText) const newValue = oldValue + value counter.innerText = newValue})在上面的代码中,我们将回调传递给从预加载脚本中暴露的 window.electronAPI.onUpdateCounter 函数。 第二个 value 参数对应于我们传入 webContents.send 函数的 1 或 -1,该函数是从原生菜单调用的。可选:返回一个回复对于从主进程到渲染器进程的 IPC,没有与 ipcRenderer.invoke 等效的 API。 不过,您可以从 ipcRenderer.on 回调中将回复发送回主进程。我们可以对前面例子的代码进行略微修改来演示这一点。 在渲染器进程中,使用 event 参数,通过 counter-value 通道将回复发送回主进程。renderer.js (Renderer Process)const counter = document.getElementById('counter')window.electronAPI.onUpdateCounter((event, value) => { const oldValue = Number(counter.innerText) const newValue = oldValue + value counter.innerText = newValue event.sender.send('counter-value', newValue)})在主进程中,监听 counter-value 事件并适当地处理它们。main.js (Main Process)// ...ipcMain.on('counter-value', (_event, value) => { console.log(value) // will print value to Node console})// ...模式 4:渲染器进程到渲染器进程没有直接的方法可以使用 ipcMain 和 ipcRenderer 模块在 Electron 中的渲染器进程之间发送消息。 为此,您有两种选择:将主进程作为渲染器之间的消息代理。 这需要将消息从一个渲染器发送到主进程,然后主进程将消息转发到另一个渲染器。从主进程将一个 MessagePort 传递到两个渲染器。 这将允许在初始设置后渲染器之间直接进行通信。对象序列化Electron 的 IPC 实现使用 HTML 标准的 结构化克隆算法 来序列化进程之间传递的对象,这意味着只有某些类型的对象可以通过 IPC 通道传递。特别是 DOM 对象(例如 Element,Location 和 DOMMatrix),Node.js 中由 C++ 类支持的对象(例如 process.env,Stream 的一些成员)和 Electron 中由 C++ 类支持的对象(例如 WebContents、BrowserWindow 和 WebFrame)无法使用结构化克隆序列化。
-
上下文隔离是什么?上下文隔离功能将确保您的 预加载脚本 和 Electron的内部逻辑 运行在所加载的 webcontent网页 之外的另一个独立的上下文环境里。 这对安全性很重要,因为它有助于阻止网站访问 Electron 的内部组件 和 您的预加载脚本可访问的高等级权限的API 。这意味着,实际上,您的预加载脚本访问的 window 对象并不是网站所能访问的对象。 例如,如果您在预加载脚本中设置 window.hello = 'wave' 并且启用了上下文隔离,当网站尝试访问window.hello对象时将返回 undefined。自 Electron 12 以来,默认情况下已启用上下文隔离,并且它是 所有应用程序推荐的安全设置。迁移没有上下文隔离,从预加载脚本提供API时,经常会使用window.X = apiObject 那么现在呢?之前: 上下文隔离禁用在渲染进程中,预加载脚本暴露给已加载的页面 API 是一个常见的使用方式。 当上下文隔离时,您的预加载脚本可能会暴露一个常见的全局window对象给渲染进程。 此后,您可以从中添加任意的属性到预加载在脚本。preload.js// 上下文隔离禁用的情况下使用预加载window.myAPI = { doAThing: () => {}}doAThing() 函数可以在渲染进程中直接使用。renderer.js// 在渲染器进程使用导出的 APIwindow.myAPI.doAThing()之后:启用上下文隔离Electron 提供一种专门的模块来无阻地帮助您完成这项工作。 contextBridge 模块可以用来安全地从独立运行、上下文隔离的预加载脚本中暴露 API 给正在运行的渲染进程。 API 还可以像以前一样,从 window.myAPI 网站上访问。preload.js// 在上下文隔离启用的情况下使用预加载const { contextBridge } = require('electron')contextBridge.exposeInMainWorld('myAPI', { doAThing: () => {}})renderer.js// 在渲染器进程使用导出的 APIwindow.myAPI.doAThing()请阅读 contextBridge 的文档,以全面了解其限制。 例如,您不能在 contextBridge 中暴露原型或者 Symbol。安全事项单单开启和使用 contextIsolation 并不直接意味着您所做的一切都是安全的。 例如,此代码是 不安全的。preload.js// ❌ 错误使用contextBridge.exposeInMainWorld('myAPI', { send: ipcRenderer.send})它直接暴露了一个没有任何参数过滤的高等级权限 API 。 这将允许任何网站发送任意的 IPC 消息,这不会是你希望发生的。 相反,暴露进程间通信相关 API 的正确方法是为每一种通信消息提供一种实现方法。preload.js// ✅ 正确使用contextBridge.exposeInMainWorld('myAPI', { loadPreferences: () => ipcRenderer.invoke('load-prefs')})与Typescript一同使用如果您正在使用 TypeScript 构建 Electron 应用程序,您需要给通过 context bridge 暴露的 API 添加类型。 渲染进程的 window 对象将不会包含正确扩展类型,除非给其添加了 类型声明。例如,在这个 preload.ts 脚本中:preload.tscontextBridge.exposeInMainWorld('electronAPI', { loadPreferences: () => ipcRenderer.invoke('load-prefs')})您可以创建一个 renderer.d.ts 类型声明文件,并且全局增强 Window 接口。renderer.d.tsexport interface IElectronAPI { loadPreferences: () => Promise<void>,}declare global { interface Window { electronAPI: IElectronAPI }}以上所做皆是为了确保在您编写渲染进程的脚本时, TypeScript 编译器将会知晓electronAPI合适地在您的全局window对象中renderer.tswindow.electronAPI.loadPreferences()
-
Electron 继承了来自 Chromium 的多进程架构,这使得此框架在架构上非常相似于一个现代的网页浏览器。 本指南将对 教程 中应用的概念进行拓展。为什么不是一个单一的进程?网页浏览器是个极其复杂的应用程序。 除了显示网页内容的主要能力之外,他们还有许多次要的职责,例如:管理众多窗口 ( 或 标签页 ) 和加载第三方扩展。在早期,浏览器通常使用单个进程来处理所有这些功能。 虽然这种模式意味着您打开每个标签页的开销较少,但也同时意味着一个网站的崩溃或无响应会影响到整个浏览器。多进程模型为了解决这个问题,Chrome 团队决定让每个标签页在自己的进程中渲染, 从而限制了一个网页上的有误或恶意代码可能导致的对整个应用程序造成的伤害。 然后用单个浏览器进程控制这些标签页进程,以及整个应用程序的生命周期。 下方来自 Chrome 漫画 的图表可视化了此模型:Electron 应用程序的结构非常相似。 作为应用开发者,你将控制两种类型的进程:主进程 和 渲染器进程。 这类似于上文所述的 Chrome 的浏览器和渲染器进程。主进程每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。窗口管理主进程的主要目的是使用 BrowserWindow 模块创建和管理应用程序窗口。BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 您可从主进程用 window 的 webContent 对象与网页内容进行交互。main.jsconst { BrowserWindow } = require('electron')const win = new BrowserWindow({ width: 800, height: 1500 })win.loadURL('https://github.com')const contents = win.webContentsconsole.log(contents)注意:渲染器进程也是为 web embeds 而被创建的,例如 BrowserView 模块。 嵌入式网页内容也可访问 webContents 对象。由于 BrowserWindow 模块是一个 EventEmitter, 所以您也可以为各种用户事件 ( 例如,最小化 或 最大化您的窗口 ) 添加处理程序。当一个 BrowserWindow 实例被销毁时,与其相应的渲染器进程也会被终止。应用程序生命周期主进程还能通过 Electron 的 app 模块来控制您应用程序的生命周期。 该模块提供了一整套的事件和方法,可以让您用来添加自定义的应用程序行为 (例如:以编程方式退出您的应用程序、修改应用程序坞,或显示一个关于面板) 。这是一个实际的例子,这个app来源于快速入门指南,用 app API 创建了一个更原生的应用程序窗口体验。main.js// quitting the app when no windows are open on non-macOS platformsapp.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit()})原生 API为了使 Electron 的功能不仅仅限于对网页内容的封装,主进程也添加了自定义的 API 来与用户的作业系统进行交互。 Electron 有着多种控制原生桌面功能的模块,例如菜单、对话框以及托盘图标。关于 Electron 主进程模块的完整列表,请参阅我们的 API 文档。渲染器进程每个 Electron 应用都会为每个打开的 BrowserWindow ( 与每个网页嵌入 ) 生成一个单独的渲染器进程。 洽如其名,渲染器负责 渲染 网页内容。 所以实际上,运行于渲染器进程中的代码是须遵照网页标准的 (至少就目前使用的 Chromium 而言是如此) 。因此,一个浏览器窗口中的所有的用户界面和应用功能,都应与您在网页开发上使用相同的工具和规范来进行攥写。虽然解释每一个网页规范超出了本指南的范围,但您最起码要知道的是:以一个 HTML 文件作为渲染器进程的入口点。使用层叠样式表 (Cascading Style Sheets, CSS) 对 UI 添加样式。通过 <script> 元素可添加可执行的 JavaScript 代码。此外,这也意味着渲染器无权直接访问 require 或其他 Node.js API。 为了在渲染器中直接包含 NPM 模块,您必须使用与在 web 开发时相同的打包工具 (例如 webpack 或 parcel):::警告为了方便开发,可以用完整的 Node.js 环境生成渲染器进程。 在历史上,这是默认的,但由于安全原因,这一功能已被禁用。:::此刻,您或许会好奇:既然这些特性只能由主进程访问,那渲染器进程用户界面怎样才能与 Node.js 和 Electron 的原生桌面功能进行交互。 而事实上,确实没有直接导入 Electron 內容脚本的方法。Preload 脚本预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。 这些脚本虽运行于渲染器的环境中,却因能访问 Node.js API 而拥有了更多的权限。预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程。main.jsconst { BrowserWindow } = require('electron')// ...const win = new BrowserWindow({ webPreferences: { preload: 'path/to/preload.js' }})// ...因为预加载脚本与浏览器共享同一个全局 Window 接口,并且可以访问 Node.js API,所以它通过在全局 window 中暴露任意 API 来增强渲染器,以便你的网页内容使用。虽然预加载脚本与其所附着的渲染器在共享着一个全局 window 对象,但您并不能从中直接附加任何变动到 window 之上,因为 contextIsolation 是默认的。preload.jswindow.myAPI = { desktop: true}renderer.jsconsole.log(window.myAPI)// => undefined语境隔离(Context Isolation)意味着预加载脚本与渲染器的主要运行环境是隔离开来的,以避免泄漏任何具特权的 API 到您的网页内容代码中。取而代之,我们將使用 contextBridge 模块来安全地实现交互:preload.jsconst { contextBridge } = require('electron')contextBridge.exposeInMainWorld('myAPI', { desktop: true})renderer.jsconsole.log(window.myAPI)// => { desktop: true }此功能对两个主要目的來說非常有用:通过暴露 ipcRenderer 帮手模块于渲染器中,您可以使用 进程间通讯 ( inter-process communication, IPC ) 来从渲染器触发主进程任务 ( 反之亦然 ) 。如果您正在为远程 URL 上托管的现有 web 应用开发 Electron 封裝,则您可在渲染器的 window 全局变量上添加自定义的属性,好在 web 客户端用上仅适用于桌面应用的设计逻辑 。效率进程每个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。
-
教程目录这是 Electron 教程的 第六部分基本要求 创建您的第一个应用程序 使用预加载脚本 添加功能 打包您的应用程序 发布和更新学习目标请注意,这是本教程的最后一步! 在这一部分,你将发布应用到 GitHub 版本中心,并将自动更新功能整合到应用代码中。 使用 update.electronjs.orgElectron 官方在 cid:link_1 上为开源应用程序提供了免费的自动更新服务。 使用它有以下几点要求:你的应用在 macOS 或 Windows 上运行 你的应用有一个公开的 GitHub 仓库 应用程序需要发布到 GitHub releases 中 应用程序需要完成 签名至此,我们假设你已将所有代码推送到公开的 GitHub 仓库。 替代更新服务如果您使用了其他存储库主机 (例如 GitLab 或 Bitbucket),或者如果您你的代码仓库不能公开,请参阅 更新应用程序 教程以托管您自己的 Electron 更新服务器。 发布一个 GitHub 版本Electron Forge 的 Publisher 插件可以自动将打包的应用程序分发到各种来源。 在本教程中,我们将演示使用 GitHub Publisher 将代码发布到 GitHub releases 中。 生成个人访问令牌未经许可,Forge无法向GitHub 上的任何仓库发布。 你需要通过一个认证令牌,授权 Forge 使用 GitHub 的发布功能。 最简单的方法是 创建一个新的个人访问令牌 (PAT) 范围为 public_repo, 它将给你的公共资源库提供写访问权限。 请确保此令牌不外泄。 设置 GitHub 发布者 安装模块Forge 的 GitHub Publisher 是一个插件, 它需要被安装到你的项目的 devDependencies 里面去:npm Yarnnpm install --save-dev @electron-forge/publisher-github在 Forge 中配置发布者一旦安装完毕,你需要在 Forge 配置中设置它。 完整的选项列表在 Forge 的 PublisherGitHubConfig API 文档中。 forge.config.jsmodule.exports = { publishers: [ { name: '@electron-forge/publisher-github', config: { repository: { owner: 'github-user-name', name: 'github-repo-name' }, prerelease: false, draft: true } } ] }发布前的草案版本请注意,你已配置 Forge 来发布你的项目作为草稿版本。 这将使您能够看到其生成的结果而无需实际发布给你的最终用户。 当你写下版本说明和并多次检查分发的工作内容后, 就可以手动在 Github 上发布你的应用. 设置身份验证 token你还需要让发布器了解你的身份验证令牌。 默认情况下,它将使用存储在 GITHUB_TOKEN 环境变量的值。 运行发布命令将 Forge 的 发布命令 添加到 npm 脚本内。 package.json//... "scripts": { "start": "electron-forge start", "package": "electron-forge package", "make": "electron-forge make", "publish": "electron-forge publish" }, //...此命令将运行你配置的创建方法并将输出的可分发文件发布到新的 GitHub 版本。npm Yarnnpm run publish默认情况下,这只会为你当前的主机操作系统架构发布一个单一的可分发文件。 你可以通过将 --arch 参数传递给 Forge 命令来发布不同的架构。版本的名称将对应于项目的 package.json 文件中的 version 字段。 发布时打上标签可选的, 你也可以 在Git上对版本打标签, 这样你的版本将关联到一个代码历史中的标签点. npm附带了一个方便的 npm version 命令,可以为你处理版本碰撞或打标签. 小技巧:在 GitHub Actions 中发布本地发布的版本可能很单一,因为只能为你的主机操作系统创建一种版本 (比如, 你不能从 macOS 系统上发布 Window 系统的 .exe 文件)。一种解决方案是通过自动化工作流来发布你的应用比如 GitHub Actions, 通过它可以在各种云系统内包括 Ubuntu, macOS 和 Windows 上运行任务. 这是 Electron Fiddle 采用的精确方法. 你可以参考 Fiddle 的 构建和发布一条龙 以及 Forge 配置 了解更多详情。 检测更新程序代码至此我们有了一个通过 GitHub 发布的功能性发布系统, 我们现在需要告诉我们的 Electron 应用来下载更新,而且是每当新版本出现时都需要更新。 Electron 应用通过 autoUpdater 模块来实现此功能, 此模块可以从更新服务源中读取信息, 并检查是否有一个新版本可供下载.网址是 update.electronjs.org 的服务器提供了一个兼容更新源. 比如, Electron Fiddle v0.28.0 将会检查接口 cid:link_0 来查看是否有新的 GitHub 版本可用.一旦你的发布版本被推送到 GitHub, 对应的更新服务 update.electronjs.org 将会自动对接你的应用. 剩下的唯一步骤是使用 autoUpdater 模块配置源。为了让整个过程更加简单, Electron 团队维护 update-electron-app 模块, 它在一次函数调用中为 update.electronjs.org 设置了 autoUpdater 样板,无需配置。 这个模块将搜索 update.electronjs.org 源中与项目内 package.json 的"repository" 字段匹配的部分。首先,安装模块作为运行时的依赖项npm Yarnnpm install update-electron-app然后导入模块并在主进程中立即调用 main.jsrequire('update-electron-app')()这是需要做的事情! 一旦你的应用程序被打包,它将在你发布每个新的 GitHub 版本时更新自己。 摘要在这个教程中,我们配置 Electron Forge 的 GitHub Publisher 来上传你的应用的 发行版到 GitHub releases。 由于可分发文件不能总是在不同的平台之间生成 构建和发布流程,如果你不能访问机器,我们建议将你的构建和发布流程设置在持续集成管道中。Electron 应用程序可以通过将 autoUpdater 模块指向一个更新服务器来进行自我更新。 update.electronjs.org 是一个 Electron 为开源应用程序 免费提供的更新服务器,发布在 GitHub Releases 上。 配置你的Electron应用来使用这个服务是很容易的,就像 安装并导入 update-electron-app 模块一样。如果你的应用程序不适合使用 update.electronijs.org,你应该自己部署 一个更新服务器,并配置自己的自动 autoUpdater 模块。 🌟 你完成了从这里开始,您已经正式完成了我们的 Electron 教程。 请自由的探索我们的剩余文档并快乐的开发! 如果您有疑问,请前往我们的社区 Discord 服务器 。
-
教程目录这是 Electron 教程的第三部分。基本要求 创建您的第一个应用程序 使用预加载脚本 添加功能 打包您的应用程序 发布和更新学习目标在这部分的教程中,你将会了解什么是预加载脚本,并且学会如何使用预加载脚本来安全地将特权 API 暴露至渲染进程中。 不仅如此,你还会学到如何使用 Electron 的进程间通信 (IPC) 模组来让主进程与渲染进程间进行通信。 什么是预加载脚本?Electron 的主进程是一个拥有着完全操作系统访问权限的 Node.js 环境。 除了 Electron 模组 之外,您也可以访问 Node.js 内置模块 和所有通过 npm 安装的包。 另一方面,出于安全原因,渲染进程默认跑在网页页面上,而并非 Node.js里。为了将 Electron 的不同类型的进程桥接在一起,我们需要使用被称为 预加载 的特殊脚本。 使用预加载脚本来增强渲染器BrowserWindow 的预加载脚本运行在具有 HTML DOM 和 Node.js、Electron API 的有限子集访问权限的环境中。::: info 预加载脚本沙盒化从 Electron 20 开始,预加载脚本默认 沙盒化 ,不再拥有完整 Node.js 环境的访问权。 实际上,这意味着你只拥有一个 polyfilled 的 require 函数,这个函数只能访问一组有限的 API。 可用的 API 详细信息 Electron 模块 渲染进程模块 Node.js 模块 events、timers、url Polyfilled 的全局模块 Buffer、process、clearImmediate、setImmediate有关详细信息,请阅读 沙盒进程 教程。:::与 Chrome 扩展的内容脚本(Content Script)类似,预加载脚本在渲染器加载网页之前注入。 如果你想为渲染器添加需要特殊权限的功能,可以通过 contextBridge 接口定义 全局对象。为了演示这一概念,你将会创建一个将应用中的 Chrome、Node、Electron 版本号暴露至渲染器的预加载脚本新建一个 preload.js 文件。该脚本通过 versions 这一全局变量,将 Electron 的 process.versions 对象暴露给渲染器。 preload.jsconst { contextBridge } = require('electron')contextBridge.exposeInMainWorld('versions', { node: () => process.versions.node, chrome: () => process.versions.chrome, electron: () => process.versions.electron // 除函数之外,我们也可以暴露变量 })为了将脚本附在渲染进程上,在 BrowserWindow 构造器中使用 webPreferences.preload 传入脚本的路径。 main.jsconst { app, BrowserWindow } = require('electron') const path = require('path')const createWindow = () => { const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js') } })win.loadFile('index.html') }app.whenReady().then(() => { createWindow() })info这里使用了两个Node.js概念:__dirname 字符串指向当前正在执行的脚本的路径(在本例中,它指向你的项目的根文件夹)。 path.join API 将多个路径联结在一起,创建一个跨平台的路径字符串。现在渲染器能够全局访问 versions 了,让我们快快将里边的信息显示在窗口中。 这个变量不仅可以通过 window.versions 访问,也可以很简单地使用 versions 来访问。 新建一个 renderer.js 脚本, 使用 document.getElementById DOM API 来替换 id 属性为 info 的 HTML 元素的文本。 renderer.jsconst information = document.getElementById('info') information.innerText = 本应用正在使用 Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), 和 Electron (v${versions.electron()})然后请修改你的 index.html 文件。加上一个 id 属性为 info 的全新元素,并且记得加上你的 renderer.js 脚本: index.html来自 Electron 渲染器的问好!来自 Electron 渲染器的问好!👋做完这几步之后,你的应用应该长这样:Electron 应用显示这个应用正在使用 Chrome (v102.0.5005.63)、Node.js (v16.14.2) 和 Electron (v19.0.3)。你的代码应该长这样: docs/fiddles/tutorial-preload (26.1.0)main.js preload.js index.html renderer.jsconst { app, BrowserWindow } = require('electron') const path = require('path')const createWindow = () => { const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js') } })win.loadFile('index.html') }app.whenReady().then(() => { createWindow()app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow() } }) })app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } })在进程之间通信我们之前提到,Electron 的主进程和渲染进程有着清楚的分工并且不可互换。 这代表着无论是从渲染进程直接访问 Node.js 接口,亦或者是从主进程访问 HTML 文档对象模型 (DOM),都是不可能的。解决这一问题的方法是使用进程间通信 (IPC)。可以使用 Electron 的 ipcMain 模块和 ipcRenderer 模块来进行进程间通信。 为了从你的网页向主进程发送消息,你可以使用 ipcMain.handle 设置一个主进程处理程序(handler),然后在预处理脚本中暴露一个被称为 ipcRenderer.invoke 的函数来触发该处理程序(handler)。我们将向渲染器添加一个叫做 ping() 的全局函数来演示这一点。这个函数将返回一个从主进程翻山越岭而来的字符串。首先,在预处理脚本中设置 invoke 调用: preload.jsconst { contextBridge, ipcRenderer } = require('electron')contextBridge.exposeInMainWorld('versions', { node: () => process.versions.node, chrome: () => process.versions.chrome, electron: () => process.versions.electron, ping: () => ipcRenderer.invoke('ping') // 除函数之外,我们也可以暴露变量 })IPC 安全可以注意到我们使用了一个辅助函数来包裹 ipcRenderer.invoke('ping') 调用,而并非直接通过 context bridge 暴露 ipcRenderer 模块。 你永远都不会想要通过预加载直接暴露整个 ipcRenderer 模块。 这将使得你的渲染器能够直接向主进程发送任意的 IPC 信息,会使得其成为恶意代码最强有力的攻击媒介。然后,在主进程中设置你的 handle 监听器。 我们在 HTML 文件加载之前完成了这些,所以才能保证在你从渲染器发送 invoke 调用之前处理程序能够准备就绪。 main.jsconst { app, BrowserWindow, ipcMain } = require('electron') const path = require('path')const createWindow = () => { const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js') } }) win.loadFile('index.html') } app.whenReady().then(() => { ipcMain.handle('ping', () => 'pong') createWindow() })将发送器与接收器设置完成之后,现在你可以将信息通过刚刚定义的 'ping' 通道从渲染器发送至主进程当中。 renderer.jsconst func = async () => { const response = await window.versions.ping() console.log(response) // 打印 'pong' }func()info要了解更详细的关于使用 ipcRenderer 和 ipcMain 模块的详细说明,请查阅完整的 进程间通信 指南。 摘要预加载脚本包含在浏览器窗口加载网页之前运行的代码。 其可访问 DOM 接口和 Node.js 环境,并且经常在其中使用 contextBridge 接口将特权接口暴露给渲染器。由于主进程和渲染进程有着完全不同的分工,Electron 应用通常使用预加载脚本来设置进程间通信 (IPC) 接口以在两种进程之间传输任意信息。在下一部分的教程中,我们将向你展示如何向你的应用中添加更多的功能,之后将向你传授如何向用户分发你的应用。
-
Electron、QT、WPF、MFC都是桌面应用程序开发中常用的框架和编程语言,但它们的应用领域和特点有所不同。Electron:使用HTML、CSS和JavaScript等Web技术进行桌面应用程序开发,适合快速构建跨平台的桌面应用程序,但不属于传统桌面应用程序开发的主流技术。QT:一种跨平台的C++图形用户界面库,可用于开发GUI应用程序,具有丰富的GUI组件和强大的跨平台性,但学的曲线较陡峭(QT很早就有了,但是在中国使用率还不及MFC)WPF:基于.NET Framework的图形用户界面库,可用于开发富客户端应用程序,具有强大的数据绑定和优美的界面设计能力,但性能方面可能不如其他桌面应用程序框架。MFC:微软基础类库(Microsoft Foundation Class Library)的简称,是微软公司提供的一组基于C++的类库,用于开发Windows桌面应用程序,但随着C#和WPF等技术的发展,MFC的应用逐渐减少。综上所述,目前桌面应用程序开发的主流技术是WPF和QT,未来很有可能是Electron,因为现在跨平台成为主流。前端人才众多,由于JS具有良好的跨平台性,一家公司如果开发了网页,想在桌面端和网页端运行,使用Electron是再简单不过了。而其它两种方式QT和WPF,还需要内嵌一个浏览器组件,升级也不如Electron友好。而MFC已经逐渐被淘汰,如果你想要从事桌面应用程序开发,直接学Electron
-
Electron和MFC内嵌Chrome内核都是用于构建跨平台桌面应用程序的技术,但它们在实现方式、性能和资源占用等方面存在一些差异。Electron的优点:跨平台性:Electron使用Web技术(HTML、CSS和JavaScript)编写桌面应用程序,因此可以在Windows、Mac和Linux等操作系统上运行。快速开发:由于Electron使用Web技术,开发者可以快速地构建和调试应用程序,而无需重新学习大量的桌面开发技术。丰富的用户界面:Electron应用程序可以使用丰富的Web技术构建用户界面,包括HTML、CSS和JavaScript等。社区支持:Electron拥有庞大的社区支持,有大量的开源框架和库可供选择。Electron的缺点:性能问题:Electron应用程序的性能通常比原生应用程序慢,因为它们需要使用JavaScript引擎来解析和执行代码。资源占用:Electron应用程序通常会占用更多的系统资源,因为它们需要运行一个完整的Web浏览器引擎。学习曲线:对于初学者来说,学Electron可能需要一些时间,因为它涉及到Web技术的知识。MFC内嵌Chrome内核的优点:原生性能:MFC内嵌Chrome内核可以充分利用Windows操作系统的原生性能,从而提供更快的程序执行速度。控件丰富:MFC提供了丰富的控件库,可以满足各种桌面应用程序的需求。稳定性高:由于MFC是微软官方提供的开发工具包,因此其稳定性较高。MFC内嵌Chrome内核的缺点:学习曲线:MFC的学习曲线相对较高,需要掌握C++和Windows编程知识。开发难度大:使用MFC进行开发可能需要更多的时间和精力,尤其是在构建复杂的用户界面时。社区支持:虽然MFC是微软官方提供的开发工具包,但其社区支持相对较少。就目前而言,还是建议使用Electron的。我公司的项目,之前就是用MFC+Chrome原生内核写的,但是到了后期升级维护非常麻烦,每次想要更新chrome内核,基本都要花2周时间痛苦升级,而且升级完还不知道会不会有问题,还要仔细测试。而用了electron直接就可以通过改配置一键升级,速度非常之快,耦合也低,安全性比起MFC高了不少
-
Electron Forge 是一个用于构建 Electron 应用程序的现代开发工具。它提供了一系列的工具和插件,可以帮助你快速打包和发布 Electron 应用程序。要打包 Electron Forge 应用程序,可以按照以下步骤进行操作:确保你已经在项目中安装了 Electron Forge。你可以使用以下命令来安装它:npm install -g electron-forge在项目的根目录下打开终端,并运行以下命令来初始化 Forge:forge init根据提示选择要使用的打包配置。Forge 提供了多种打包配置选项,包括用于 Windows、macOS 和 Linux 的选项。运行以下命令来构建应用程序(Electron-forge 会创建 out 文件夹,您的软件包将在那里找到):npm run make这将使用所选的打包配置构建应用程序。构建过程将在项目的 build 文件夹中生成应用程序的可执行文件。 (可选)如果你想要对构建的应用程序进行签名或加密,可以使用 Forge 的签名和加密工具。有关详细信息,请参阅 Electron Forge 的文档。// Example for macOS out/ ├── out/make/zip/darwin/x64/my-electron-app-darwin-x64-1.0.0.zip ├── ... └── out/my-electron-app-darwin-x64/my-electron-app.app/Contents/MacOS/my-electron-app将构建的应用程序部署到你选择的分发渠道(如 Windows 应用商店、macOS App Store 或自行分发)。这些步骤可以帮助你使用 Electron Forge 打包 Electron 应用程序。请注意,具体的步骤可能会因你的项目配置和需求而有所不同。建议查阅 Electron Forge 的官方文档以获取更详细的说明和指导。
-
要使用 electron-builder 打包程序,你需要遵循以下步骤:安装 Node.js 和 npm(Node.js 的包管理器)。使用 npm 安装 electron-builder。在终端中输入以下命令:npm install electron-builder --save-dev在项目的根目录下创建一个名为 build 的文件夹,并在其中添加一个名为 builder.js 的文件。这个文件用于配置 electron-builder。以下是一个简单的示例:const builder = require('electron-builder'); builder .build({ targets: builder.Platform.WINDOWS.createTarget(), config: { appId: 'com.example.app', productName: 'Your App Name', directories: { output: 'dist', // 输出目录,默认为 dist }, }, }) .then(() => { console.log('Build completed successfully.'); }) .catch((error) => { console.error('Error occurred during build:', error); });在上面的示例中,我们配置了应用程序的 ID、名称以及输出目录。你还可以根据需要添加其他配置项。有关更多详细信息,请参阅 electron-builder 的文档。在 package.json 文件中添加一个 build 脚本,以便可以通过运行 npm run build 来执行 electron-builder:{ "name": "your-app", "version": "1.0.0", "description": "Your app description", "scripts": { "start": "electron .", "build": "electron-builder --dir", "package": "electron-builder", "dist": "electron-builder" }, "build": { "appId": "com.example.app", "productName": "Your App Name", "directories": { "output": "dist" } }, "devDependencies": { "electron": "^11.2.3", "electron-builder": "^22.7.0" } }在上面的示例中,我们添加了 build 脚本并将 electron-builder 的相关配置放在了 build 字段中。这些配置与我们在 builder.js 文件中指定的配置相同。我们还添加了一个名为 dist 的脚本,以便可以通过运行 npm run dist 来构建并打包应用程序。
推荐直播
-
华为云码道Agent集成与鸿蒙实战2026/08/11 周二 19:00-21:00
王一男-华为云码道产品规划专家;李炎-华为云码道产品专家;彭江敏-华为云鸿蒙端云一体化开发专家
本次直播带你解读华为云码道7月份产品新特性、新功能。更有专家演示码道Agent Space × 钉钉机器集成实战,从0到1打通消息通道;码道鸿蒙端云一体化实战,快速搭建员工签到系统。
回顾中 -
华为云开发者AI素养直播课·第三期2026/08/21 周五 16:00-18:00
林华鼎-华为云AI开发者运营负责人;念擎-华为云AI开发者运营案例开发专家
本期直播内容:AI六层能力首次详细解读 + 新一代华为云开发者空间亮相 + 校园案例直播带练
回顾中
热门标签