ES6 Promise 实践 Tips Pt.2 (升级现有代码)

前言

在上一篇 (ES6 Promise 实践 Tips Pt.1) 中, 我们总结了一些在使用ES6 Promise模型进行编程时的注意事项,但多面向基于promise实例方法来编写异步流程的场景。现在我们回过头来关注如何改造现有需要传入回调函数来实现异步执行的功能代码,使其返回promise对象实例。

现状

由于JavaScript中没有线程模型,为了在执行需要耗时较久的任务(比如网络请求和动画)时不阻塞后续逻辑或失去对用户操作的反馈,我们一般的做法是指定一个回调函数,让需要长时间执行的任务在状态发生变化时调用以便获取任务执行结果(比如XMLHttpRequest对象实例上的onreadystatechange方法 ,或正在执行动画的DOM元素上的transitionend事件触发函数)。

例如:

function massiveTask(requirement , successCB, errorCB){
     console.log("begin working on :" + requirement);
     setTimeout(function(){
        try{             
        var result = requirement + "done";
        successCB && successCB(result);
        } catch (err){
        errorCB && errorCB(err);                    
        }
      },3000)
}

// massiveTask 调用见下文

如果我们现在需要把上面的代码改造为支持 ES6 Promise ,我们该怎么做呢?

在Promise对象创建时处理异步逻辑

在大多数介绍 ES6 Promise 的文章中或例子中,都会把业务逻辑封装为Promise构造函数所需要的参数, 例如下面这样

function massiveTask(requirement){
    var p = new Promise(function(resolve,reject){
        console.log("begin working on :" + requirement);
        setTimeout(function(){
          try{        
            var result = requirement + "  done";
            resolve(result);
           }catch (err){
            reject( err);           
           }
        },3000);
    })
    return p;
}

这里特别需要注意的是, Promise 构造函数所需要的参数的类型是一个有两个参数的函数 , 这两个参数又分别是用来传递结果和用来报告错误的的函数.

这种方式生成Promise对象最为直接,不过如果想更完备一些,我们需要考虑

  • 对异常情况的处理
  • 对传统callback回调函数风格的支持.
  • 在不支持Promise环境的环境运行不能报错.

考虑以上几点后的代码会进化成这样:


function massiveTask(requirement ,successCB, errorCB){
    var DummyPromise = function( func ){
        func();
    }
    var P = window.Promise || DummyPromise;
    
    var p = new P(function(resolve,reject){
        console.log("begin working on :" + requirement);
        setTimeout(function(){
        try{        
            var result = requirement + "  done";
            successCB && successCB(result);            
            resolve && resolve(result);
        } catch (err){
            errorCB && errorCB(err);            
            reject && reject(err);
          }
        },3000)
        
    })
    
    return  window.Promise ? p ; null ;
}

在进行了如上改造后, 我们就既能以传统 callback 函数方式调用

massiveTask("earn 100 million",
            function(res){
                console.log("i need report");
                console.log(result) ;
                console.log("good job");                
            },
            function(error){
                console.log("WTF?  you are been fired ");                
            }
           );

也能用 promise 方式调用


var workResult = massiveTask("earn 100 million");

workResult.then(function(result){
    console.log("i need report");
    console.log(result) ;
    console.log("good job");                
}).catch(function(error){
    console.log("WTF?  you are been fired ");
})

如果是新写的代码或需要改造的逻辑比较简单,可以采用上面这种直观,完备的改造方法。 但如果需要改造的代码较为复杂,上面的方法就会暴露改造范围较大且引入多一层嵌套的问题,使得代码Review和后续维护都不易进行. 这种情况下,我们就要考虑用不同的方法来解决问题.

在异步业务开始前创建Promise对象

认真观察上面经过改造过支持Promise的代码,我们能发现要支持Promise实际上最关键的只需要在支持Promise的环境中 (1.)创建并返回Promise对象. (2.)使用resolve和reject来传递结果或报告错误. 我们能不能以某种形式让这部分代码和我们待改造的代码分离呢? 比如如下这样:

function massiveTask(requirement , successCB, errorCB){
    var promiseData = {promise: null,resolver: null,rejecter: null};
    if (!successCB && window.Promise){
      promiseData.promise = new Promise(function (resolve, reject) {
        promiseData.resolver = resolve;
        promiseData.rejecter = reject;
      });
    }
    
    console.log("begin working on: " + requirement);
    setTimeout(function(){
        try{
            var result = requirement + "  done";
            promiseData.promise && promiseData.resolver(result);        
            successCB && successCB(result);
        }catch (err){
            promiseData.promise && promiseData.rejecter( err);        
            errorCB && errorCB(err);            
        }
    },3000)
    
    if ( promiseData.promise ){
        return promiseData.promise;
    }
}

虽然代码的行数稍微有些增长,但我们做到了最大程度的Promise支持与原有业务代码的分离. 在正常业务代码部分仅需在原本需要回调函数执行的位置增加一行利用提前存储起来的resolver来传递执行结果的代码即可。

总结

作为ES6引入新特性中被当前浏览器支持得较好,同时又能被较好的在老旧浏览器中被polyfill补全的重要特性, Promise已经获得了越来越广泛的使用, 如果你想给你手头的老旧代码带来新的活力,不妨现在就开始为她增加ES6 Promise支持。

下面我们做什么

背景

基础产品型前端项目(承载海量用户,要求高可靠性,需要长期维护持续改进)虽然仍然具备前端项目共通的更新方便,发布灵活特点.但由于维护周期较长,业务影响范围大,需要我们相比运营形项目或快速迭代主动试错型项目更加注重质量和优化.

在最近两年的前后端分离改造后,前端项目得以从整体Web项目中独立出来,有了更加自主的工作空间以应接业界不断提升的对交互体验的极致追求.同时,随着模块化,依赖管理,构建技术的引入,前端项目的开发流程和可改进优化空间也有了巨大的变更.很多以往多运用于服务端或客户端的软件开发技术和流程(编译优化,自动化测试,持续集成 etc..)现在也能逐步运用于前端项目.

但同时我们也在不断经历着难于维护或完全没有维护性的项目;虽然有了构建过程,但不完整;在使用了发布平台托管HTML后,托管的版本和代码库中的版本逐渐变得不统一,以至代码库失去完整性.随着项目功能的不断增加及维护时间的延长,项目中的技术债逐渐积累,优化难于下手,偶尔为之的优化由于缺乏可显性和记录,逐渐难于持续.

在新的财年中,出于以上的背景和问题,我们可以在以下几方面争取有所作为.

项目质量改进

过去一年里,我们建立起了项目质量保证Checklist和代码CodeReview机制,今年应该延续,在此之外,为进一步提高项目质量,我们需要对 构建/发布流程;自动化测试;项目工程文档进行改进和完善.

构建/发布流程

  • 更深入的了解学习 Npm,Grunt/Gulp ,规范化npm package描述和Grunt/Gulp脚本,尝试把HTML中引入的库在npm package 依赖中有所体现.
  • 构建过程覆盖完整, 托管于AWP/MT的HTML应该由构建过程生成.
  • 发布过程应该更加自动化,尽量的避免重复的复制粘贴与手工修改.

自动化测试与持续集成

  • 对于现成业务,应该尝试使用BDD对关键业务流程进行覆盖.
  • 对于新业务,时间允许的情况下应该尝试对关键模块编写TDD用例.
  • 尝试把测试用例接入Gitlab持续集成过程,避免测试腐化.

项目工程文档完善

  • 对项目代码组织方式,关键模块需要有记录并及时更新.
  • 对项目运行环境(测试,预发,正式)以及正常访问所需URL Schema需要有记录并及时更新.
  • 对项目配置项及修改入口需要有记录.

可度量的优化

为什么健康手环这么火热,特别在健身人群中,因为这种小工具让健身进展变得可测量并能数据化显示出来.为什么统计,埋点对运营这么重要,因为这能对运营效果进行测量,如果没有这些数据,运营同学就只能凭空猜测运营活动的效果,设计运营方案时也会变得盲目和主观.

类似的,我们要对代码进行优化的第一步也应该是对改进项进行测量(Profiling),并建立起对Profiling数据的记录.多维度的Profiling数据应该成为我们描述自己项目时的关键一项.这些可能的维度有

  • 首次加载时的请求数.
  • 所需下载的静态资源体积.
  • 在可测量环境中(Chrome/PhantomJS)执行完主要业务流程的内存占用.
  • 在可测量环境中执行关键交互时的帧率.

平台化与SDK

平台化是本财年大部门对技术团队的重要目标. 对应到我们团队,我们手头的众多项目天然就是作为支撑无线淘宝及兄弟团队业务的基础平台存在.在这个基础之上,我们没有理由不朝这个目标尽量前进一些.对这个目标的细化,我目前想到如下一些:

  • 优化整个m.taobao.com的访问体验,准备兼容新的移动平台.
  • 尝试从工程上把业务逻辑和UI/UE界面分离, 在改善工程质量的同时让基础业务做好更好服务其他团队和产品的准备.
  • 尝试把分离后的核心业务逻辑模块作为SDK完善和推广.

综上, 项目质量改进,可度量的优化,平台化与SDK 这三项是我认为本财年我们团队在支持好业务方之外值得共同努力的方向.

Learn To FE AutoTest

前言

前端自动测试在强调快速完成速度的环境常常不能获得足够支持,以至大家在这个领域实践较少.但随着业务环境的变化和技术的发展,这种情况会发生变化. 以下是我又一次实践前端自动测试的总结,希望能对大家进行尝试时有所帮助.

TDD/BDD

  • TDD: Test Driven Development ,用测试代码来固化需求,针对测试代码编写实现代码,再重构实现代码的一种开发方法论.

  • BDD: Behaviour Driven Development ,相对TDD侧重模块API,BDD更加注重业务描述,追求基于用户故事,由外及内和让测试用例更加贴近自然语言.

受BDD的影响,出现了越来越接近自然语言的断言库 ,我们可以称他们BDD风格的断言库。虽然是BDD风格,但不妨碍我们用这样的断言库写TDD流程的测试.

前端测试框架的分类和选型参考

基于浏览器的框架

在浏览器里把测试框架和其他JavaScript脚本一并引入。测试结果直接显示在浏览器里。 这种方式比较直观,但不易于接入更为完整的工作流内。jasmine是当前这种类型测试框架里不错的选择.

基于Node环境的框架

在Node环境运行,并以Node模块的方式加载测试用例,测试结果在终端显示. 如果是Node项目或者以Node模块组织代码的前端项目( 通过 browserify 提供给浏览器使用),这种类型的测试框架能最好的为你服务. 该类别中 Mocha 目前较为流行.

基于Server / Client 结构的测试框架

这种方式通过让浏览器接入一个Server,再由这个Server向接入的浏览器下发测试用例,让其同时兼备上面提到的两种类型的特点 (代码运行于浏览器,同时在终端获取测试结果)。这种方式在需要测试不同浏览器的JavaScript执行环境的情景特别适合,但部署比较麻烦。 Google系较旧的jsTestDriver 和伴随Angularjs发展起来的 Karma和支付宝的totorojs 都是以这种思路实现的.

我自己先后学习过jsTestDriver , jasmine 和 mocha.

如果一个项目能从底层就开始以TDD思想作为指导编写测试,根据场景特点,以上提到的框架都是不错的选择。但如果是想通过编写测试对一个已成型项目进行质量保障,我们就要借鉴BDD思想做出新的选择。

基于浏览器行为的测试框架

除了以TDD的思路从每一个模块,每一个函数这样从内及外的去测试我们的代码。我们也能从外到内的去测试我们的项目, 比如给定时间内你的页面是否如预期的渲染出来 ; 比如点击特定元素时,浏览器是否切换到了预期的URL ; 比如加载一份特定mock数据时,页面上是否渲染出了预期的内容.

这时我们就可以利用基于浏览器行为的框架来对项目进行测试.

selenium是一套控制浏览器的强大工具, 我们可以利用它来进行浏览器行为测试。但selenium的设计意图是一套提供给专业测试人员的可视化IDE , 而不仅仅是能方便集成于开发工作流的测试框架,所以用起来会有一些不便。

幸好有更好的选择 , PhantomJSCasperJS

PhantomJS是剥离了显示部分的Webkit(headless webkit ), 和其他基于WebKit的浏览器不同, PhantomJS没有提供哪怕最简单的UI界面,但提供了一套控制WebKit的 API . 我们可以利用它的API加载任意URL,然后像在Chrome控制台中一样对页面进行探知或执行代码获取结果.

CasperJS是在PhantomJS之上的抽象层,它包裹了PhantomJS的底层方法,以更高级,更方便开发者使用的API暴露出来. 通过CasperJS我们可以更直观的编写浏览器控制代码,触发事件,探测页面DOM结构。

好上加好的是, 我们还能结合CasperJS 和 Mocha 。利用mocha-casperjs 我们可以用Mocha作为框架,定义我们项目在特定行为发生时应该具备的表现. 然后在用例里以CasperJS来重现特定行为,并判断表现是否符合预期。

例如以下这个例子

 it('Test Demo From H5 Detail', function() {
      casper.start(TEST_TARGET_URL);    //打开特定URL
      return casper.waitFor(function() {  //直到传入函数返回True
        return this.exists('#J_detail_main');
      }, function() {
        var itemTitle;
        itemTitle = this.fetchText('.dtif-h');
        return expect(itemTitle).to.be.a('string');
      }, function() {
        return this.echo('页面未能加载出商品信息', 'ERROR');
      }, 1000); // 1s 超时阈值
    });

这是一个mocha测试用例,其中测试了在访问TEST_TARGET_URL时,页面在1秒之内是否已经渲染出了”#J_detail_main”元素,同时”.dtif-h”元素是否存在并包含文本内容。

另一个例子

it('Another Test Demo From H5 Detail', function() {
      casper.start(TEST_TARGET_URL);
    return casper.waitFor(function() {  
        return this.exists('#J_detail_main');
      }, function() {      
        this.mouseEvent('click', '.dt-sku'); //模拟点击
        return casper.wait(700, function() {
          var targetClass;
          expect(this.visible('.dt-sku')).to.be["false"];
          expect(this.visible('#J_detail_skum')).to.be["true"];
          targetClass = this.getElementAttribute('.dgsc-pc i:nth-child(1)', 'class');
          expect(targetClass).to.eql('sel');
        });
 });

这个例子中,我们测试了点击页面元素”.dt-sku”后,页面在700毫秒内是否已经符合我们一系列的判断。

得益于Mocha在Node环境中非常好的可集成性, 我们可以把Mocha和现有的 Grunt / Gulp 构建流程整合在一起,在每一次构建,准备发布时为我们的项目增加一重或许能避免重大事故的保障。

ES6 Promise 实践 Tips Pt.1

Promises是一种成熟的异步编程模型, 它把程序中的特定后续状态抽象为Promise对象实例,让我们可以方便的对该未来状态进行编程. 在JavaScript世界中,Promises模型有着众多的实现. 虽然是同样或相似的思想,但每一种实现都会有一套自己的API或术语,这种分裂状态让我们对Promises的学习增添了不少困扰.

已经被各浏览器广泛实现的ECMAScript6中对Promise模型进行了精简 , 同时ES6 Promise模型也能在旧版本浏览器中被较好的模拟. 无论为了改善自己的异步编程技巧,还是使用基于ES6 Promise的新Web API(WebFont ,WebMidi)或是释放ES6 Generator的威力, 学习ES6 Promise都是必须的. ES6 Promise API大家可以通过MDNes6.ruanyifeng.com学习,我在下面会整理一些自己学习/使用Promise过程中的最佳实践.

NO.1增强异常处理意识

Promise模型消解“callback hall”的功用已经被之前众多介绍Promise的文章进行了充分强调,这里不继续累述.在实践中除了该项好处之外,Promise模型在 优化复杂控制流程,增强代码健壮性 这方面的作用对我们强调高可用性的电商系统也有着非常大的帮助. 从另一个角度看, ES6 Promise在出现异常时不会像jQuery Deferred 模型一样立即抛出异常,而是把Promise实例置为待Reject Handle状态,并向后传递.jQuery的策略有其优势,但这点上也提醒我们在使用ES6 Promise模型时应该更积极主动的思考异常策略,而不是等待最后调试时控制台抛出的大红叉.

No.2拆解 then 链

很多大牛在展示自己的Promise代码时喜欢用类似 .then().then().then().then()…… 这样的长长的then链来突出链式调用. 当其中每一步所需的业务逻辑简单或类似时,这种写法有其优势和美感.但当你的业务流程更为复杂或没有重复性时,这样的写法只会让你才出 “callback hall” 又入 “then hall”. ES6 Promise 模型中,无论是使用new或Promise.resolve ,Promise.reject构造实例,还是then或catch方法,你获得的都是另一个Promise对象实例.在编程时,你可以把每一次获得的Promise对象实例想象成未来的一个状态, 为每一个状态起一个能描述该状态的名字作为该Promise对象实例的变量名. 这样,你的Promise代码就能更清晰整洁,同时方便后续更细粒度的封装.

No.3使用catch来分离 resolve handler 和 reject handler

ES6 Promise的API几乎是所用Promise模型中最简单的( 构造函数之外,只有四个静态方法resolve,reject,all,race和两个实例方法then,catch ) ,而其中的catch方法实际上是 then(undefined, rejectHandlerFunc) 调用的语法糖 catch(func)的作用相等于在当前Promise对象实例上追加一次.then(undefined, func)调用 . 如此设计,可见API设计者对.catch 调用形式的重视.

No.4尽量为拆解后的每一步流程建立独立的异常处理机制

基于以上我们学到的then链拆解和catch使用技巧,我们可以如此这般来整理自己的ES6 Promise代码:

var domReadyPromise = new Promise(function(resolve,reject){
    $(document).ready(function() {
        resolve();
    })
});

var requestPromise  = domReadyPromise.then(function(){
    return lib.mtop.request({ api: "mtop.xxx", v: "1.0",data:"..."});
}).catch(function(e){
    console.error(e);
    throw new Error("mtop 请求错误");
});

var parserPromise = requestPromise.then(function(reqRes) {
    return parse(reqResres);
}).catch(function(e){
    console.error(e);
    throw new Error("parser error");
});

这样,我们既能让异步处理流变得清晰明了,又有了改善代码健壮性的机会.

No.5尝试从异常状态中恢复

在ES6 Promise模型中, 异常会让then方法把Promise实例置为待RejectHandle处理,并向后传递.就是说整个then链或拆解后的then链中的每次catch调用都有机会处理之前任意步骤时出现的异常. 同时,如任意 .catch(rejectHandler) 或 .then(undefined, rejectHandler) 中没有把截获的异常继续抛出或抛出新的异常. 后续的promise链路将恢复到正常待ResolveHandler处理的状态. 这样,就为我们提供了从异常中恢复的便捷途径:


var requestPromise  = domReadyPromise.then(function(){
    return lib.mtop.request({ api: "mtop.xxx", v: "1.0",data:"..."});
}).catch(function(e){
    console.error("mtop 请求错误");
    console.error(e);
    var defaultData = {.....};
    return defaultData;
});

var parserPromise = requestPromise.then(....);

如上面的代码,mtop请求时出现了错误,这时我们返回了默认数据,后续的流程将不会受mtop请求失败的影响继续进行.

其他

  • 我目前在分析中的一个ES6 Promise polyfill

http://g.alicdn.com/mtb/lab-zikuan/0.0.7/promise/es6-promise.debug.js

  • 支持ES6 Promise的mtop (仅供测试,下个版本的lib-mtop将正式支持ES6 Promise )

http://g.alicdn.com/mtb/lab-zikuan/0.0.6/mtop/mtop_es6promise.js

参考

夏天到了

如果能持续表达

如果不对别人产生打扰

如果不对内容质量产生要求

以自己最舒服的方式记录,总结

不追求反馈也不拒绝表达

看看会发生什么?

记录片"穹顶之下"笔记

看了柴静的纪录片 穹顶之下, 以下是我的笔记

煤炭

  • 当前中国燃烧的煤炭比世界上其他国家加起来还多
  • 经过”洗煤”能减少燃煤污染,但得不到强制执行
  • 利润压力已经迫使一些城市使用对环境有更大破坏的”褐煤”来供暖/供能

杭州

  • 杭州全年雾霾 200 天以上 ,也是严重污染的城市
  • 人均汽车拥有比例高&市场只供应低环保标准的车用燃油

北京

  • 凌晨是每天污染的峰值时段 ,原因是没有排放限制措施的运输车辆大量进入城市
  • 就算在北京,规定汽车排放标准的法律也不能得到执行, 原因是”执法主体不明”

石化行业

  • 我国油品环保标准是由石化行业自己制定的, 出于行业利益保护的原因,石化行业没有动机提高油品环保标准
  • 当前不光环保部门不能遏制石化行业,发改委对石化行业也无能为力

城市化

  • 投资导向的发展策略
  • “城市化过剩”

可行的监督方式与场景

  • 环保举报热线 12369
  • 加油站的呛人味道(原因是没有按规安装或维护油气回收设备)
  • 工地没有对裸露沙土进行覆盖
  • 餐厅没有安装油烟过滤设备

虚拟机镜像文件同步

过去几年来,我的主力工作环境一直是Linux VirtualBox虚拟机。过去一段时间,工作的ThinkPad X1笔记本,家里的MacBook Air笔记本和iMac中都装了虚拟机,不时在这三台电脑间切换. 我不愿意在这三个环境下做重复的环境搭建及软件安装工作。

春节期间这个问题变的明显,同时也有了多一些的空闲时间. 于是思考如何解决该问题.

期间的试错过程不在这里复述,最终的方案如下:

  • 买了移动硬盘盒把空闲的SSD硬盘利用了起来,在其中放置了虚拟机硬盘镜像,移动硬盘以exFAT方式格式化,以便能放置大体积的镜像文件,同时支持在Windows/Mac下进行写入.

  • 两个常用的工作电脑(Thinpad X1, iMac)在本机放置虚拟机镜像文件。一段时间的工作结束后,使用rsync同步到移动硬盘上。

rsync  --human-readable --partial --inplace --no-whole-file --progress   SRC  DEST
  • 不常用来工作的MacBook air笔记本在需要虚拟机环境时,就直接连接移动硬盘,使用移动硬盘上的镜像文件.

一些额外的信息

  • 测试发现 差量/块级 同步(rsync –no-whole-file)相对直接复制(rsync –whole-file) 没有明显的速度优势, 不过从磁盘IO监控来看,写入会少很多.

  • 虽然Mac环境能通过NTFS-3G支持NTFS文件系统的写入,但发现速度相对原生支持的exFat,速度下降明显。

  • 硬盘盒换过一次,第一次买的会出现传输过程中失去响应的问题,后面换了个更“高档”的,能够稳定运行了.

  • 在Windows下用的rsync是这个cwrsync

  • 除了rsync之外,还考虑了基于unison的方案,unison和rsync一样,支持差量同步算法,但更适合用于整个工作目录的双向同步(merge 两边的区别)

day7 练习

最近两周为了练《涩》,逐一弥补了之前没有掌握的几个基础技术

  • 分解和弦
  • 用拨片的情况下用手指拨弦
  • 分解和弦与扫弦切换

伴奏部分原来预计一周练习时间,后来几乎用了近三周时间.

这首歌前奏和高潮对比非常明显,以至高潮的伴奏练好了后,花了不少精力练习同步节拍才能唱进去.

最后录出来后发现为了和伴奏同步节奏,人声中的小节重音过于明显,不好听了。

每日听歌不断但唱歌越来越少,期待还能唱好听了,这是不现实的.

把”唱歌“当作个技术去看待,还是得”练习“

Unbroken

去新的电影院了看《坚不可摧》(Unbroken) unbroken 新导演(安吉丽娜·朱莉)的好莱坞主旋律电影.

表达对自身极限的挑战与逆境中坚持

观影前所期待的”战后宽恕”主题,仅用最后的一屏字幕带过

对于新导演,这么做是明智的

居中与左对齐

这几天在做一个小页面中需要实现这样一种排版

A, B两个区域 ,A页面居中, B左端对齐A左 端

原本我是让A text-align:center; 后通过Javascript让B对齐A.后面寻思不依赖JavaScript的方法,陷入 flexbox 中各种尝试一番,无果.

经过和同事讨论,学习到了仅使用CSS解决这个问题的排版方法(当A的宽度超过B的情况下):

  • 为A,B建立两级父节点, 在最外一层中设置 text-align:center; 在近一层父节点设置 display:inline-block;
  • 为B设置 text-align:left;

这种实现的关键是利用了inline-block “宽度收缩” 的特性,使得包含A,B的最近一层父节点宽度收缩为A的宽度.