Electron的一个增量升级方案

Electron的一个增量升级方案

本文主要是讲述一个托管于Github的Electron项目,如何实现使用小文件的增量升级

背景

我有一个托管在github的开源项目,使用的技术框架是 Electron+Vue,electron可以说是一个有一定名气的框架了,但是在处理一个增量更新的需求的时候就遇到了一些问题,特将我的处理方法分享一下。

已知 Electron其实是有很多自动升级的方案的,但是增量升级的非常少,所谓的自动升级方案大多是利用模块中的打包工具的预留接口或者在代码中直接下载整个安装数据包,如果是公司或者大型的项目,当然无可厚非,但是对于一个小型开源项目来说,这种做法十分的不经济。

首先一方面,我所使用的Electron-builder,每次打包生成的安装文件有40M之大,这是Electron 2.0的,前期项目还使用了Electron 1.8,安装文件也有36M。第二,这个项目比较小,直接托管在github上,使用github的release进行分发,如果要做自动更新的话,势必要自己部署更新和分发服务器(因为github的release基于aws,在国内被墙)。

想到之前看到的一篇文章,利用他们团队每次只更新Renderer.js的特性,做了个小型分发服务器,于是我开始寻找是否有直接分发升级代码的可能。

问题定位

我的项目是由 Electron-builder 进行打包的,其在打包过程中使用 nsis来创建windows环境下的安装包。于是先去找到安装后释放的文件。

在对应的安装目录下,会释放一个resource文件夹,其下有一个比较大的app.asar,根据程序 log,程序的主要代码文件就都储存在这里。

然后我们将 Electron-builder的配置文件进行修改,使其不打包为asar。

由于我使用的是Electron-Vue基于Vue-cli的自动构建,因此配置文件就是package.json

"build":{
   "asar": false, 
}

在build配置项下,将asar,设置为false,重新打包。这次直接使用dir模式打包来查看他的目录结构。

代码提示了一个不安全的warning,

asar using is disabled — it is strongly not recommended solution=enable asar and use asarUnpack to unpack files that must be externally available

不用管,打包完成后,在原app.asar的位置,变成了app文件夹,打开文件夹,是node_modules、package.json和一个dist文件夹,dist文件夹里是electron四件套

├─dist
│  └─electron
│      │  index.html
│      │  main.js
│      │  renderer.js
│      │  styles.css
│      │
│      └─fonts
│              element-icons--fonts.ttf

好了,这里就是重点了,这里的index.html、main.js、rederer.js和styles.css就是每次编译生成的文件,因此,如果要进行增量更新,这四个文件是必须更新的。

那么首先,我们要去除掉对这几个文件的打包,由于每次版本更新时,package.json的内容也会改变,所以package.json也必须排除。再次修改 Electron-builder的配置:

"build":{
   "asar": true,
    "asarUnpack":[
      "./dist/electron",
      "./package.json"
    ],
}

为什么一定要保留asar包呢?因为asar包有助于加快安装文件的安装速度,如果保留所有文件夹的话,在释放文件的时候磁盘压力会比较大,因此,使用asarUnpack属性,将不需要打进asar包里的文件路径指定。

再次查看打包后的结果:

│  app.asar
│  electron.asar
│
└─app.asar.unpacked
    │  package.json
    │
    └─dist
        └─electron
            │  index.html
            │  main.js
            │  renderer.js
            │  styles.css
            │
            └─fonts
                    element-icons--fonts.ttf

我们可以看到,在原来resource文件下,多出了一个app.asar.unpack文件夹,里面有所有我们所需要的文件。

完整方案

之所以说是完整方案而不是完整代码,是因为其具备一定的特例性。

我们这次的重点,依然还是针对托管在github的项目的增量更新,因此,我们首先要保证的,就是增量更新还得在github上。

我们不可能在发布一个release安装包的同时还去发布一个增量更新文件来进行增量更新,这不好,也不自动化,根据上节,我们已知,我们需要更新的就是包括package.json在内的五个文件,因此,我们首先需要定位这五个文件的位置,我使用的方法是,基于__filename,在代码中,通过搜索app.asar来定位到resource文件夹,再通过相对的路径获取到对应的文件的目录。

为什么是执行的时候路径里会有app.asar而不是app.asar.unpack呢 ,我是这么理解的,unpack的作用就是使程序在每一次运行之前,将unpack的文件重新打进asar包,然后再继续运行,这样既不破坏asar包的路径关系,也不会影响文件的独立性。

然后,我们需要在git里解除对dist目录的ignore,这样,我们每一次提交都会留下一个可以使用的增量更新文件,在release以后也可以根据tag来找到对应的dist里的文件。

其他的就没什么好说的了,剩下的就是程序该干的事情了,使用程序检查更新,是使用github的

https://api.github.com/repos/{username}/{repo}/releases/latest

可以获取到最后一个release的tag和一些release的信息,我们主要使用的是tag_name,根据tag_name,我们可以在

https://raw.githubusercontent.com/{username}/{repo}/${tag}/{relative_route}

这个地址获取到项目对应文件的原始代码。

github的请求需要带上有效的UserAgent,否则会被认为是非法

请求到对应文件之后,根据前面获取的本地路径,调用fs模块将字符写进文件,然后重启程序,就完成了这一次的增量更新。

总的来说,这样的更新方式有优点有缺点,优点是:增量更新的文件小,更新速度快,没有分发的烦恼。缺点当然也有,缺点就是无法适用于依赖会改变的情况,如果程序的依赖发生更改,那么之前的app.asar的文件就不再和新文件匹配,需要重新安装新的安装包。

后记

从全局来看,这种增量更新的办法是非常好的,既解决了用户频繁更新需要下载大安装包的问题,又解决了开发者不便部署安装服务器的问题,整个操作的过程也并不十分复杂,只需要在取消git对dist的ignore,并且每次relase之前确保当前分支build了最新的dist文件即可,如果github的连通率太低,甚至还可以使用国内的gitee的raw文件,使用克隆git仓库,每次release之后到gitee手动点一下拉取最新更新即可,如果不嫌烦的话,还可以多部署几个其他的开源平台,运维成本基本等于0。


项目相关:BLS,一个B站直播批量挂机工具。

关于Node.js的依赖管理

关于Nodejs的依赖管理

npm和yarn之类的工具能够自动化引入和构建依赖,但是最近发现一个比较坑的地方。

在npm上发布的package一般来说会锁定其所依赖的包的版本

我在使用request-promise的时候,发现request-promise已经依赖了tough-cookie,于是在把几个代码文件迁移到另一个项目的时候没有重新添加tough-cookie的依赖,导致中间解析cookie时出现了很多莫名其妙的错误,遂重新添加依赖后才成功。

也就是说,当一个项目越做越大以后,里面的包的依赖关系将会变得更加复杂,一个包所依赖的其他包的可能包含了某个package的多个版本。而随着package的增多,这种臃肿的包将会越来越多。

不禁想反思一下前端这个步子是不是迈得太大了

浅述使用 Javascript 事件劫持干扰 Vue v-model的一些思路

浅述使用 Javascript 事件劫持干扰 Vue v-model的一些思路

关于近日在开发一个UserScript的时候一些做法和思考

相关:

前言

本文主要探讨一下Javascript解决Vue的v-model绑定机制的问题。案例实现代码如上。

主要实现方法是事件捕获和事件生成。

准备

首先我们需要准备一个Vue页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>测试页面</title>
    <script src="//cdn.bootcss.com/vue/2.5.13/vue.js" ></script>
    <script src="//cdn.bootcss.com/jquery/3.3.1/jquery.js" ></script>
</head>
<body>
<div id="app1">
    <div id="in">
        <textarea name="input" id="myinput" cols="60" rows="5" v-model="text" @input.prevent="text=text.substr(0,10)"></textarea>
        <p>
            <span>输入:</span>{{text}}
        </p>
        <p>
            <button @click="sended=text,text=''">点击发送</button> {{sended}}
        </p>
    </div>
</div>
</body>
<script>
    new Vue({
        el:"#app1",
        data:{
            text:"",
            sended:"",
        },
    });
</script>
</html>

该页面实现了将输入的内容回显,在点击发送的时候将内容显示到发送位置,并且限制文本框最大输入字符个数为10,接下来将使用用户脚本,来干扰 Vue 的 v-model。

用户脚本

用户脚本是指由用户指定的运行在页面之上的 Javascript 脚本,用于为网站提供新功能或者对页面进行修改使其更易用,主要依赖 油猴、Tampermonkey 等浏览器插件,现在也有很多浏览器自带用户脚本的功能,在专门的网站上可以搜索到其他人上传的用户脚本,比如 GreasyFork

Vue 中的 v-model 是通过监听事件来实现监听页面元素更改的

用户脚本相当于是在页面中直接插入由你编写的一个 js 文件,并由浏览器扩展负责在每次展现指定页面的时候都自动引入这个文件。

为了方便调试,我们直接使用Chrome控制台来进行代码调试

调试

  • 首先,使用jq修改输入框
    运行$("#myinput").val("1234567890"),发现textarea元素改变,但是没有在页面中看到输入的回显,点击发送时也不能获取该值,说明 v-model 中的值没有得到更新。
  • 第二步,伪造一个input事件
    使用 $("#myinput")[0].dispatchEvent(new Event("input")) 后,发现能够正确回显,说明 v-model 的值得到了更新
  • 第三步,确认 v-model 的更新位置
    由于事件存在捕获阶段和冒泡阶段,我们在input上使用了stop修饰符阻止了事件冒泡,所以只需要添加一些监听器即可。

使用以下代码部署监听器:

let m = $("#myinput")[0];
m.addEventListener("input",(e)=>{console.log("textarea :在冒泡阶段发现该事件!")});
m.addEventListener("input",(e)=>{console.log("textarea :在捕获阶段发现该事件!")},true);
let d = $("#in")[0];
d.addEventListener("input",(e)=>{console.log("div :在冒泡阶段发现该事件!")});
d.addEventListener("input",(e)=>{console.log("div :在捕获阶段发现该事件!")},true);

然后使用 $("#myinput")[0].dispatchEvent(new Event("input")),触发监听器,结果为:

div :在捕获阶段发现该事件!
textarea :在冒泡阶段发现该事件!
textarea :在捕获阶段发现该事件!

textarea的冒泡事件触发在捕获事件之前,测试了一下时间,大概确实是在目标阶段,会先触发元素的冒泡时间,再触发捕获事件。(也许冒泡事件的利用率高,所以为了提高性能故意提前?坐等指教)。

可以看到在整个事件流中,v-model 的监听是在目标阶段,然后就因stop修饰符阻止了事件继续冒泡。所以如果我们要对 input 事件进行劫持,可以依靠父元素的事件监听,在捕获阶段将事件劫持。

尝试

刷新页面清除之前的监听器,然后使用以下代码,尝试在捕获阶段对事件进行劫持:

$("#in")[0].addEventListener("input",(e)=>{
    e.stopPropagation(); //阻止事件传播
    console.log("Hijacked A Event");
    return false;
},true);

此时,直接在输入框中进行输入不会回显,可以看到控制台打印出 Hijacked A Event,说明劫持事件已经完成。

完成事件的劫持,则我们最终对 v-model 的欺骗完成了一半,接下来要使用自定义事件来使 v-model 的值可以被js 修改。

首先,刷新页面清除之前的监听器,然后对之前的生成事件的代码进行修改。

function FakedValue(){
     $("#myinput").val("Faked");//修改value    
  
    let e = new Event("input");
    e.myself = true;
      $("#myinput")[0].dispatchEvent(e)//伪造事件,触发V-model更新
}

为事件设置自定义属性,即可在捕获的时候进行区分,监听器的代码只需修改成:

$("#in")[0].addEventListener("input",(e)=>{
    if(e.myself) return true; //判断是否是伪造的事件
    e.stopPropagation(); //阻止事件传播
    console.log("Hijacked A Event");
    return false;
},true);

在页面上运行上述代码,然后就可以发现,自己在输入框的输入会被劫持,而执行 FakedValue() 才可以触发v-model的更新。

总结

以上就是使用 Javascript 通过对事件的劫持来对v-model进行控制的一些思路,对按钮的click事件劫持也可以使用同样的操作。

该文仅供同样开发 UserScript 的同学们参考,正常的网页开发用不到这些内容。详细案例代码参考在文章开头。

tampermonkey的一个坑

关于Tampermonkey脚本开发时遇到的一个坑

  • GM_XmlhttpRequest
    这个GM函数在wiki上是如此解释的:

Make an xmlHttpRequest.
Property of details:
method one of GET, HEAD, POST
url the destination URL
headers ie. user-agent, referer, ... (some special headers are not supported by Safari and Android browsers)
data some string to send via a POST request
binary send the data string in binary mode
timeout a timeout in ms
context a property which will be added to the response object
responseType one of arraybuffer, blob, json
overrideMimeType a MIME type for the request
anonymous don't send cookies with the requests (please see the fetch notes)
fetch (beta) use a fetch instead of a xhr request
(at Chrome this causes xhr.abort, details.timeout and xhr.onprogress to not work and makes >xhr.onreadystatechange receive only readyState 4 events)
username a username for authentication
password a password
<events> onload, onerror, onreadystatechange, onprogress, onloadstart, ontimeout

在这个地方,约定了Data参数,与ajax的不同,是使用string字符串的形式,详细表述在网上也有一些,但是问题不在这里
比较坑的是,使用post方法传送数据时,一直出现400错误,经过反复排查,才发现headers首部设置不能随便添加值,headers的字段会对请求发生干扰,由于chrome环境没法捕获数据包,因此不清楚到底是哪里发生了错误。

解决这个错误之后,又发现,不设置headers字段时,服务器上会接收不到参数
必须设置headers参数为

headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }

至此,一个完整的post请求才算正常