本篇文章分享来自小伙伴「huanxing」的一次学习总结分享,希望跟社区的同学一起探讨。
Electron 是什么
Electron是一个内嵌了 Chromium(注:Chromium其实就是 Google 为发展 Chrome 浏览器而启动的开源项目)和 Node.js,可以使用 JavaScript、HTML 和 CSS 构建跨平台(Windows、MacOs、Linux)的桌面应用程序的框架。
最早在2011年左右,中国英特尔开源技术中心的王文睿(Roger Wang)希望能用Node.js来操作WebKit,而创建了 node-webkit 项目,这就是 NW.js 的前身。当时的目的并不是用这两个技术来开发桌面GUI应用。
中国英特尔开源技术中心大力支持了这个项目,不但允许王文睿分出一部分精力来做这个开源项目,还给了他招聘名额,允许他招聘其他工程师来一起完成这个项目。2012年,故事的另一个主角赵成(Cheng Zhao)加入到王文睿的小组,并对 node-webkit 项目做出了大量的改进。
后来赵成离开了中国英特尔开源技术中心,帮助 github 团队尝试把 node-webkit 应用到 Atom 编辑器上,但由于当时 node-webkit 还并不稳定,且 node-webkit 项目的走向也不受赵成的控制,这个尝试最终以失败告终。但赵成和 github 团队并没有放弃,而是着手开发另一个类似 node-webkit 的项目:Atom Shell,这个项目就是Electron 的前身,赵成在这个项目上倾注了大量的心血,这也是这个项目后来广受欢迎的关键因素之一,再后来github 把这个项目开源出来,最终更名为 Electron(2016)。
从这个表格我们也能看出来,今年其实是 electron 诞生的十周年
2011年-2012年 | 王文睿创建Node Webkit(NW.js前身) ,赵成加入 |
---|---|
2013年4月 | Atom Shell 项目启动 |
2014年5月 | Atom Shell 被开源 |
2015年4月 | Atom Shell 被重命名为 Electron |
2016年5月 | Electron 发布1.0版本 |
为什么选择 Electron
在这里我们用 Electron 和 WebView2 进行对比,因为她们最大的相似之处在于它们都是基于Chrome内核+前端技术结合提供的解决方案。(严格的说,WebView2 是基于 Edge 内核,但我们都知道 Edge 内核只是 Chrome 内核的 fork 版本而已)
相同点:
- 因为都用的 Chrome 内核去解释JS来运行程序,理所当然的,这两个技术在性能上差别并不大。
- 有相似的进程模型,由于都是源自于 Chrome 内核,所以它们的进程模型也是类似的。这也是 Chrome 浏览器的进程模型。
- 都是由一个 Main Process 与多个 Render Process 合作完成。Render Process 负责渲染展现页面,而 Main Process 则负责初始化应用程序,应用窗体等。
简单直观点说,你在 Chrome 浏览器打开了很多个tab页,每个 tab 页就是一个 Render Process,而 Chrome 本身还有个 Main Process,负责初始化与管理各个tab等。当然,它们都还有一个 help process 进程,负责处理一些额外的工作。
不同点:
- Eelectron 是一个独立的,整体的,单一的解决方案。意思就是你不需要其它框架,语言搭配来完成一个桌面应用程序。仅仅是前端技术就能完整的开发一个桌面应用。不管是页面上的 vue,TypeScript 或是与原生系统打交道的 NodeJS,它们通通是前端技术。这意味着一个前端团队能够在不依赖其它团队的前提下,基于Electron开发一个完整的桌面应用。而严格的来说,WebView2是一个组件或叫控件。就像我们在开发移动端的时候,经常说新开一个 webview 打开我们的H5页面,WebView2 也是这样的,所以不能独立存在,必须依赖于某种原生应用壳而存在的,这就是说一个前端团队,是没法利用 WebView2 开发出一个独立的应用程序,还需要一个原生开发团队配合着来做一个壳子。
- 构建方式不同:Electron 只支持一种构建方式,就是将 Chrome 内核一起打进你的最终程序中。所以,因为这一点它有一个问题,就是安装包会有点大。当然,优势是你使用的一定是特定的 Chrome 版本,不会有版本混乱问题。而 WebView2 则支持两种方式,一种是和Electron一样,将内核打进包中,另一个是共享系统的内核。
- Electron 是通过 NodeJS 来与原生打交道,比如读写系统文件等。而 WebView2 则是通过壳子的语言来与原生API打交道
- Electron 是支持两种不同的沙盒机制,一种是 Render Prcoess 不限制,可以通过 NodeJS 任意与原生API打交道,另一个是限制 Render Process 只能渲染展现网页,不能和原生系统打交道。而 WebView2 只有一种模式,就是开启安全沙盒机制。
- WebView2当下只支持Windows,而 Electron支持多端。
Electron 的优缺点
优点:
- 跨平台,一次开发,支持几乎所有平台。
- 不需要再考虑浏览器兼容了
- 使用前端开发技术栈,可以独自开发一个完整的桌面端应用。
缺点:
- 由于 Electron 是采取把 Chrome 内核打包进应用的模式,理所当然的体积会比较大。
- 资源消耗较大 Chrome 是比较吃内存的。
- 性能问提,性能上肯定无法和原生相比。
虽然 electron 有一点的缺点,但是 Electron 是最受欢迎的桌面端解决方案之一,基于它开发的项目非常多,如vscode,GitHub Desktop,Postman、迅雷等等。
Electron 的运行原理
Electron 是一个集成项目,由 Chromium + Nodejs + native api 构成:
- Chromium : 为Electron提供了强大的UI能力,可以不考虑兼容性的情况下,利用强大的Web生态来开发界面。
- Node.js :让Electron有了底层的操作能力,比如文件的读写,甚至是集成C++等等操作,并可以使用大量开源的npm包来完成开发需求
- Native API : Native API让Electron有了跨平台和桌面端的原生能力,比如说它有统一的原生界面,窗口、托盘、消息通知等。
每个 Electron 都是由 1 个主进程、1 个或多个渲染进程组成的,我们开发者的主要工作就是完成主进程和渲染进程的逻辑。
Electron 应用启动时,首先会加载主进程的逻辑,主进程会创建一个或多个窗口,我们可以简单地认为一个窗口就代表一个渲染进程,主进程负责管理所有的渲染进程。窗口内加载的页面就是开发者要实现的渲染进程的逻辑,渲染进程与主进程通信,是通过 IPC 消息管道进行通信的。两个渲染进程直接通信,主要通过主进程来中转消息以达到渲染进程间通信的目的。
Electron 提供的一系列内置为主进程、渲染进程服务的模块。大部分是专门为主进程的逻辑服务的,比如 app 模块、BrowserWindow 模块和 session 模块等;少量模块是专门为渲染进程的逻辑服务的,比如 ipcRenderer 模块等。
主进程:
- 连接着操作系统和渲染进程,可以把她看做页面和计算机沟通的桥梁。
- 可以用来做进程间通信、窗口管理等全局通用服务,维护一些必要的全局状态。
- 还有一些只能或适合在主进程做的事情。例如全局快捷键处理、设置托盘等等。
渲染进程:
- Electron 使用了 Chromium 来展示 web 页面,所以 Chromium 的多进程架构也被使用到。
- 每个 web 页面运行在它自己的渲染进程中。每个渲染进程都是相互独立的,并且只关心他们自己的网页。
- 使用 Electron 的 BrowserWindow 类开启一个渲染进程并将这个实例运行在该进程中,当一个BrowserWindow 实例被销毁后,相应的渲染进程也会被终止。
- 渲染进程中不能调用原生资源,但是渲染进程中同样包含 Node.js 环境,所以可以引 入Node.js(配置项可以配置,但不建议)
简单来说 Electron 的运行流程,,项目启动的时候,会先去读取 package.json 中的主程序入口文件,然后运行主程序,创建渲染程序,最后再执行IPC任务
预加载脚本:
在渲染器进程加载之前加载,并有权访问两个环境 (window /document 和 Node.js 环境) 。需要注意的是,如果操作 DOM 那就必须再 DOM 树加载完成,或者页面加载完成之后再执行
除了 dom 操作,一般我们通过 contextBridge 将 electron API 暴露给渲染进程(如,进程之间的通信),所以预加载脚本,可以看成是渲染程序和主程序之间的桥梁
Electron 拓展
Electron 除了可以用来开发桌面应用,还可以用来做一些有意思的事情,可能会对我们平时开发调试有所帮助。
注入脚本
这个其实就是我们之前提到的预加载脚本,预加载脚本是在目标网页的作用域下执行的,与目标网页的工程师自己写的代码没什么区别。所以我们可以随意修改网页 DOM,而且如果你想调用目标网页的某个服务端接口,只需要在脚本里直接写调用接口的逻辑就可以了,也不需要考虑如何模拟token,如何跨域等等问题。
比如像下面这样,注入一个 JS 文件到目标网页
csharp
let win = new BrowserWindow({
webPreferences: {
preload: path.join(appPath, 'yourPreload.js'),
nodeIntegration: true
}
});
win.loadURL('https://www.baidu.com/');
允许跨域
注入了脚本,获取到了受限的资源,你可能希望把这些资源提交到你自己的服务器上,或者你可能希望在注入的脚本里,访问另一个网站的 API,以获取更多的资源,这个时候,如果没做特殊配置的话,同源策略就会起作用,限制你这么干,浏览器往往会报跨域错误
在 Electron 中突破同源策略,就是一两个配置的事情,代码如下:
less
let win = new BrowserWindow({
width: 800,height: 600,
webPreferences: {
nodeIntegration: true,
webSecurity: false, //此参数禁用当前窗口的同源策略
}
})
win.loadURL('https://www.baidu.com/');
读写受限访问的Cookie
一般情况下,我们可以使用 document.cookie 访问浏览器里存储的同域的 Cookie,但也有例外,凡标记了HttpOnly 的 Cookie,通过这种方式都是访问不到的。
网站开发者之所以这么做,主要是为了防止跨站脚本攻击(XSS)和跨站请求攻击(CSRF)。
但这个限制在 Electron 面前也不值一提,如:
csharp
//获取Cookie
async function(name) {
let cookies = await remote.session.defaultSession.cookies.get({name});
if(cookies.length>0) return cookies[0].value;
else return '';
}
//设置Cookie
async function(cookie) {
await remote.session.defaultSession.cookies.set(cookie);
}
修改和转发请求
比如有的时候你不单单是希望给第三方网页附加代码逻辑,而是希望侵入式的修改第三方网站自身的代码逻辑。
但往往第三方的js代码是在一个闭包作用域内执行的,你的代码没办法注入到这个作用域内,去访问作用域内的变量或方法。
这个时候,可以把他的脚本文件下载下来,然后在这个文件中加上你的逻辑。你的逻辑可能就是粗暴的把它闭包作用域内的变量暴露到 window 对象上。这样你注入的脚本,就可以访问这个变量了。
修改完这个脚本文件后,把这个脚本文件上传到你自己的一个服务器上(或者本地起一个静态服务器),然后通过 Electron 把网页加载这个脚本文件的请求,转发到你自己的服务器上去,当这个网页再试图加载这个脚本时,得到的结果将是你修改过的脚本。
ini
win.webContents.session.webRequest.onBeforeRequest({ urls: ["https://*/*"] }, async (details, cb) => {
if (details.url === 'https://xxx/vendors.js') {
cb({ redirectURL: 'http://xxx/vendors.js' });
} else {
cb({})
}
});
通过这种方式,我们还可以进行线上代码本地调试。
反防盗链
有的时候你可能只是想把目标网站的一些静态资源嵌入到你的应用程序中,比如:图片或者视频。然而如果目标网站已经做了防盗链的工作,你这个功能可能就没那么容易实现了。
防盗链的主要目的有两个:一个是版权问题,别人未经授权就使用你的资源,另一个是流量压力的问题,盗链产生了大量的请求,这些请求对于网站运营者来说没任何价值。
防盗链最常见的做法就是识别 HTTP 的 Refer 请求头,这个请求头代表着发起请求时的来源地址,
服务器会根据这个 Refer 请求头来推测出当前请求是否为一个盗链请求(判断这个 Refer 请求头的内容是不是自己域名下的一个地址)。
Electron 允许开发者监听发起请求的事件,并允许开发者在发起请求前修改请求头,我们可以在这个事件里修改这个 Refer 请求头。
ini
let session = window.webContents.session;
let requestFilter = { urls: ["http://*/*", "https://*/*"] };
session.webRequest.onBeforeSendHeaders(
requestFilter,
(details, callback) => {
if (details.resourceType == "image" && details.method == "GET") {
delete details.requestHeaders["Referer"]; //这里我直接删掉了这个请求头,也可以修改成其他内容
}
callback({ requestHeaders: details.requestHeaders });
}
);
socks代理
Chrome 本身是支持代理的,使用 Electron 你可以通过编程的方式把你的代理内置到你的应用程序中,这样你的用户就可以自由的访问国外的一些网站了
常见的代理服务器有http代理、https 代理和 socks 代理,socks 代理隐蔽性更强,效率更高,速度更快。咱们这里就聊聊如何在 Electron 应用内植入socks5 代理访问网络服务。
rust
let result = await win.webContents.session.setProxy({
proxyRules: 'socks5://58.218.200.249:2071'
});
win.loadURL('https://www.ipip.net');
上面代码中,我们使用 session 实例的 setProxy 方法来为当前页面设置代理,socks 代理,代理设置成功后,我们马上使网页加载了一个IP地址查询的网址,在此页面上我们可以看到访问该页面的实际IP地址,如果这里显示的地址是你的代理服务器的所在地的地址,那么说明代理设置成功。
以上这种方法可以给单个渲染进程设置代理,如果你需要给整个应用程序设置代理,可以使用如下代码完成:
rust
app.commandLine.appendSwitch('proxy-server', 'socks5://58.218.200.249:2071');