【CEP专题】聊聊插件怎么更新
之前在群里有很多小伙伴在讨论插件更新这个话题,我自己在插件开发这么些年,前后也折腾过各种插件的更新方式,今天在这里做一些整理,给大家一些参考。
1. 为什么要更新
当我们的插件已经发布到了用户手中,用户已经在正常使用我们的产品,但是我们又想做一些更新,比如提供了一个新的功能版本,或者需要修复一个严重的bug,这个时候就需要考虑插件更新的问题了,我们希望用户能够及时的更新到软件的最新版本,以保证最好的使用体验。
但是插件是一个类客户端App产品,不像网站那种可以随时更新,用户如果想要更新,得经历下载, 安装,重启之类的操作,对于用户而言是一种负担。同时,以我的个人经验来看,用户对软件的更新会有自发的恐惧,他们会担心两点:
- 新版本不稳定,带来更多的问题
- 新版本会修改原有的交互,自己好不容易稳定下来的使用体验会被打乱
基于以上的两点,用户一般不太愿意去更新新版本,只要当前的版本能够满足需求,够用就行。这就会导致你的版本可能更新到很高了,依然还有许多用户在用一些很旧的版本。给大家看看我的切图工具的版本分布就能一探究竟。插件最新已经到4.3的版本了,依然还有大量的用户在用4.1甚至4.0的版本。
2. 如何更新
那如何才能让用户能够及时的更新到最新版本呢?我们可以分主动更新和被动更新两种方式来考虑。主动更新指的是用户来自己选择是否更新,被动更新是插件自己完成更新,用户无感知。
无论哪种模式,我们都有一个必备的环节是:版本检测。我们需要在app里头设置一个版本号,并且在插件启动的时候,请求我们的服务器做新版本检查,如果存在新版本,再继续下一步的操作。
2.1 版本检测
这个过程其实很简单,在插件中内置版本号的配置
1 | { |
在插件启动的时候,将版本号发送给服务器,服务器根据最新的版本号进行比对,返回是否有新版本以及更新的介绍。
2.2 主动更新
主动更新,指的是用户自己进行更新,更新的过程由用户来进行主导和选择。当然,用户也可以选择不更新(如上图)。如果用户选择更新,会下载出新的版本安装包,用户安装后,重启ps,新版本生效。
你会发现这个主动更新的过程,就是将更新的决策权交给用户,然后用户一般都会选择不更新。但是我目前依然采用的是这种模式,我在文章后面会讲原因。
2.3 被动更新
我们来重点聊一下被动更新,就是在用户无感的情况下,让插件自动升级到最新的版本,这也应该是大家最想知道的内容。
方案1: 基于远程网页
前文提到了,我们访问的网站一般都可以做到自动更新,那我们也可以利用此机制到我们的插件当中。如果你还记得我前面的文章里头有介绍过,CEP的插件支持iframe,那我们就可以考虑将本地的插件面板变成一个壳,而真正的页面内容呢,都放到远程服务器上。这样每次我们只要将新版本插件更新到服务器上,用户就自动打开最新的版本了。
这种方案,大家可能会好奇了,基于远程服务器的网页,也能和宿主进行交互么,调用jsx文件也能执行么?这里有两个需要注意的地方
- JSX文件加载
由于你的文件都在远程服务器,对于html/js/css这些就和传统的网页没有什么区别了,重点是JSX文件,由于我们一般都是通过$.evalFile这样的方式来加载jsx文件的,那远程的jsx文件如何来加载呢?
我们有两种方法来解决jsx文件加载的问题:
- 将jsx文件当做字符串进行网络请求读取下来之后,通过
evalScript
来执行 - 将jsx文件下载保存到本地目录下,再进行
evalFile
加载
- 与宿主的交互
以前旧版本的Photoshop对于iframe里头的页面不是做任何限制的,插件框架也依然会在你的iframe页面中的window
对象中注入__adobe_cep__
还有cep_node
等这些对象,你完全可以正常使用CSInterface
对象,和普通本地插件的使用没有任何区别。
但是等到CEP11的版本之后(photoshop CC2022),iframe就加入了同源限制,使得你在iframe里头调用CSInterface
的evalScript
函数会拿不到回调
1 | var cs = new CSInterface(); |
这个时候,我们就得通过和父级窗口来进行通信,让本地插件的如何html文件来承载和宿主的交互,并将结果返回给iframe页面。父级和子级页面之间的通信,我们可以通过postMessage
来实现。
1 | <html> |
在iframe的子页面,通过postMessage
来发送消息给父级页面,父级页面通过addEventListener
来监听消息,然后调用evalScript
来执行宿主的jsx文件,最后将结果通过postMessage
发送给子页面。
1 | // iframe 页面 |
整个流程是可以正常跑通的,但是你也会发现,它大幅增加了插件的复杂度,由于增加了iframe层的通信,使得你的插件在开发、调试、问题定位上都变得更加麻烦,并且还会影响你的编码结构,你需要将postMessage
的通信进行二次封装,才能最终模拟出来默认evalScript
的语法效果。
我们来评估一下这个方案的优缺点:
优点:
- 安装包会很小,因为插件只是一个壳,核心都在远程服务器上
- 可以实现自动更新,插件每次打开都是服务器最新的代码,完全没有版本的概念
缺点:
- 对用户的网络有强要求,无网或弱网的情况下无法正常使用你的产品
- 由于iframe的同源限制,使得你的插件在开发、调试、问题定位上都变得更加麻烦
- 外壳的的部分如果要升级,依然得用户下载安装更新,对外壳的功能设计有很强的通用性要求
- 还需要关注浏览器的缓存带来的文件更新不完整的问题
综合来看,这个方案虽然看起来很性感,但是在实际应用层面要踩的坑也非常多,总体来说,性价比不太高。
方案2: 本地覆盖
既然要做到无感更新,那我能不能直接插件后台默默的将新版本的文件下载下来,并覆盖现有的插件文件呢?看起来是一个简单直接的方法呀?
答案其实也是可以的。我们将新版本的文件下载下来直接覆盖安装到插件的目录文件夹下,保证文件名和目录结构一致,直接覆盖就可以了。下面给个简单的代码样例:
1 | // 挨个下载新版本的文件,下载完直接覆盖到插件的同级目录下 |
覆盖完文件之后,我们重启插件,就会发现它真的就是新版本了!重启插件也可以用代码来实现,于是整个流程就是后台默默下载更新的文件,并覆盖当前的插件文件,接着用代码执行插件重启(如何用代码重启插件,大家可以看我前面那篇隐藏面板的妙用的文章)。
这个方案,看起来好像很完美?也不完全是,它也有一些问题:
- 它要求你的插件必须安装在用户目录的路径下,如果你的插件是安装在系统目录下,那么你就没有权限覆盖它了。但是安装在用户目录下,有时候可能会有问题,因为用户的电脑在用户管理上是很乱的,可能存在找不到你插件的情况。
- 下载的过程需要非常小心,因为你需要下载许多的文件,在网络传输的过程中会面临个各种文件损坏,下载不完整404等等情况,要保证下载的文件是正确的,要做好文件hash验证,网络失败异常处理等等。
- 某些情况下,文件覆盖会失败,比如你的index.html文件当前正在被引用,就可能导致无法进行覆盖。
综合来看,这个方案除了对插件的安装路径有要求之外,其他的都还勉强可以解决,总体比第一个方案要好很多。
方案3: 动态加载
这个方案,相当于是方案1和2的结合体,我们将主体面板也当做一个壳,但不通过iframe来加载远程页面了,而是将主体面板功能单独拆分出来一个模块,在打开面板的时候,先按照方案二的步骤将主体面板的文件全部下载下来,放到本地的某个位置(不一定需要在插件目录下)。接着在通过模块的入口文件加载进来主体内容。
这种模式,比较适合那种插件内容很多,需要分模块的产品,比如大家都比较熟悉的设计助理这款产品,就是采用这种模式来实现的。在插件目录下的代码中,只有很少的内容,将不同的模块甚至版本,都通过动态下载文件的方式,存放在另外的位置,在加载进来,一定程度上还可以提高插件的安全性。
这种方案的示例我就不贴了,大家可以自行去尝试。
这种方案的好处是,不要求你将插件安装在用户的目录下,只要将动态的内容部分下载下来,下载的内容放到有权限写的用户目录的某些位置就可以了(一般会放在%userdata%
目录下)。这个方案带来的问题也和之前类似:
- 整体增加的插件的复杂度,对你的开发、调试、问题定位都提出了更高的要求,更加适合插件体量比较大的产品
- 稳定性层面也会受到影响,像设计助理这种通过动态加载html进来,就还得使用iframe,会一定程度降低插件的交互体验
总结
这篇文章我介绍了几种能够静默更新插件的方法,能够让用户无感知的去完成插件的更新,但是每一种方案都没法十全十美,各自存在优缺点,你可以根据自己当前的情况来进行选择。
综合来看,方案二是一个相对好一些的方案。虽然这些方案我在以前都陆续的尝试过,最终,还是选择了主动更新,这样的考虑主要从用户视角出发,自动更新从产品形态上其实是会造成用户的预期不稳定的,比如我昨天用的还是这个样子,第二天打开变成另外一个样子了,对使用者而言会带来恐慌。另外一个点是这些自动更新的方案都存在一定程度的失败率,不管你的更新过程写的多完整,用户的场景是多变的,比如你下载的文件被用户的杀毒软件给删掉了,就会导致更新不完整,造成产品不可用等等情况……于是我最终选择将更新的权利交给用户。虽然对于产品升级迭代不理想,但是用户的使用预期是稳定的,这本身也是用户体验的很重要一部分。
这篇文章就到这里了,如果你有什么问题,或者有更好的方案,可以在评论区留言,也可以加我们的微信群一起交流讨论。
【CEP专题】聊聊插件怎么更新