Electron的原理

Electron是一个集成项目,允许开发者使用前端技术开发桌面端应用。它做了如下几个重要的工作:

  • 定制 Chromium,并把定制版本的 Chromium 集成在 Electron 内部;
  • 定制 Node.js,并把定制版本的 Node.js 集成在 Electron 内部;
  • 通过消息轮询机制打通 Node.js 和 Chromium 的消息循环;
  • 通过 Electron 的内置模块向开发者提供桌面应用开发必备的 API;

其中 Chromium 基础能力可以让应用渲染 HTML(CSS) 页面,可以执行页面的JavaScript脚本,让应用可以在 Cookie、LocalStorage或 IndexedDB 中存取数据。除此之外,Electron还允许开发者突破同源策略的限制:伪装请求,截获响应,修改session等。

Node.js 基础能力可以让开发者读写本地磁盘的文件、通过 socket 访问网络、创建和控制子进程等。除此之外,还修改了Node的加解密机制让Chromium的BoringSSL和Node的OpenSSL兼容的更好,让Node.js可以加载asar压缩包内的文件等。

Electron 内置模块可以让开发者创建操作系统的托盘图标、访问操作系统的剪切板、屏幕信息、发送系统通知等,除此之外还提供了崩溃报告收集能力、性能问题追踪能力等。

另外,Electron继承了Chromium的多进程架构,也是分一个主进程多个渲染进程的。

Electron 应用启动时,首先会加载主进程的逻辑,主进程会创建一个或多个窗口,我们暂时可以粗浅的认为一个窗口就代表一个渲染进程,主进程负责管理所有的渲染进程。具体的多进程概念请看Chromium的原理。


Chromium的原理

Chromium是一个多进程架构的浏览器。

以前(大约2008年)浏览器大都是单进程、多线程的架构模式实现的,浏览器中任何一个行为不当的网页或插件都可能让整个浏览器崩溃。

Chromium为了解决这个问题,把每个页面约束在单独的进程中,以保护整个浏览器不受单个页面中的故障所影响。这极大地缓解了浏览器容易崩溃的问题。如下图所示:

Chromium把管理页面、管理选项卡和插件的进程称为浏览器进程(Browser Process)。把特定于页面的进程称为渲染进程(Render Process)。

渲染进程使用Blink布局引擎来解释和渲染HTML。渲染进程与浏览器进程通过IPC管道进行通信(详细资料)。

通常每个新窗口或选项卡都会在新进程中打开。浏览器进程(Browser Process)负责创建这些新的进程(Render Process)。一旦渲染进程崩溃或挂起,则浏览器进程控制着界面,提示用户需要重新加载页面,当用户点击重新加载按钮后,浏览器进程则创建一个新的渲染进程来为用户服务。

有的时候创建新窗口或选项卡不会创建新渲染进程。比如开发者使用window.open方法打开新窗口时,就希望这个窗口复用当前的渲染进程,因为两个窗口之间往往需要同步的数据交互。另外还有一些情况需要复用渲染进程,比如打开一个新的渲染进程时,发现系统中已经有一个同样的渲染进程可以复用的情况(详细资料)。

由于渲染进程运行在一个单独的进程中,所有页面脚本都在此进程中运行,当页面脚本尝试访问网络或本地资源时,当前渲染进程会发消息给浏览器进程,由浏览器进程完成相应的工作。此时浏览器进程会判断这些操作是否合法,比如跨越同源策略的请求、突破限制访问Cookie、https页面内嵌http的页面等,这些行为都是不合法的行为,浏览器进程可以拒绝提供服务,这就是浏览器的沙箱模式。

多进程模式还带来了性能上的提升,对于那些不可见的渲染进程,操作系统会在用户可用内存较低时,把它们占用的内存部分或全部交换到磁盘上,以保证用户可见的进程更具响应性。相比之下,单进程浏览器架构将所有页面的数据随机分布在内存中,不可能如此干净地分离使用和未使用的数据,性能表现不佳。

多进程架构模式每个进程都会包含公共基础结构的副本(例如V8引擎的执行环境)、更复杂的通信模型等,这都意味着浏览器会消耗更多的内存、CPU甚至电能。


Node.js的原理

Node.js也是一个集成项目,它允许JavaScript脱离浏览器执行,并提供了一系列的API,供JavaScript访问用户操作系统的资源。

它集成的项目:

  • V8:高性能JavaScript的执行引擎,同时拥有解释执行和编译执行的能力,可以将JavaScript代码编译为底层机器码,Node.js通过V8引擎提供的c++ API使V8引擎解析并执行JavaScript代码,并且通过V8引擎公开的接口和类型把自己内置的C++模块和方法转换为可被JavaScript访问的形式;(Chromium网页中解释执行JS脚本用的也是V8引擎);
  • libuv:高性能、跨平台事件驱动的I/O库,它提供了文件系统、网络、子进程、管道、信号处理、轮询和流的管控机制。它还包括一个线程池,用于某些不易于在操作系统级别完成的异步工作;
  • c-ares:异步DNS解析库。用于支持Node.js的DNS模块;
  • llhttp:一款由TypeScript和C语言编写的轻量级HTTP解析器,内存消耗非常小;
  • OpenSSL:提供了经过严格测试的各种加密解密算法的实现,用于支持Node.js的crypto模块;
  • zlib:提供同步的、异步或流式的压缩和解压缩能力,用于支持Node.js的zlib模块;

JavaScript在Node.js运行环境中的执行流程

  1. 初始化自己的执行环境:在这个阶段Node.js会注册一系列的C++模块以备将来使用。
  2. 创建libuv的消息循环:这个消息循环会伴随着整个应用的生命周期,运行线程退出它才会退出。libuv模块内部持有一个非常复杂的结构体,当用户的代码开始读取文件或发起网络请求时,Node.js就会给这个结构体增加一个回调函数,libuv的消息循环会不断的遍历这个结构体上的回调函数,当读取文件或发起网络请求有数据可用时,就会执行用户的回调函数。
  3. 创建V8引擎的运行环境:这是一个拥有自己的堆栈的隔离环境。
  4. 绑定底层模块:Node.js会使V8引擎执行一个JavaScript脚本(node_bootstrap.js),这是Node.js内置的一个脚本,这个脚本负责绑定Node.js注册的一系列C++模块。
  5. 读取并执行用户脚本的内容:Node.js会把这个文件的内容交给V8引擎运行,并把运行结果返回给用户。