构建 GitHub 项目

如何用好 GitHub

如何用好 GitHub,并实践一些敏捷软件开发是一个很有意思的事情.我们可以在上面做很多事情,从测试到CI,再到自动部署.

敏捷软件开发

显然我是在扯淡,这和敏捷软件开发没有什么关系。不过我也不知道瀑布流是怎样的。说说我所知道的一个项目的组成吧:

  • 看板式管理应用程序(如trello,简单地说就是管理软件功能)
  • CI(持续集成)
  • 测试覆盖率
  • 代码质量(code smell)

对于一个不是远程的团队(如只有一个人的项目) 来说,Trello、Jenkin、Jira不是必需的:

你存在,我深深的脑海里

当只有一个人的时候,你只需要明确知道自己想要什么就够了。我们还需要的是CI、测试,以来提升代码的质量。

测试

通常我们都会找Document,如果没有的话,你会找什么?看源代码,还是看测试?

  1. it("specifying response when you need it", function (done) {
  2. var doneFn = jasmine.createSpy("success");
  3. lettuce.get('/some/cool/url', function (result) {
  4. expect(result).toEqual("awesome response");
  5. done();
  6. });
  7. expect(jasmine.Ajax.requests.mostRecent().url).toBe('/some/cool/url');
  8. expect(doneFn).not.toHaveBeenCalled();
  9. jasmine.Ajax.requests.mostRecent().respondWith({
  10. "status": 200,
  11. "contentType": 'text/plain',
  12. "responseText": 'awesome response'
  13. });
  14. });

代码来源: https://github.com/phodal/lettuce

上面的测试用例,清清楚楚地写明了用法,虽然写得有点扯。

等等,测试是用来干什么的。那么,先说说我为什么会想去写测试吧:

  • 我不希望每次做完一个个新功能的时候,再手动地去测试一个个功能。(自动化测试)
  • 我不希望在重构的时候发现破坏了原来的功能,而我还一无所知。
  • 我不敢push代码,因为我没有把握。

虽然,我不是TDD的死忠,测试的目的是保证功能正常,TDD没法让我们写出质量更高的代码。但是有时TDD是不错的,可以让我们写出逻辑更简单地代码。

也许你已经知道了SeleniumJasmineCucumber等等的框架,看到过类似于下面的测试

  1. Ajax
  2. specifying response when you need it
  3. specifying html when you need it
  4. should be post to some where
  5. Class
  6. respects instanceof
  7. inherits methods (also super)
  8. extend methods
  9. Effect
  10. should be able fadein elements
  11. should be able fadeout elements

代码来源: https://github.com/phodal/lettuce

看上去似乎每个测试都很小,不过补完每一个测试之后我们就得到了测试覆盖率

File Statements Branches Functions Lines
lettuce.js 98.58% (209 / 212) 82.98%(78 / 94) 100.00% (54 / 54) 98.58% (209 / 212)

本地测试都通过了,于是我们添加了Travis-CI来跑我们的测试

CI

虽然node.js不算是一门语言,但是因为我们用的node,下面的是一个简单的.travis.yml示例:

  1. language: node_js
  2. node_js:
  3. - "0.10"
  4. notifications:
  5. email: false
  6. before_install: npm install -g grunt-cli
  7. install: npm install
  8. after_success: CODECLIMATE_REPO_TOKEN=321480822fc37deb0de70a11931b4cb6a2a3cc411680e8f4569936ac8ffbb0ab codeclimate < coverage/lcov.info

代码来源: https://github.com/phodal/lettuce

我们把这些集成到README.md之后,就有了之前那张图。

CI对于一个开发者在不同城市开发同一项目上来说是很重要的,这意味着当你添加的部分功能有测试覆盖的时候,项目代码会更加强壮。

代码质量

jslint这类的工具,只能保证代码在语法上是正确的,但是不能保证你写了一堆bad smell的代码。

  • 重复代码
  • 过长的函数
  • 等等

Code Climate是一个与github集成的工具,我们不仅仅可以看到测试覆盖率,还有代码质量。

先看看上面的ajax类:

  1. Lettuce.get = function (url, callback) {
  2. Lettuce.send(url, 'GET', callback);
  3. };
  4. Lettuce.send = function (url, method, callback, data) {
  5. data = data || null;
  6. var request = new XMLHttpRequest();
  7. if (callback instanceof Function) {
  8. request.onreadystatechange = function () {
  9. if (request.readyState === 4 && (request.status === 200 || request.status === 0)) {
  10. callback(request.responseText);
  11. }
  12. };
  13. }
  14. request.open(method, url, true);
  15. if (data instanceof Object) {
  16. data = JSON.stringify(data);
  17. request.setRequestHeader('Content-Type', 'application/json');
  18. }
  19. request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
  20. request.send(data);
  21. };

代码来源: https://github.com/phodal/lettuce

Code Climate在出现了一堆问题

  • Missing “use strict” statement. (Line 2)
  • Missing “use strict” statement. (Line 14)
  • ‘Lettuce’ is not defined. (Line 5)

而这些都是小问题啦,有时可能会有

  • Similar code found in two :expression_statement nodes (mass = 86)

这就意味着我们可以对上面的代码进行重构,他们是重复的代码。

模块分离与测试

在之前说到

奋斗了近半个月后,将fork的代码读懂、重构、升级版本、调整,添加新功能、添加测试、添加CI、添加分享之后,终于almost finish。

今天就来说说是怎样做的。

以之前造的Lettuce为例,里面有:

  • 代码质量(Code Climate)
  • CI状态(Travis CI)
  • 测试覆盖率(96%)
  • 自动化测试(npm test)
  • 文档

按照Web Developer路线图来说,我们还需要有:

  • 版本管理
  • 自动部署

等等。

代码模块化

在SkillTree的源码里,大致分为三部分:

  • namespace函数: 顾名思义
  • Calculator也就是TalentTree,主要负责解析、生成url,头像,依赖等等
  • Skill 主要是tips部分。

而这一些都在一个js里,对于一个库来说,是一件好事,但是对于一个项目来说,并非如此。

依赖的库有

  • jQuery
  • Knockout

好在Knockout可以用Require.js进行管理,于是,使用了Require.js进行管理:

  1. <script type="text/javascript" data-main="app/scripts/main.js" src="app/lib/require.js"></script>

main.js配置如下:

  1. require.config({
  2. baseUrl: 'app',
  3. paths:{
  4. jquery: 'lib/jquery',
  5. json: 'lib/json',
  6. text: 'lib/text'
  7. }
  8. });
  9. require(['scripts/ko-bindings']);
  10. require(['lib/knockout', 'scripts/TalentTree', 'json!data/web.json'], function(ko, TalentTree, TalentData) {
  11. 'use strict';
  12. var vm = new TalentTree(TalentData);
  13. ko.applyBindings(vm);
  14. });

text、json插件主要是用于处理web.json,即用json来处理技能,于是不同的类到了不同的js文件。

  1. .
  2. |____Book.js
  3. |____Doc.js
  4. |____ko-bindings.js
  5. |____Link.js
  6. |____main.js
  7. |____Skill.js
  8. |____TalentTree.js
  9. |____Utils.js

加上了后来的推荐阅读书籍等等。而Book和Link都是继承自Doc。

  1. define(['scripts/Doc'], function(Doc) {
  2. 'use strict';
  3. function Book(_e) {
  4. Doc.apply(this, arguments);
  5. }
  6. Book.prototype = new Doc();
  7. return Book;
  8. });

而这里便是后面对其进行重构的内容。Doc类则是Skillock中类的一个缩影

  1. define([], function() {
  2. 'use strict';
  3. var Doc = function (_e) {
  4. var e = _e || {};
  5. var self = this;
  6. self.label = e.label || (e.url || 'Learn more');
  7. self.url = e.url || 'javascript:void(0)';
  8. };
  9. return Doc;
  10. });

或者说这是一个AMD的Class应该有的样子。考虑到this的隐性绑定,作者用了self=this来避免这个问题。最后Return了这个对象,我们在调用的就需要new一个。大部分在代码中返回的都是对象,除了在Utils类里面返回的是函数:

  1. return {
  2. getSkillsByHash: getSkillsByHash,
  3. getSkillById: getSkillById,
  4. prettyJoin: prettyJoin
  5. };

当然函数也是一个对象。

自动化测试

一直习惯用Travis CI,于是也继续用Travis Ci,.travis.yml配置如下所示:

  1. language: node_js
  2. node_js:
  3. - "0.10"
  4. notifications:
  5. email: false
  6. branches:
  7. only:
  8. - gh-pages

使用gh-pages的原因是,我们一push代码的时候,就可以自动测试、部署等等,好处一堆堆的。

接着我们需要在package.json里面添加脚本

  1. "scripts": {
  2. "test": "mocha"
  3. }

这样当我们push代码的时候便会自动跑所有的测试。因为mocha的主要配置是用mocha.opts,所以我们还需要配置一下mocha.opts

  1. --reporter spec
  2. --ui bdd
  3. --growl
  4. --colors
  5. test/spec

最后的test/spec是指定测试的目录。

Jshint

JSLint定义了一组编码约定,这比ECMA定义的语言更为严格。这些编码约定汲取了多年来的丰富编码经验,并以一条年代久远的编程原则 作为宗旨:能做并不意味着应该做。JSLint会对它认为有的编码实践加标志,另外还会指出哪些是明显的错误,从而促使你养成好的 JavaScript编码习惯。

当我们的js写得不合理的时候,这时测试就无法通过:

  1. line 5 col 25 A constructor name should start with an uppercase letter.
  2. line 21 col 62 Strings must use singlequote.

这是一种驱动写出更规范js的方法。

Mocha

Mocha 是一个优秀的JS测试框架,支持TDD/BDD,结合 should.js/expect/chai/better-assert,能轻松构建各种风格的测试用例。

最后的效果如下所示:

  1. Book,Link
  2. Book Test
  3. should return book label & url
  4. Link Test
  5. should return link label & url

测试示例

简单地看一下Book的测试:

  1. /* global describe, it */
  2. var requirejs = require("requirejs");
  3. var assert = require("assert");
  4. var should = require("should");
  5. requirejs.config({
  6. baseUrl: 'app/',
  7. nodeRequire: require
  8. });
  9. describe('Book,Link', function () {
  10. var Book, Link;
  11. before(function (done) {
  12. requirejs(['scripts/Book'、], function (Book_Class) {
  13. Book = Book_Class;
  14. done();
  15. });
  16. });
  17. describe('Book Test', function () {
  18. it('should return book label & url', function () {
  19. var book_name = 'Head First HTML与CSS';
  20. var url = 'http://www.phodal.com';
  21. var books = {
  22. label: book_name,
  23. url: url
  24. };
  25. var _book = new Book(books);
  26. _book.label.should.equal(book_name);
  27. _book.url.should.equal(url);
  28. });
  29. });
  30. });

因为我们用require.js来管理浏览器端,在后台写测试来测试的时候,我们也需要用他来管理我们的依赖,这也就是为什么这个测试这么长的原因,多数情况下一个测试类似于这样子的。(用Jasmine似乎会是一个更好的主意,但是用习惯Jasmine了)

  1. describe('Book Test', function () {
  2. it('should return book label & url', function () {
  3. var book_name = 'Head First HTML与CSS';
  4. var url = 'http://www.phodal.com';
  5. var books = {
  6. label: book_name,
  7. url: url
  8. };
  9. var _book = new Book(books);
  10. _book.label.should.equal(book_name);
  11. _book.url.should.equal(url);
  12. });
  13. });

最后的断言,也算是测试的核心,保证测试是有用的。

代码质量与重构

  • 当你写了一大堆代码,你没有意识到里面有一大堆重复。
  • 当你写了一大堆测试,却不知道覆盖率有多少。

这就是个问题了,于是偶然间看到了一个叫code climate的网站。

Code Climate

Code Climate consolidates the results from a suite of static analysis tools into a single, real-time report, giving your team the information it needs to identify hotspots, evaluate new approaches, and improve code quality.

Code Climate整合一组静态分析工具的结果到一个单一的,实时的报告,让您的团队需要识别热点,探讨新的方法,提高代码质量的信息。

简单地来说:

  • 对我们的代码评分
  • 找出代码中的坏味道

于是,我们先来了个例子

Rating Name Complexity Duplication Churn C/M Coverage Smells
A lib/coap/coap_request_handler.js 24 0 6 2.6 46.4% 0
A lib/coap/coap_result_helper.js 14 0 2 3.4 80.0% 0
A lib/coap/coap_server.js 16 0 5 5.2 44.0% 0
A lib/database/db_factory.js 8 0 3 3.8 92.3% 0
A lib/database/iot_db.js 7 0 6 1.0 58.8% 0
A lib/database/mongodb_helper.js 63 0 11 4.5 35.0% 0
C lib/database/sqlite_helper.js 32 86 10 4.5 35.0% 2
B lib/rest/rest_helper.js 19 62 3 4.7 37.5% 2
A lib/rest/rest_server.js 17 0 2 8.6 88.9% 0
A lib/url_handler.js 9 0 5 2.2 94.1% 0

分享得到的最后的结果是:

![Coverage][1]

代码的坏味道

于是我们就打开lib/database/sqlite_helper.js,因为其中有两个坏味道

Similar code found in two :expression_statement nodes (mass = 86)

在代码的 lib/database/sqlite_helper.js:58…61 < >

  1. SQLiteHelper.prototype.deleteData = function (url, callback) {
  2. 'use strict';
  3. var sql_command = "DELETE FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
  4. SQLiteHelper.prototype.basic(sql_command, callback);

lib/database/sqlite_helper.js:64…67 < >

  1. SQLiteHelper.prototype.getData = function (url, callback) {
  2. 'use strict';
  3. var sql_command = "SELECT * FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
  4. SQLiteHelper.prototype.basic(sql_command, callback);

只是这是之前修改过的重复。。

原来的代码是这样的

  1. SQLiteHelper.prototype.postData = function (block, callback) {
  2. 'use strict';
  3. var db = new sqlite3.Database(config.db_name);
  4. var str = this.parseData(config.keys);
  5. var string = this.parseData(block);
  6. var sql_command = "insert or replace into " + config.table_name + " (" + str + ") VALUES (" + string + ");";
  7. db.all(sql_command, function (err) {
  8. SQLiteHelper.prototype.errorHandler(err);
  9. db.close();
  10. callback();
  11. });
  12. };
  13. SQLiteHelper.prototype.deleteData = function (url, callback) {
  14. 'use strict';
  15. var db = new sqlite3.Database(config.db_name);
  16. var sql_command = "DELETE FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
  17. db.all(sql_command, function (err) {
  18. SQLiteHelper.prototype.errorHandler(err);
  19. db.close();
  20. callback();
  21. });
  22. };
  23. SQLiteHelper.prototype.getData = function (url, callback) {
  24. 'use strict';
  25. var db = new sqlite3.Database(config.db_name);
  26. var sql_command = "SELECT * FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
  27. db.all(sql_command, function (err, rows) {
  28. SQLiteHelper.prototype.errorHandler(err);
  29. db.close();
  30. callback(JSON.stringify(rows));
  31. });
  32. };

说的也是大量的重复,重构完的代码

  1. SQLiteHelper.prototype.basic = function(sql, db_callback){
  2. 'use strict';
  3. var db = new sqlite3.Database(config.db_name);
  4. db.all(sql, function (err, rows) {
  5. SQLiteHelper.prototype.errorHandler(err);
  6. db.close();
  7. db_callback(JSON.stringify(rows));
  8. });
  9. };
  10. SQLiteHelper.prototype.postData = function (block, callback) {
  11. 'use strict';
  12. var str = this.parseData(config.keys);
  13. var string = this.parseData(block);
  14. var sql_command = "insert or replace into " + config.table_name + " (" + str + ") VALUES (" + string + ");";
  15. SQLiteHelper.prototype.basic(sql_command, callback);
  16. };
  17. SQLiteHelper.prototype.deleteData = function (url, callback) {
  18. 'use strict';
  19. var sql_command = "DELETE FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
  20. SQLiteHelper.prototype.basic(sql_command, callback);
  21. };
  22. SQLiteHelper.prototype.getData = function (url, callback) {
  23. 'use strict';
  24. var sql_command = "SELECT * FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url);
  25. SQLiteHelper.prototype.basic(sql_command, callback);
  26. };

重构完后的代码比原来还长,这似乎是个问题~~