本博客同步自我的GitHub博客

本来,做这么个东西也没太大记录的必要,但是通过这个组件(其实就是一段小脚本)的开发,我有了一些别的感悟。考虑到给未来留个回忆,姑且写上一写。

背景

最初接到这个东西的需求时,我大概在做门店频道项目中的一个页面,那是我进入苏宁以后第一个参与的项目。8月的某天,老大突然喊了一嗓子,把我叫到他那里去捡肥皂,哦不是,布置任务。任务的需求是这样的,苏宁易购作为一家电商网站,运营部门总是会搞各种各样的促销活动,一天好几个都是有可能的。既然是活动,那么活动页面是免不了的,猛戳如下链接:
苏宁活动页

易购有这么多的活动页,网购研发中心的前端就这么多,当然不会去做这些繁琐的工作。生产这些页面的任务一般都是由运营部门自己的前端(说是前端,更多时候可能就是设计来兼任一下)来完成。他们的完成方式也很传统,通常拿着视觉给出的视觉稿大图,三下五除二从上到下垂直切成若干小块,用几个img标签标签包裹,往页面里一塞,再用Dreamweaver这样的工具把页面上的链接用map一个个框出来,填上url,搞定。一个做视觉的,前端造诣能这样已经不错了,现在的前端那么缺人。。。

然而这种开发方式虽然相当快,但有一个致命的缺点,那就是页面中所有商品的价格都是以图片方式呈现,电商之间的价格战如此激烈,商品价格的频繁变更是免不了的。一旦后台商品的价格变了,活动页上价格图片因为难以快速更新,就会出现用户在活动页上看到的价格和实时后台的价格不一致的情况,最终导致消费者的投诉,损害公司的信誉,这是个严重的问题(真的是很严重,我不止一次看到某些部门负责人因为一些这样的“价格欺诈”而遭到处罚甚至卷铺盖走人了)。

所以,运营那边提出的需求也很好猜了,由研发中心的前端开发部,提供页面价格实时更新的组件,使得每次刷新页面时异步从后台取得最新的价格,提供了脚本后,运营部门再做类似的活动页时,按照一定的规则编写HTML,引入我们提供的脚本,页面中所有用来填充价格的标签都会由JS异步填充价格。现有的资源只有后台提供的查询价格API一枚。

说实话在接到这个项目之前我一直认为这些活动页的价格本来就应该异步更新的,可事实上不是(但似乎也不是所有的活动页都有这个问题,有的页面价格就是最新的,可能是因为页面来源部门不同?)。

例1 京东

拿京东来说吧,他们的活动页很多也是以图片方式呈现的,就像这样:

这也是可以理解的,一方面可能是前端资源不够,另一方面,活动页的设计本来就力求能吸引消费者的眼球,价格数字的字体通常都会又大又鲜艳,甚至可能使用很多变型后的字体,通过文本方式来实现显然也不现实。

那么京东采用了文本方式展现价格的活动页面是怎么做的呢?随便找到一个,可以发现这些页面用来现实价格的标签上都加上了商品编码之类的属性,就像这样:

<span class="jdNum" jshop="price" jdprice="1175126">2.90</span>

标签中jshopjdprice两个属性用作取价格脚本的hook,在页面中异步填充了script标签来发起请求,如下图:

在Chrome Developer Tools中看到的请求返回如下:

从请求参数和返回的数据结构也可以看得出,异步的ajax请求使用了jsonp的方式,其回调函数是挂载在window全局环境下的一个函数,which means,在控制台中直接打函数名callBackPriceService就可以看到函数内容了。

就这样把函数添加为全局变量有什么道理,又有什么问题,可以思考一下。

同样,找到这个函数所在的脚本也很方便,我观察了一下京东活动页的写法,应该是没有用到模块化管理脚本,直接将script标签插在<body>中同步阻塞地加载,以加时间戳参数的方式区分版本,还是比较原始的。不过看得出,京东还是有一套自己的方法库的,总算不是太粗暴,每个页面重复实现一遍代码。至于更新价格的具体逻辑实现就不看了,我在写代码的时候还没有看过京东的代码。

例2 天猫

作为国内B2C网站的领头羊,天猫的实现也是值得研究一下的,其实我很不喜欢扒阿里系网站的代码,他们的JS实现封装完全自成一派,高度密集,很难直接看懂。

当然,如前文所说,切大图框map的方式在天猫的活动页中同样存在,图就不贴了,各位可以自己去看。我们要找的是异步更新的实现。但悲剧的是,没找到,整个文档从后端吐出时价格数据就已经渲染好了,没有异步更新这一步的存在。具体的原因么,略过不表,待我深究。

咱的实现

别人家的实现看过了,下面就轮到自己了。同样的业务,对不同的公司来说实现方式也是不同的,就拿这个需求来说,首要的制约因素就是后台接口的规定。易购后台同步价格的接口的参数要求是这
样的(简化版):

必填字段: skuids,cityId
非必填字段: districtId,clientType,callFrom,callback
样例: http://www.suning.com/emall/getPrice?callback=fn38515&skuids=923448257||2|&clientType=1&cityId=9173&callback=fn38515

很奇葩的商品编码skuids,服务器会做一个类似split('|')的操作,取出不同的状态位进行查询。另外,受后台负载能力的限制,接口的同次请求数量会受到限制,一次不能请求太多,最多一次16个。注意,这只是理论值,当网站进行大促时,请求量可能特别大,因此,实际的单次请求数会更低,一般是8个或4个,最低的时候1个也有可能,花费这么多建立请求的开销无疑会降低页面的性能,但另一方面保证后台不会崩溃导致无价格响应。在服务器能力有限的情况下,一些取舍也是需要的,理想化的情况不会总是存在,尤其是在技术能力不是特别强的公司或是服务器性能不够高的创业公司。

言归正传,我们采用和京东一样的方法,在价格标签上加上供脚本识别的hook:

<em class="js-unit-price" data-sku="123440432||2|">299</em>

这样可以取得页面中所有的商品编码,存入一个数组,下面要用这个很长的数组去拼接参数字符串。不要忘了单次最高请求数,因此,要先将这个长数组转换成二维数组,每个子数组的最大长度不超过单次最高请求数,将转换过程封装成一个方法:

function sliceArray(arr, size) {
    var len = parseInt(arr.length / size, 10);
    var remain = arr.length % size;
    var flag = remain ? len : len - 1;
    var newArr = [];
    var temp = [], start, end;

    for (var i = 0; i <= flag; i++) {
        start = size * i;
        end = (i < len) ? (i + 1) * size : i * size + remain;
        temp = arr.slice(start, end);
        newArr.push(temp);
    }
    return newArr;
}

每个子数组就可以转化为合适长度的字符串:

skuIdsParam = skuIds[currentIndex].join();

生成参数的工具函数准备好后,可以发起请求了:

$.ajax({
    url: 'http://www.suning.com/emall/getPrice?callback='+ time +'&skuids=' + skuIdsParam + '&clientType=1&cityId=' + cityId,
    dataType: 'jsonp',
    jsonpCallback: time,
    error : function(XMLHttpRequest, textStatus, errorThrown) {
    },
    success: function(data) {
        priceCallback(data);
        currentIndex++;
        setTimeout(newRequest, 0);
    }
});

接下来是回调函数,用于将取到的数据填入页面。其中,商品的原价和促销价会封装在同一个对象的不同字段中,要对促销价的存在进行判断,保证呈现的价格是最低的那个:

function priceCallback(data, skuArr){
    var priceList = data.price;
    if (priceList && priceList.length > 0) {
        for (var i = 0, len = priceList.length; i < len; i++) {
            var skuId = priceList[i].inputkey;
            var price = priceList[i].promotionPrice ? priceList[i].promotionPrice : priceList[i].price;
            var priceWrap = $(".js-unit-price[data-sku='" + skuId + "']");
            if (price > 0) {
                priceWrap.html(price);
            }
        }
    }
}

在前后端工作完全正常无错误的情况下,需求是实现了,然而奇葩的事情是不断发生的。这个接口的内部实现是怎么样我不知道,估计是一团糟,因为我不止一次的发现商品有取不到价格的情况,这价格一取不到,返回的json数据里相关商品的对象就没了,就像这样:

// 这是返回的jsonp对象
fn60710(
    {
        cityId: "9173",
        price: [
            {
            cityId: "9173",
            depot: "X",
            inputParameter: {
                districtId: "",
                itemInputData: "923448243||2|"
            },
            inputkey: "923448243||2|",
            partNumber: "000000000923448243",
            price: 1468,
            priceType: "0",
            promotionPrice: 1468,
            salesOrg: "X",
            vendor: "X"
            }
        ]
    }
    // 这里可能应该还有一个对象,但是丢失了
)

为此,还需要在遍历数据后再将没有填入价格的标签通通填入“暂无报价”这样的文字。为了知道原来到底有哪些商品,还需要将原来用作拼接参数的数组传入回调函数作为参考对象,修改$.ajax()中回调函数的调用语句:

priceCallback(data, skuIds[currentIndex]);

在回调函数中添加代码:

for (var i = 0, len = skuArr.length; i < len; i++ ) {
    var priceWrap = $(".js-unit-price[data-sku='" + skuArr[i] + "']");
    if (!priceWrap.html()) {
        priceWrap.html('<b style="font-size: 16px; white-space: nowrap;">暂无报价</b>');
    }
}

至此,需求基本上实现了。当然,仅仅是实现而已,还远远谈不上完美。随便举一例,当单次请求数限制为一的时候,划分二维数组就没有意义了,回调函数中的两个遍历同样有损性能。再举个例子,样式问题,价格标签的样式都是为了数字的视觉而写的,通常字号比较大,一旦填入“暂无报价”这样的中文,视觉效果会非常不美观,甚至出现文字溢出的情况。如果在样式表中不特别加上针对中文的样式的话,只能将样式强加在JS中,这种方法好不好,不用多说。问题这么多,但具体的优化工作留待以后实现吧,项目多到忙不过来了。

一点感悟

从大三开始不断在各个互联网公司实习起,遇到的需求也是不少了。要说实现,最后都能实现;而要说让自己满意的实现有几个,几乎一个都没有。如果只说前端,各种浏览器的兼容问题已经老大难了,倒也不算什么。更糟糕的是,在各种不同的业务场景下,前端的业务往往会受到后台开发极大的制约,比如更新价格这个需求,为了满足接口奇葩的参数设定,为了保证服务器的负载能力不被压垮,看得见的恶心事全部落在了前端身上。而页面一旦出了问题,被问责的又往往是前端,我不止一次的对问题进行排查后,给予的答复是:这是后台问题,与前端无关。

不可否认,国内前端的发展虽然遭受过不少质疑,前端这项工作到底是什么也少有人了解,但总体上仍然称得上是势头迅猛。但实际上,我认为前端的位置在很多公司,尤其是对前端不够重视的公司,还比较尴尬,没有足够的资源提供,甚至职级晋升规范都不完备。有的时候,只有在脱离了无尽的业务需求,专心做一些自给自足的项目时,才能有些快乐自由的感觉。但前端技术不是象牙塔,不能脱离实际需求而存在。只能说各自努力,去推动前端的环境越变越好吧。

下周末去杭州参加D2,希望能认识各路朋友,我的微博@吴双Orange,欢迎互粉


classicemi
1.2k 声望122 粉丝

你又来看我了