微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

案例分析之JavaScript代码优化

2015-01-29 19:55 前端大全
点击上方蓝字↑↑↑,轻松关注哦~

大家好,我是Raymond,我写了很多糟糕的代码。好吧,其实并没有很糟糕,只是我没有遵循所谓的“最佳实践”罢了。我敢打赌看这篇文章的很多人也都没有遵循最佳实践。在这篇文章中,我将谈一谈在最近的一个项目中,我使用了一些简单的工具帮我完成了自己非常满意的代码。下面我就跟大家分享这个故事。


故事背景


假期中我尽量让自己远离工作,甚至连电脑都不去看一眼,但最后彻底失败了。


当时我饶有兴致正准备玩一轮电子游戏(见笑了,是手眼协调训练的游戏),忽然有人跟我分享了一个让人有点小激动的消息——Star Wars API发布了。即使它并不是“官方”发布的,这个“非官方”的API提供了抓取人物角色、电影、星际飞船、交通工具、物种、星球等内容的方法。Star Wars API提供免费服务,没有认证要求。它不提供搜索功能,对于一项免费服务来说,它已经很好了。如果你了解我的话,就知道我对跟Star Wars相关的东西是很着迷的。


我一时心血来潮,快速写了一个JavaScript库来集成这个API。最简单的,你可以使用它来抓取某一类型资源的全部内容:


//get all starships

swapiModule.getStarships(function(data) {

console.log("Result of getStarships", data);

});


或者抓取一个特定的项目:


//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,我相信你也能弄明白这里发生了什么事情:


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返回的数据都是一致的,如果以后不一致了,我需要更新一下测试,并且更新起来也简单。还有,我现在所做的测试也许并不成熟。现在只是初步这么做,以后肯定会再回来做修改,让它们变得更聪明。现在来看一下获取所有人的调用。


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很相似,主要的不同在于返回的键值,下面测试获取第二页。


it("should be able to get the second page of People", function(done) {

swapiModule.getPeople(2, function(people) {

var keys = ["count", "next", "previous", "results"];

for(var i=0, len=keys.length; i<len; i++) {

expect(people[keys[i]]).toBeDefined();

}

expect(people.previous).toMatch("page=1");

done();

});

});


这跟前面的测试非常相似,只是增加了一个指向前一页的链接。同样,我认为这里能做一些改进。我真的有必要在“页面”的测试里检查对象的键值吗?没必要吧?我现在先把它放在一边。


这样就差不多了。接下来,我只是对另外5种资源重复写这三类的调用。在写测试的时候,我发现了代码里的一些有趣的问题,这更加让我觉得写测试是非常有帮助的。我发现自己在API中并没有充分处理某些情况下的错误,比如,getFilms只返回一页的电影数据,那么getFilms(2)到底应该返回什么呢?返回一个对象?JavaScript 异常?我现在还不知道怎么办,不过这个问题我最后是会处理的(我决定等到下一次重大改动时再来解决这个问题)。当所有的测试写好了,我就运行页面,确保所有的测试都通过。



用JSHint检测代码质量


我的优化列表的下一项是使用代码检测工具。它能检测代码中存在的问题,这些问题可能是代码中的bug,或者是代码的性能优化,或者只是检测代码有没有做到最佳的实践。最开始的JavaScript代码质量检测工具是JSLint,但我用了另外一种叫JSHint的。JSHint在使用上比JSLint更方便,由于我是个喜欢简便的人,所以觉得前者更适合我。


有很多种方式使用JSHint,包括在你最喜欢的编辑器中。我用的是Brackets (目前是我最喜欢的编辑器),它提供了JSHint的扩展,在我敲代码的时候就能看到问题。但在这篇文章中我用的是JSHint的命令行版本。只要你安装了npm,你就可以用npm install -g jshint命令将JSHint CLI添加到自己的环境中。


如果安装好了,你就可以用它来检测你的代码。例如:


jshint swapi.js


当我输入这个以后,命令行什么也没有输出。我于是花了十多分钟来研究命令行的参数,最后发现我这个简单的库实际上已经通过了JSHint默认设置的测试。是的,它通过了,但我仍然感到吃惊。在我看来,命令行什么都没输出说明代码有错误。但是对于JSHint,命令行没有输出就表示代码通过检测,其实是好消息。


为了验证一下,我故意在代码中放了一些糟糕的代码。


var swapiModule = function () {

var rootURL = "http://swapi.co/api/";

y=1

<bold>


我写了一行没有分号的代码(很恐怖吧!),也加了一个HTML标签。JSHint都检测出来了,并且把语法错误也标识了出来。下面是命令行的输出结果:



问题错误都报告得很准确,但是注意看上面截图中输出的行数,第一行正确,但是第二行就把行数完全弄错了。我的猜测是JSHint不能识别HTML标签。由于初学者一般不会这么做,所以我也就不担心这事。如果你把HTML标签去掉了,它就会报出分号的错误,并且也会报出正确的行数。为满足你的求知欲,JSHint支持一系列设置,包括选择去掉分号的检测。但是切记,每当你省略一个分号,上帝就杀死一只喵咪,真的(我是从网上看到的)。


压缩代码库


我下一步要做的是压缩库的大小。虽然这个库现在不大(有128行),但我也没指望以后它自己会变小,坦诚地讲,如果压缩代码很容易,那么没有理由不去做。压缩主要是去掉空格,减少变量名,让文件越小越好。我选择UglifyJS 来完成这个任务。一旦安装好,我就可以这么做:


uglifyjs swapi.js -c -m -o swapi.min.js


你可以查询相关的文档来获取各个参数的详细解释,这里我只简单地用这个工具来缩短变量名及去掉空格。很棒的一点是,当我运行它时,它真的提醒了我曾忘记的一件事:


//generic for ALL calls, todo, why optimize now!

function getResource(u, cb) {

}


这个函数虽说不妨碍正常运行,但它占用了空间,UglifyJS 马上就注意到它了:



自动化这个过程


好了,到目前为止一切顺利。我使用了单元测试来保障代码的正确运行,用了代码质量检测工具,最后也对代码进行了压缩。虽然所有这些事都相对比较简单,但我是个非常懒的人,我还想把这些事情全部自动化。在我心中,这个过程最好是这样子的:


  • 进行单元测试,如果它们通过了…

  • 进行JSHint测试,如果它通过了…

  • 生成一个库的精简版本.


为了使这个过程自动化,我打算用Grunt。它并不是唯一的Web 开发任务运行器,但是由于我还没用过Gulp ,所以就默认用Grunt了。


Grunt让你定义一系列的任务,并用命令行来运行它。你可以定义一条任务链,一旦其中一个任务运行失败,整个过程就停止。基本上来说,如果你发现自己要运行多个任务,并重复多次,那么你就可以使用像Grunt这样的任务运行器来简化你的过程。


如果你以前从没用过Grunt,可以去看一下这个入门教程。当添加package.json文件来加载我所需要的Grunt插件(支持Jasmine, JSHint, 和 Uglify)之后,我就建了这个Gruntfile.js:


module.exports = function(grunt) {

// Project configuration.

grunt.initConfig({

pkg: grunt.file.readJSON('package.json'),

uglify: {

build: {

src: 'lib/swapi.js',

dest: 'lib/swapi.min.js'

}

},

jshint: {

all: ['lib/swapi.js']

},

jasmine: {

all: {

src:"lib/swapi.js",

options: {

specs:"tests/spec/swapiSpec.js",

'--web-security':false

}

}

}

});

grunt.loadNpmTasks('grunt-contrib-uglify');

grunt.loadNpmTasks('grunt-contrib-jshint');

grunt.loadNpmTasks('grunt-contrib-jasmine');

grunt.registerTask('default', ['jasmine','jshint','uglify']);

};


基本上是按照Jasmine、 JSHint 、uglify的顺序执行,我在命令行只需敲入grunt就能让三个任务运行起来。



假如我对程序做点破坏,比如添加一些会让JSHint中断的代码,Grunt就会报告出这个问题并且停止执行:



结果如何?


综上所述,我的原始库并没有发生功能性的改进,因为我还没有针对代码做优化,但是看一下我所做的:


我进行了单元测试来检查现有的所有功能,当添加新的功能时,我能确信不会破坏用户正在使用的功能,真的确信。

我使用了代码质量检测工具来确保我的代码符合人们所认可的最佳实践。尽管不是很严格,但它就像另外一双眼睛在看我的代码,我喜欢它这样。

我增加了一个精简版本的库。尽管它并没带来多大的节省,但是当以后我的库变大了,它就可以派上用场。

我让所有的这些事自动执行。现在我只用一个快捷命令就可以运行上面所有的任务。生活真美妙!我现在就是一个超级代码忍者!

现在我拥有一个绝好的项目,这是一件很棒的事情!你可以去这儿看项目的最终版本。



原文出处:developer.telerik.com

译文出处:伯乐在线 - XfLoops


/////////////////


1. 『前端大全』分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯,欢迎关注。微信号:FrontDev

2. 点击“阅读原文”,查看更多前端文章。


 
前端大全 更多文章 5个典型的JavaScript面试题(上) Limu:JavaScript的那些书 Web开发:我希望得到的编程学习路线图 JavaScript基础工具清单 常用排序算法之JavaScript实现
猜您喜欢 高级技巧!有什么方法可以打造“触动人心的设计”? 理解CSS中BFC O2O产品质量保障体系(四)| 基于学习的线下商户质量挖掘 PingCAP 第 19 期 NewSQL Meetup Node.js新手必须知道的4个JavaScript概念