店铺 KISSY 版本大乱斗
作者: 发布于:

店铺 KISSY 版本大乱斗

可能你不知道,从去年(或者前年?)某个时候起,店铺开始着手将 KISSY 版本从 1.3 升级到 1.4, 期间因为业务或团队变动已经换了两波前端,然而还是没有升级彻底,即部分店铺是 1.4, 另一部分还停留在 1.3 上。不彻底的升级意味着根本就没有升级,因为所有的代码要同时兼容两个版本。

我从 15 年 10 月接手店铺业务,前不久学长找我商量店铺升级 KISSY 6.0 的事情,那一刻我的内心是崩溃的、拒绝的、惨不忍睹的……然而痛苦归痛苦,坑终究是要填的,跟各位大大讨论方案后做了一些尝试,最后终于在上周把这个问题解决掉了。本文就来向大家简单介绍下这段历史以及最终的解决方案,最后附带个人对于开放脚本的一些思考。

TL;DR

对于第三方代码,安全问题自不用去提,在此基础上,对于官方代码和三方代码一定要做技术方案的隔离,否则当未来官方代码需要升级技术方案的时候就会步履维艰。

背景

在店铺体系里,装修系统是很重要的一部分,在整个体系建立之初,为了保证卖家可以在装修过程中有更多的选择,店铺开放了其模块体系:第三方开发者可以开发店铺的模块,然后上架到装修市场,然后卖家需要的时候去市场里订购。(第三方开放者开发的模块下文统一称为设计师模块)

开放模块意外着第三方开发者可以写 HTML, 写 CSS, 而且可以写 JavaScript, 在当时(2010?)的技术条件下,主要使用了两个库:

  • Caja: 负责三方代码的安全问题,出自于谷歌
  • KISSY 1.3: 作用跟之于我们的业务类似,工具库

KISSY 1.3 自不必多说,Caja 是什么呢,借用其官方介绍:

Caja is a tool for safely embedding third party HTML, CSS and JavaScript in your website.

值得一提的是,经过这套开发体系的编译,最终我们存储的设计师代码是这样的:

{  ___.loadModule({      
  'instantiate': function (___, IMPORTS___) {        
    return ___.prepareModule({            
      'instantiate': function (___, IMPORTS___) {
        var dis___ = IMPORTS___;              
        var moduleResult___, x0___, x1___, x2___, x3___, x4___, x5___,              
              x6___, x7___, x8___, x9___, x10___, x11___, x12___, x13___,              
              x14___, x15___, x16___, x17___, x18___, x19___, x20___, x21___,              
              x22___, x23___, x24___, x25___, x26___, x27___, x28___, x29___,              
              x30___, x31___, x32___, x33___, x34___, x35___, x36___, x37___,              
              x38___, x39___, x40___, x41___, x42___, x43___;              
        moduleResult___ = ___.NO_RESULT;              
        try {                
          {                  
            {                    
              function log(x) {                      
                var x0___;
                // ...

压缩的、混淆的,同时更是不可控的。

另一方面,基于 Caja 我们定制了自己的 shop-caja, 其中在初始化设计师模块时会对 KISSY 做一些改写,负责这部分的代码是 Caja-adapter,因此,我猜测,选择这套方案的同学一定是考虑到未来 KISSY 版本的升级问题,而对应的策略就是在 Caja-adapter 这一层做文章:无论是 KISSY 哪个版本,在 adapter 里总会让其 API 兼容 KISSY 1.3.

以上只是个人猜测,因为人员的变动这些猜测很难去考证或者考证了也没什么意义。最终的结果就是:前任同学在接手了进行一半的升级工作之后,试图通过这样方式去实现,然而在一年之后还是把这个半成品交接到我这个现任手中。

多版本引发的问题

因为上文说的 KISSY 版本问题,引发了一些不良后果:

1. 凌晨的惊悚

大促活动有个超级顶通,各个业务方都要引入,店铺当然也不例外,之前作为日常需求接入,因为顶通开起来就是一个很简单的展示区块,因此和开发在预发上看了几个店铺没有什么问题于是就推到预发,然后这个顶通是凌晨才会推送上线的,因此这时候线上也自然不会有任何感知。

直到凌晨一点左右,之前的缓存基本失效,预发环境超级顶通生效,使用 KISSY 1.3 的店铺脚本无法执行,很多模块初始化失败,排查之后才发现超级顶通里没有兼容 1.3 的语法,其中最为严重的一点是 use io 模块失效,这里不得不提两个点:

  1. KISSY 臭名昭著的核心模块改名事件:ajax(1.3) -> io(1.4)
  1. KISSY.use 失败之后会导致页面其他依赖 KISSY 的脚本都无法执行

然后开发同学紧急下掉了超级顶通,我当时惊出了一身冷汗,然而负责超级顶通的然姐倒是很淡定 LOL...

所以出现这个问题可能是因为很多因素:测试的时候没有回归 1.3 的店铺、超级顶通没有兼容 1.3 语法、KISSY.use 的机制太过脆弱……但是原罪还是店铺里竟然还存在 KISSY 1.3 版本这一事实。

2. 难以升级的开发模式

店铺目前的整个开发模式很落后 + 繁琐:KISSY 1.3 的语法,自写的打包脚本 Gruntfile, 异常落后的模块组织体系……自我然不会将这些都归罪于 KISSY 版本,但是如果不解决 KISSY 版本的问题,我们未来无法升级到 KISSY 6.0, 无法升级到 cake, 无法重新梳理店铺的模块机制,无法切换到新版的发布系统,接下来就是一步一步的老死?NO!

3. 时不时的线上问题

因为两种版本同时存在,导致了线上的情况会变复杂,进而就是时不时的线上问题。是的,每次线上问题花两到三个小时都可以解决,然而这些付出几乎是毫无意义的,每次排查类似的线上问题我都会很头疼:

  • 技术提升了吗?No
  • 会更加了解业务吗?Maybe
  • 会帮助到业务成长吗?Very little
  • 所以,有意义吗?Who knows……

所以,基于这些原因,必须要扛起担子解决问题,接着我们就来聊聊方案吧。

解决方案的探索

在这个问题上,大致的解决思路有两个:

1. 利用 caja-adapter 保证 KISSY 代码兼容 1.3 的语法

这个方案,上文已经简单提过,并且店铺前任前端同学之前也有做一些尝试,但是因为 1.4 跟 1.3 的 anim 模块差别太大,一直没有进展,最终随着人员变动无疾而终。简单总结下优缺点:

  • 优点:比较优雅,始终只会加载一份 KISSY
  • 缺点:
  • 可持续升级难度太大,1.4 到 1.3 的语法兼容已经停顿了一年多,更不用提 6.0 到 1.3
  • 方案相对复杂,需要熟悉 shop-caja 的整个机制,然而熟悉了还不一定能搞定

2. 隔离设计师模块和官方代码所依赖的 KISSY 版本

这个方案是之前跟@展炎、@释然、@乔福、@林谦一起讨论出来的结果,大体思路:有设计师模块的店铺引入两份 KISSY,一份供官方代码依赖,另一份供 shop-caja 和设计师模块使用。同样优缺点:

  • 优点:一劳永逸,未来店铺官方代码做技术升级时基本不会影响到设计师模块
  • 缺点:稍稍有点粗暴,有设计师模块的页面会加载两份 KISSY 以及用到的核心模块,多多少少会影响性能

对比了两种方案之后,最终选择了方案 2, 原因呢:一方面方案 1 已经有同学尝试未果,另一方面自身没有太多的精力去研究 shop-caja 以及 KISSY 的核心代码。 确定方案之后就是 step by step 的行动啦~

Just Do IT

整个方案实施起来并没有花费太多精力,大体的步骤如下:

  1. 基于 KISSY 1.3 迁出一个 kissy-caja 的仓库,其中包括 KISSY 所有的核心模块
  1. 为了保证两个 KISSY 互不影响,将 kissy-caja 里所有的 KISSY 全局变量替换为 KISSY_CAJA
  1. 同时将 shop-caja 里所有用到 KISSY 的地方替换为 KISSY_CAJA, 其中较为重要的是初始化设计师模块的地方:

prepareEnv.run({
  // KISSY 就是从这里传进设计师模块脚本里的
  KISSY: exposed_kissy,
  GS: tameGlobalService,
  onerror: onerror
 }, function (re, a) {
});

  1. 在页面中 shop-caja 之前引入 kissy-caja
  1. 测试:或许这一步花费时间最多了吧,找了 N 多个有设计师的模块在预发上测试,中间遇到了一些小问题下文会提及
  1. 线上验证:发布完成后,观察了 jstracker 的异常数据,每 PV 报错率跟之前差别不大,基本稳定在 10% 左右,基本确保问题解决。

过程中遇到问题

比较欣慰的是用了这个方案之后,预发上只出现了一个问题:jsonp 方法 callback 同名导致报错。KISSY 中 jsonp 的回调名是通过 jsonp + S.guid() 来生成的,而这个 guid 是挂载在 KISSY 下而非全局变量下,因此两个版本的 KISSY 很大几率会生成相同的 guid, 而多个 jsonp 回调名相同之后就会导致后面执行的方法出错,解决方案也很简单,因为 guid 是从 0 开始累加的,所以我直接将 kissy-caja 的 guid 基准值从 0 改为 12306, 别问我为什么是 12306, 任性!

思考

最后,虽然我对于开放第三方代码没有做过研究,但是因为这件事对开放三方代码有一点自己的认识:

对于第三方代码,安全问题自不用去提,在此基础上,对于官方代码和三方代码一定要做技术方案的隔离,否则当未来官方代码需要升级技术方案的时候就会步履维艰。

比如说,如果当时店铺的设计师模块直接基于 jQuery 某个指定版本去开发,现在也就不会遇到这些问题了,当然这只是个马后炮,希望对类似应用会有帮助吧。