极客手中的利器:Electron

九路 等级 332 0 0

作为一个前端开发人员,你可能已经听说过Electron了,

你知道VS Code是基于这个技术开发的。

不但VS Code,目前一些大热的软件:飞书、Slack、skype的桌面版都是基于这个技术开发的。

即使如此,这也并不足以引起你的重视,

毕竟桌面软件式微,移动端和Web开发才是大方向,你的工作可能根本就不会涉及桌面开发领域的内容。

但我想告诉你的是:

即使工作不会涉及到,也应该学一下Electron,因为它是极客手里的利器,你可以通过他做很多Amazing的事情

本文主要讲讲Electron是什么,以及它能做什么极客工作。

极客手中的利器:Electron

本文大部分内容摘自我的新书《Electron实战——入门、进阶与性能优化》,目前刚刚上市:

京东购买地址: https://www.helloworld.net/redirect?target=https://item.jd.com/69220543055.html

天猫购买地址: https://detail.tmall.com/item.htm?spm=a230r.1.14.144.38ec66adZeGfZS&id=618291446747&ns=1&abbucket=16

(_市面上还有另外两本关于Electron的书,它们都没有涉及本文中所讲的这些极客知识_)

同时,也欢迎对这个领域知识感兴趣的朋友加我的QQ群:949674481,一起交流

Electron是什么

官方的介绍是:

基于它开发者可以“使用 JavaScript,HTML 和 CSS 构建跨平台的桌面应用程序”。

我们知道 JavaScript,HTML 和 CSS都是前端的技术,前端技术是要用在浏览器中的,

Electron能让开发者这么做,它内部至少有一个浏览器核心。

没错,它内部的浏览器核心就是谷歌的Chromium

你可以把Chromium理解为Chrome的精简版,

Chromium不包含tab页的控制,搜索引擎、书签等等这类东西,

但它包含:排版引擎、渲染引擎、脚本引擎、调试工具等。

它是一个很纯净的浏览器核心(还称不上一个浏览器,只是一个浏览器核心)。

但单单有浏览器核心还不够,

因为开发一个桌面应用还涉及到文件系统访问、进程控制、使用各种网络通信协议等

(浏览器对HTTP协议支持的很好,但对更底层的TCP/IP协议就没什么支持了)

再加上各种常见加解密、集成原生程序模块等需求,

如果自己实现一套库以支持这些需求,显然工作量非常巨大,Electron团队索性把Node.js集成进来了

但这依然不够,

因为Node.js毕竟更善于做服务端的事情,大部分开发者使用Node.js都是在命令行下使用的,

像使用系统菜单、系统级快捷键、剪切板、托盘图标、系统对话框这些能力,

Node.js就显得无能为力了,

所以这部分工作是Electron团队自己用C++实现的,而且各个平台都实现了一遍,以保证跨平台需求。

可以说Chromium、Node.js和一套C++库是构成Electron的三大基石。

当然这样分析有点浮于表面,还有很多细节值得深究,

比如Electron是如何打通Chromium和Node.js的事件循环的、

Electron是如何通过主进程控制各个窗口,以及各个窗口是如何相互通信的等。

这篇文章里我们就先暂且不详细讨论这些内容。

另外,Electron也不是以我分析的这种方式设计演化来的,

它的由来还要从它的一个竞品NW.js说起

NW.js和Electron非常相似,我们前面说的三大基石NW.js也都有。

非但如此,这两个框架还有着极深的渊源,以下内容摘自我书稿的一部分:

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。
你可能从没听说过这两个名字,但开源界就是有这么一批“英雄”,他们不为名利而来,甘做软件行业发展的铺路石,值得这个领域的所有从业者尊敬。

这些技术(三大基石)组合在一起,不但能开发出大型商业应用的桌面客户端,

而且也为一些极客应用提供了很大的便利性。下面我们把典型的几个拿出来聊聊:

注入脚本

Electron内部拥有一个完整得浏览器核心,

你可以用程序操纵这个浏览器核心,让它加载一个第三方网页,比如:淘宝的生意参谋、网易云音乐、gitee等,

但单单加载这些网页,并没有什么稀奇的,毕竟在浏览器里也能加载这些网页。

最有意思的是,你还可以给这些网页注入脚本,比如像下面这样,注入一个Js文件到目标网页:

let win = new BrowserWindow({
 webPreferences: { 
   preload: path.join(appPath, 'yourPreload.js'),
   nodeIntegration: true 
 }
});
win.loadURL('https://www.baidu.com/');

如果你只希望注入一两句代码,也可以通过如下形式注入脚本:

let decryptStr = await this.win.webContents.executeJavaScript(`window.__allen_decrypt('${encryptStr}')`);

这些脚本是在目标网页的作用域下执行的,与目标网页的工程师自己写的代码没什么区别。

想象一下,你如果想调用目标网页的某个服务端接口,你是不是应该考虑如何模拟token,如何跨域等等

现在你只需要在脚本里直接写调用接口的逻辑就可以了。

另外,如果你不喜欢目标网页的页面样式,你也可以直接注入一段样式脚本,代码如下:

let key = await win.webContents.insertCSS("html, body {  background-color: #f00 !important; }");
//await win.webContents.removeInsertedCSS(key); //在未来的某个时刻,还可以移除掉这个样式的作用

值得一提的是,你注入的脚本还可以访问Node.js的API

也就是说,你在脚本中获取到了目标网页的资源后,可以直接写到你本地文件里。

突破同源策略

注入了脚本,获取到了受限的资源,你可能希望把这些资源提交到你自己的服务器上,

或者你可能希望在注入的脚本里,访问另一个网站的API,以获取更多的资源,

这个时候,如果没做特殊配置的话,同源策略就会起作用,限制你这么干,

浏览器往往会报如下错误:

Access to XMLHttpRequest at 'https://www.helloworld.net/redirect?target=https://www.domain1.com/aggsite/AggStats' from origin 'https://www.helloworld.net/redirect?target=http://www.domain2.com:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

“同源策略”是浏览器的一个安全功能,

“同源”是指如果两个页面的协议(http/https)、端口和主机都相同,则两个页面具有相同的源。

同源策略规定不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。

只有同一个源的脚本才具备读写cookie、session、ajax等的操作的权限。

在Electron中突破同源策略,就是一两个配置的事情,代码如下:

let win = new BrowserWindow({
    width: 800,height: 600,
    webPreferences: {
        nodeIntegration: true,
        webSecurity: false, //此参数禁用当前窗口的同源策略
    }
})
win.loadURL('https://www.baidu.com/');

这样设置之后,浏览器的所有同源策略限制就全部失效了。

如果你只是希望在https域下访问http的资源,那么你可以不用关掉整个同源策略,

只需要把allowRunningInsecureContent这个配置设置为true即可(它同样也是webPreferences下的一个属性)

如果你设置webSecurity为false了,那么allowRunningInsecureContent会自动设置为true。

读写HttpOnly标记的Cookie

由于浏览器每次与服务端的交互,都会携带同域下的所有Cookie,

所以网站开发者往往会把标记用户身份的信息(比如用户token)放到Cookie里。

一般情况下,前端开发工程师可以使用document.cookie访问浏览器里存储的同域的Cookie,

但也有例外,凡标记了HttpOnly的Cookie,通过这种方式都是访问不到的。

网站开发者之所以这么做,主要是为了防止跨站脚本攻击(XSS)和跨站请求攻击(CSRF)

跨站脚本攻击(XSS,是Cross Site Scripting的缩写),一旦网站允许用户提交内容,并且会在网站的某些页面上显示用户提交的内容,比如留言或者博客,那么不做防范的话,就有可能受到跨站脚本攻击。恶意用户会在提交内容时在内容中夹带一些恶意JavaScript脚本,当其他用户访问页面时,浏览器会运行这些恶意脚本,恶意脚本有可能会窃取用户的Cookie、页面上的用户隐私信息等,并发送到恶意用户的服务器。他们可以通过这些窃取来的信息模拟用户身份完成非法操作。这就是跨站脚本攻击。  
跨站请求攻击(CSRF,是Cross Site Request Forgery的缩写),当用户登录了自己信赖的网站后,用户身份信息(token)会被保存保存在用户的浏览器上,后来用户又不小心打开了一个恶意网站,这个恶意网站可能会要求浏览器请求用户信赖的网站(通过iframe等形式),如果用户信赖的网站没有做安全防范的话,可能会被恶意网站获取到用户的敏感信息(token),从而给用户带来伤害。

但这个限制在Electron面前也不值一提,我们可以通过下面这种方式读写受限访问的Cookie:

//获取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);
}

通过这种方式,无论Cookie有没有设置HttpOnly属性,都可以成功读写。

转发/修改请求

有的时候你不单单是希望给第三方网页附加代码逻辑,

而是希望侵入式的修改第三方网站自身的代码逻辑。

但往往第三方的JavaScript代码是在一个闭包作用域内执行的

你的代码没办法注入到这个作用域内,去访问作用域内的变量或方法

碰到这样的状况该怎么办呢?

此时,你第一步要做的,就是分析清楚它的脚本是如何执行的,是哪个脚本文件执行的。

这个过程是一个非常有趣的过程,不可避免的你要用到谷歌浏览器的开发者调试工具,

如果对方的代码是压缩过的,你可能还要给它“美化”一下,再逐步调试(如下图)。

极客手中的利器:Electron

搞清楚逻辑之后,就把他的脚本文件下载下来,然后在这个文件中加上你的逻辑,

你的逻辑可能就是粗暴的把它闭包作用域内的变量暴露到window对象上。

这样你注入的脚本,就可以访问这个变量了。

(这个过程有很多技巧,因为与Electron无关,我就不展开了)

修改完这个脚本文件后,把这个脚本文件host到你自己的一个服务器上,

然后通过Electron,把网页加载这个脚本文件的请求,转发到你自己的服务器上去,

这个转发请求的代码如下:

win.webContents.session.webRequest.onBeforeRequest({ urls: ["https://*/*"] }, async (details, cb) => {
    if (details.url === 'https://g.alicdn.com/dt/op-mc/vendors.js') {
        cb({ redirectURL: 'http://domain.com/vendors.js' });
    } else {
        cb({})
    }
});

当这个网页再试图加载这个脚本时,得到的结果,将是你修改过的脚本。

有些网站,对一些敏感数据保护的很好,客户端请求这些数据时,得到的是服务端加密过的数据,

客户端执行解密后,再使用这些数据。

对于这类网站,Electron的这个能力,无疑是非常有力的。

当然,你也可以考虑使用service worker来办这个事儿,甚至可以自己在客户端模拟一个响应,不用再经由你的服务器转发了。

如果你没有自己的服务器,也可以通过Node.js的能力,自己在软件里起一个localhost的服务。

反“防盗链”

有的时候,你可能只是想把目标网站的一些静态资源嵌入到你的应用程序中,

比如:图片或者视频

然而如果目标网站已经做了防盗链的工作,你这个功能可能就没那么容易实现了。

防盗链的主要目的有两个:一个是版权问题,别人未经授权就使用你的资源,

另一个是流量压力的问题,盗链产生了大量的请求,这些请求对于网站运营者来说没任何价值。

防盗链最常见的做法就是识别HTTP的Refer请求头,这个请求头代表着发起请求时前一个网页的地址,

网站运维工程师会根据这个Refer请求头来推测出当前请求是否为一个盗链请求,

(判断这个Refer请求头的内容是不是自己域名下的一个地址)

Electron允许开发者监听发起请求的事件,并允许开发者在发起请求前修改请求头

我们可以在这个事件里修改这个Refer请求头,代码如下:

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 });
    }
);

给应用植入socks5代理科学上网

Chrome本身是支持代理的

使用Electron你可以通过编程的方式把你的代理内置到你的应用程序中

这样你的用户就可以自由的访问国外的一些网站了

这部分的技术我不多做介绍了,书里写了。

最后

我曾经用Electron开发过一个文章发布工具,可以把文章一键发布到:博客园、CSDN、简书、大鱼号、头条号等

不过很久没维护了,希望以后有时间再重写一版。

收藏
评论区