大家好,我是Raymond,我写了很多糟糕的代码。好吧,其实并没有很糟糕,只是我没有遵循所谓的“最佳实践”罢了。我敢打赌看这篇文章的很多人也都没有遵循最佳实践。在这篇文章中,我将谈一谈在最近的一个项目中,我使用了一些简单的工具帮我完成了自己非常满意的代码。下面我就跟大家分享这个故事。
故事背景
假期中我尽量让自己远离工作,甚至连电脑都不去看一眼,但最后彻底失败了。
当时我饶有兴致正准备玩一轮电子游戏(见笑了,是手眼协调训练的游戏),忽然有人跟我分享了一个让人有点小激动的消息——Star Wars API发布了。即使它并不是“官方”发布的,这个“非官方”的API提供了抓取人物角色、电影、星际飞船、交通工具、物种、星球等内容的方法。Star Wars API提供免费服务,没有认证要求。它不提供搜索功能,对于一项免费服务来说,它已经很好了。如果你了解我的话,就知道我对跟Star Wars相关的东西是很着迷的。
我一时心血来潮,快速写了一个JavaScript库来集成这个API。最简单的,你可以使用它来抓取某一类型资源的全部内容:
1 2 3 4 5 6 7 |
//get all starships swapiModule.getStarships(function(data) { console.log("Result of getStarships", data); }); |
或者抓取一个特定的项目:
1 2 3 4 5 6 7 |
//get one starship (assumes 2 works) swapiModule.getStarship(2,function(data) { console.log("Result of getStarship/2", data); }); |
实际的包装器是一个js文件,我也写了相应的test.html,并把它们放到了GitHub:https://github.com/cfjedimaster/SWAPI-Wrapper/tree/v1.0.(注意:该链接指向项目的原始版本,最新版本在这里https://github.com/cfjedimaster/SWAPI-Wrapper )。这种打发时间的方式很有意思,坦诚来讲,每写一行JavaScript代码,我就觉得自己技能在(慢慢地)提高。
但是接下来状况就出现了,有个细小的声音在我脑袋里喋喋不休:我是不是还可以把代码写的更好一点?我应该写一些测试单元,不是吗?我怎么忘了加上精简的版本?这些事情我知道不是必须要做的,但是没有做到自己最好的水平,我对此感到内疚(也并没有内疚到立即去看代码,毕竟现在还是假期嘛!)。
我被这纠缠了好几天,于是开始在头脑里思索能改进项目的事情,让它更符合最佳实践。我这里列出的可能会跟你的大有不同,但是它们的确使这个项目有了很大的改进。
- 在写JavaScript的时候,我发现一些代码反复出现并且可以进行优化。但当时我专注于代码功能的实现,有意地忽略了这些。提前进行优化多少让我有点不乐意。然而现在代码发布了,我觉得此时回头再对它做一些改进,这还是很合理的。
- 显然,在优化之前做一下单元测试或许更合理些。由于项目依赖远程服务,做测试可能会有一些问题,但还是可以假设远程服务运行顺利,做一下测试,这样有总比没有强。另外,如果先写这些测试,我就能查看代码的变化,从而确保自己没有破坏掉什么事情。
- 我是JSHint的拥趸,我喜欢把代码放到JSHint里面跑一遍,确保代码能通过测试。
- 我也希望发行一个精简版本的库。老实讲,我以前没有做过代码压缩,但是直觉告诉我肯定有这么一个脚本,我只需在命令行里跑一下就可完成代码压缩 了。
- 最后一点,我确信我能处理好单元测试,JSHint检查,以及使用Grunt或Gulp的自动化工具来完成所有步骤。
最终我将拥有一个让自己倍感自信的项目,这个项目能更好地服务我的最终用户,我将把这个项目从Jar Jar一样的东西变为Jedi一样(译者注:Jar Jar是星战系列中的一个二货角色,Jedi指星战系列中的绝地武士)。在这篇文章中,我将回顾这其中的每一步,并且描述我是怎么做来改进我的项目的。第一条所讲的代码优化,由于它是最含糊也是最开放的,我将放在最后再讲。
增加单元测试
现在是2015年,我假设大家都知道什么是单元测试。万一你不知道,那么最简单的方法是把它们看成能让你的代码正常运行的一套测试。想象一个库有两个函数:getPeople 和getPerson,针对每个函数写一个测试,这样你就有两个测试。现在假设getPeople可以让你有进行选择性的搜索,那么你就要写第三个测试来确保搜索功能正常运行。如果getPeople 也可以让你给返回结果分页并且为返回结果指定起始点,那你就要写更多的测试来涵盖这些功能。你应该懂了吧,写的测试越多,就越能确保代码正确地运行。
我的库有3类函数调用。第一类是getResources,它用于返回其他API的端点(end points)列表,从用户的实际使用来看,这其实并非是必不可少的东西,但为了完整性我还是保留了它。接着是获取某一项目的函数调用,和获取所有项目的函数调用。比如对于星球这一项,我们就有getPlanet 和getPlanets。但是光有这些还不够,因为获取所有项目的函数调用返回的是分了页的数据。于是我在API里提供了getPlanets 和getPlanets(n),其中n表示数据的第几页。
这就意味着我要对四种情况进行测试:
- 调用getResources
- 调用getSingular 获取每种资源
- 调用getPlural 获取每种资源
- 调用getPlural 获取每种资源的返回结果中的某一页数据
由于我们有一个常规方法和三个遍历资源的方法,这就是说需要进行1+(3*资源数目)次测试。现在有6种类型的资源,我就需要19个测试。这还不是很糟糕,我的一个最喜欢的库Moment.js有43399个测试!
我决定用Jasmine来做单元测试,我觉得Jasmine的语法很友好,并且它是我最熟悉的一个JavaScript测试框架。
我喜欢Jasmine的一个地方是它包含一个“spec runner”以及测试示例,你可以快速修改它并且马上开始测试。spec runner只是一个包含你的库和测试代码的HTML文件,当打开它,它就运行代码并且将测试结果漂亮地展示出来。我开始时写了一个getResources的单元测试,即使你以前没接触过Jasmine,我相信你也能弄明白这里发生了什么事情:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
it("should be able to get Resources", function(done) { swapiModule.getResources(function(data) { expect(data.films).toBeDefined(); expect(data.people).toBeDefined(); expect(data.planets).toBeDefined(); expect(data.species).toBeDefined(); expect(data.starships).toBeDefined(); expect(data.vehicles).toBeDefined(); done(); }); }); |
getResource返回一个含有键值集合的简单对象,这些键值表示API所支持的每一种资源。因此我仅仅用toBeDefined就可以作一种声明:“我希望这个键值存在”。代码末尾的done()是Jasmine测试异步调用的方式。现在来看看其他三种类型的调用,首先来看一下获取一种资源的调用。
我先来对getPerson做测试。正如你想的那样,它会从Star Wars的世界里获取一个人,跟getResource一样它返回一个对象。为避免输入麻烦,我创建了一个键值集合,这样我就能通过循环的方法来利用toBeDefined()。这种测试会有一些问题。我假设这个人的ID为2,并且键值所代表的这个人不会发生变化,我觉得这么做是可以的。我希望从API返回的数据都是一致的,如果以后不一致了,我需要更新一下测试,并且更新起来也简单。还有,我现在所做的测试也许并不成熟。现在只是初步这么做,以后肯定会再回来做修改,让它们变得更聪明。现在来看一下获取所有人的调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
it("should be able to get People", function(done) { swapiModule.getPeople(function(people) { var keys = ["count", "next", "previous", "results"]; for(var i=0, len=keys.length; i<len; i++) { expect(people[keys[i]]).toBeDefined(); } done(); }); }); |
这个跟getPerson很相似,主要的不同在于返回的键值,下面测试获取第二页。