桌面端 → Electron碎碎念

前置知识

早期想开发一个桌面GUI应用软件,且希望在不同平台上运行,可选的技术和框架并不多,主要有 wxWidgetsGTKQt;这三种框架都是基于C/C++实现的,导致开发桌面端程序十分困难;

后续又出现了NW.jsElectron,这两个框架都是基于NodeJS和Chromium API来实现的,致使向前端开发打开了开发桌面程序的大门;这两种框架为了弥补NodeJS和前端访问系统API方面的不足,两个框架都对系统API进行了封装,如系统对话框、系统托盘、系统菜单、剪切板等,开发者可以使用JS来访问这些API;

由于NodeJS本身是可以很方便的调用C++的拓展的,而这两种框架又是内置NodeJS,因此这两种框架会涉及到不同的领域,如通过NodeJS的C++拓展来实现对一些音视频编解码和图像处理的需求;可以说Electron可以使用几乎所有的Web前端生态领域和NodeJS生态领域的组件和技术方案;

此外由于Electron是内置了Chromium浏览器的,该浏览器对各个标准支持的都非常好,甚至支持一些尚未通过的标准,因此通过Electron开发的应用不会有兼容性的问题;

  • 当然liao,也是有不少的缺点滴,比如:

    • 打包后的体积较大(每次发包都需要下载同样大小的文件进行升级)
    • 进阶曲线较陡,跨进程通信是开发者必须要深入了解滴,主进程和渲染进程间的通信与交互回调也是相当滴烦人滴,比如为啥回调没生效、为啥报了一堆错。。。。。。
    • 安全性问题:有些模块和API在官方中是被关闭的,要是单独使用需要打开,但也因此带来了不可控的风险,因此权衡利弊是需要慎重考虑的
    • 资源消耗较大:正是由于基于Chromium浏览器,带来了资源占用过大的问题
  • Electron与NW的区别点在于

    • Electron区分了主进程和渲染进程;主进程负责创建、管理渲染进程以及控制整个应用的生命周期,渲染进程负责显示界面及控制与用户的交互逻辑。
    • 两个底层使用的整合技术截然不同:
      • NW通过修改源码合并了NodeJS和Chromium的事件循环机制,但因此也导致了NodeJS和Chromium的耦合性过高;
      • Electron则是通过各操作系统的消息循环打通了NodeJS和Chromium的事件循环机制(新版本的Electron是通过一个独立的线程完成这项工作的),但因此也间接的创造出了主进程和渲染进程的概念

初识Electron

Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。
业内优秀的Electron应用有:VS code、飞书客户端、抖音客户端、B站客户端、QQ、阿里云盘、迅雷、OpenFin、Brave浏览器、Postman、Atom、WhatsApp等;
Electron继承了来自Chromium的多进程架构,使得Electron更像一个现代版的网页浏览器;和现代浏览器类似,为了解决不同标签页间的崩溃相互影响,引入了单标签单进程然后浏览器统一管理的方式,Electron中的主进程(在node环境中运行)和渲染器进程就是异曲同工之妙;

Electron的潜力

Electron还被用于Web界面测试。自PhantomJS宣布停止更新后,Electron成了有力的替代者。测试工程师可以通过编写自动化测试脚本,轻松地控制Electron访问网页元素、提交用户输入、验证界面表现、跟踪执行效率等;

此外,Electron还具有自定义代理、截获网络请求、注入脚本到目标网站的能力,它也成了众多极客的趁手工具,比如有开发者开发过一个音乐聚合软件,把QQ音乐、网易云音乐、虾米音乐聚合在一个软件里播放;甚至还可以实现一键将所写文章发布到不同的自媒体平台;

  • Electron框架生态
    • Vue:
      • Vue CLI Plugin Electron Builder(👍🏻)、electron-vue
    • React:
      • electron-react-boilerplate
    • Angular:
      • angular-electron
    • 传统技术:
      • electron-webpack、纯Electron实现
    • 项目推荐
      • awesome-electron:记录了大量的与Electron有关的项目和组件

深入分析内部原理

主进程和渲染进程

  • 主进程
    • 应用启动时会先创建一个主进程,一个应用有且只有一个主进程
    • 只有主进程可以进行GUI的操作,即调用Native API
    • 主要目的:使用BrowserWindow模块创建和管理应用程序的窗口
    • 主要API
      • app:控制应用的事件生命周期
      • BrowserView:创建和控制视图
      • BrowserWindow:创建和控制窗口
        • BrowserWindow类的每个实例创建一个应用程序的窗口,且在单独的渲染进程中加载一个网页,可以在主进程中用Window的webContent对象与网页内容进行交互
      • ipcMain:从主模块到渲染模块(ipcRenderer)的异步通信
      • Menu:创建远程应用及上下文菜单
      • MenuItem:在菜单中添加菜单项
      • net:发起HTTP或HTTPS请求
      • Notification:创建桌面通知
      • screen:检索有关屏幕大小、显示器、光标位置等的信息
      • session:管理浏览器会话、cookie、缓存、代理设置等
      • systemPreferences:获取系统配置信息
      • TouchBar:(MAC专用)配置 TouchBar布局
    • 📢:主进程在调用new BrowserWindow()开启一个系统窗口时,不会开启一个独立的进程,这个窗口是由主进程进行管理的;窗口可以loadURL加载一个页面,此时会开启这个页面的渲染进程(electron Helper名称)
  • 渲染进程
    • 一个应用可以有多个渲染进程,windows中展示的界面通过渲染进程进行表现,因此渲染进程主要负责渲染网页内容,遵从网页开发标准,无权直接访问require或其他node API;

      • 当然也可以通过Preload预加载脚本来实现运行在渲染器环境中但是可以访问node API等更多权限的功能,但是需要注意的是虽然预加载脚本和渲染器环境共享「Window」接口,但是不可以直接在预加载脚本上附加任何变动到Window上,因为contextIsolation: 上下文隔离(语境隔离(Context Isolation)意味着预加载脚本与渲染器的主要运行环境是隔离开来的,以避免泄漏任何具特权的 API 到您的网页内容代码中。)是默认的,可以通过contextBirdge模块来实现相关逻辑交互
      js 复制代码
      const { BrowserWindow } = require('electron')
      // ...
      const win = new BrowserWindow({
        webPreferences: {
          preload: 'path/to/preload.js'
        }
      })
      // ...
      js 复制代码
      const { contextBridge } = require('electron')
      
      contextBridge.exposeInMainWorld('myAPI', {
        desktop: true
      })
      
      //渲染进程
      console.log(window.myAPI)
      // => { desktop: true }
    • 渲染进程要访问GUI操作就需要通过主进程来实现,即要先与主进程进行IPC通信

    • 主要API

      • ipcRenderer:从渲染器进程到主进程的异步通信
      • remote:在渲染进程中使用主进程模块
      • webFrame:自定义渲染当前网页
      • desktopCapturer:通过 [navigator.mediaDevices.getUserMedia] API,可以访问那些用于从桌面上捕获音频和视频的媒体源信息

计算机中的进程和进程间是不共享内存的,一个进程可以创建多个线程,线程间是共享内存的;一个线程等待IO时,另一个线程可以接管CPU,但也因此带来了开发者无法准确的知道某个时刻究竟是哪个线程在执行工作,因此就需要有诸如线程锁或信号量这样的同步技术来实现

js 复制代码
{
    ......
    "scripts": {
        "start": "electron ./index.js"
     },
     ......
}

执行yarn start是让Electron的可执行程序执行index.js中的逻辑;index.js的代码逻辑运行在Electron的主进程中,主进程负责创建窗口并加载index.html,而index.html中写的代码将运行在Electron的渲染进程中;

在Electron中GUI相关的模块仅在主进程中可用,如果想在渲染进程中进行相关操作则需要渲染进程和主进程进行通信从而实现相关逻辑的实现;

主进程和渲染进程之间是通过IPC消息管道进行通信的,当然也有其他的实现方式,一般渲染进程间的通信是通过主进程进行中转实现的;

进程访问

渲染进程可以通过Electron提供的remote对象进行与主进程进行通信,remote对象的属性和方法(包括类型的构造函数)都是主进程的属性和方法的映射,这样就可以直接调用main进程的方法,实现不用显示的发送进程间的消息了;

在通过remote访问时,Electron内部会为你构建一个消息,这个消息会从渲染进程传递给主进程,主进程完成相应的操作,得到操作结果,再将操作结果以远程对象的形式返回给渲染进程;

  • 渲染进程访问主进程
    • 渲染进程通过remote访问主进程中自定义的内容、主进程的app、browserWindow等对象和类型,📢:渲染进程上获取到的主进程的某个对象只是一个对象的映射,是一个代理对象,原型链上并不会存在相应的继承关系
js 复制代码
//main.js
let { BrowserWindow } = require('electron')
exports.makeWin = function() {
    let win = new BrowserWindow({
        webPreferences: { nodeIntegration: true }
    });
    return win;
}
js 复制代码
// index.html
let mainModel = remote.require('./mainModel');
let win2 = null;
document.querySelector("#makeNewWindow2").addEventListener('click', () => {
    win2 = mainModel.makeWin();
    win2.loadFile('index.html');
});
  • 渲染进程向主进程发送消息

    • 渲染进程通过Electron内置的ipcRenderer模块向主进程发送消息,

      • 渲染进程发送数据:→ ipcRenderer
        • ipcRenderer.send('hello_main',{name: 'lbxin1'},{age: 18})
      • 主进程接受数据: → ipcMain
      js 复制代码
      let { ipcMain } = require('electron')
      ipcMain.on('hello_main', (event, param1, param2) => {
          console.log(param1);
          console.log(param2);
          console.log(event.sender);
      });
      // event.sender是渲染进程WebContents对象的实例
  • 主进程向渲染进程发送数据

    • 主进程发送数据:→ ipcMain
      • ipcMain.on('hello_render', (event, param1, param2) => {})
    • 渲染进程接受数据:→ ipcRenderer
    js 复制代码
    let { ipcRenderer } = require('electron')
    ipcRenderer.on('hello_render', (event, param1, param2) => {
        console.log(param1);
        console.log(param2);
        console.log(event.sender);
    });
  • 渲染进程间通信

    • 可以依赖主进程进行数据中转

      js 复制代码
      // 发起消息的渲染进程
      ipcRenderer.send('Lbxin', '来自 Lbxin 的消息')
      // 主进程
      ipcMain.on('mti', (ev, data) => {
          // 通过 id 获取到对应的渲染进程,然后消息传递
          BrowserWindow.fromId(mainId).webContents.send('Lbxin', data)
      })
      // 接收消息的渲染进程 
      ipcRenderer.on('Lbxin', (ev, data) => {
              console.info(data)
      })
      • 原理:所有窗口的创建都是由主进程完成的,主进程持有所有窗口的实例,因此可以进行相应的数据中转
      • 主要依赖的是ipcRenderersendTo()方法,但是前提是渲染进程之间互相知道需要通信的渲染进程的WebContents的id
      js 复制代码
      // win1窗口发送消息的代码
      document.querySelector("#sendMsg2").addEventListener('click', _ => {
          ipcRenderer.sendTo(win2.webContents.id, 'msg_render2render', { name: 'param1' }, { name: 'param2'
          })
      });
      // win2窗口接收消息的代码
      ipcRenderer.on('msg_render2render', (event, param1, param2) => {
          console.log(param1);
          console.log(param2);
          console.log(event.sender);
      });
注意点
  • 少用「remote」模块,每次remote都会触发底层的同步ipc事件,影响性能,当使用不当的时候会出现进程卡死状态;
  • 不要使用「sync」模式:ipcRenderer.sync()方法会导致卡死现象的出现
  • 在请求和响应的通信模式下,需要自定义超时限制,需要「response」一个异常的超时事件让业务进行交互

remote模块浅析

electron的remote模块为渲染进程主进程通信封装了一种简单的方法,通过该模块可以'直接'获取主进程对象或者调用主进程对象或对象的方法,而不必显式的发送进程间消息,类似于Java中的RMI;

本质上remote模块是基于Electron的IPC机制的,进程间的通信数据必须是可序列化的,例如JSON序列化;而Electron内部是通过使用MetaData(元数据)来描述这些对象外形的协议;

优缺点分析
  • 缺点
    • 真的很慢
      • 通过remote模块可以访问主进程的对象、类型、方法等,但是这些操作是跨进程的,性能损耗相当大
    • 会造成混乱
      • 由于是通过渲染进程触发到主进程中的方法,事件的最先接收到的是主进程,然后才是异步方式的渲染进程,因此会导致难以捕获排查
    • 会制造出假象
      • 当渲染进程使用到了主进程中的某个对象时,实际上只是一个对象的代理,原始对象原型链上的属性是不会映射到代理对象上的,某些像NaN、infinity的值也不会被正确的映射到代理对象上
    • 存在着安全隐患
      • remote底层还是通过IPC管道与主进程通信的,这会使得渲染进程中的某些逻辑获取到访问主进程的权利,从而出现安全漏洞
    • 揭开Electron remote模块的神秘面纱

拓展知识

前端HMR(hot-module-replacement)

之前前端的调试是非常麻烦的,开发者需要通过刷新浏览器来验证是否运行;直到后来出现的live-reload工具的出现,该工具只要检测到代码有改动,就会帮助开发者自动刷新页面;而现在的webpack更进一步,做到了可以不刷新页面即可更新改动后的代码,这就是HMR技术;

npm scripts命令原理

启动脚本运行前会先自动新建一个命令行环境,然后把当前目录下的node_modules/.bin加入系统环境变量中,接着执行scripts配置节指定的脚本的内容,执行完成后再把node_modules/.bin从系统环境变量中删除。所以,当前目录下的node_modules/.bin子目录里面的所有脚本,都可以直接用脚本名调用,不必加上路径。

相关推荐
YBN娜5 分钟前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=5 分钟前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css
minDuck10 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
小政爱学习!30 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。36 分钟前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼42 分钟前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k09331 小时前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang13581 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning1 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人1 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架