|
1 |
| -#构建Github项目 |
| 1 | +#构建Github项目 |
| 2 | + |
| 3 | +##从模块分离到测试 |
| 4 | + |
| 5 | +在之前说到 |
| 6 | + |
| 7 | +> 奋斗了近半个月后,将fork的代码读懂、重构、升级版本、调整,添加新功能、添加测试、添加CI、添加分享之后,终于almost finish。 |
| 8 | +
|
| 9 | +今天就来说说是怎样做的。 |
| 10 | + |
| 11 | +##Github项目组成 |
| 12 | + |
| 13 | +以之前造的[Lettuce](https://github.com/phodal/lettuce)为例,里面有: |
| 14 | + |
| 15 | + - 代码质量(Code Climate) |
| 16 | + - CI状态(Travis CI) |
| 17 | + - 测试覆盖率(96%) |
| 18 | + - 自动化测试(npm test) |
| 19 | + - 文档 |
| 20 | + |
| 21 | +按照[Web Developer路线图](https://github.com/phodal/awesome-developer)来说,我们还需要有: |
| 22 | + |
| 23 | + - 版本管理 |
| 24 | + - 自动部署 |
| 25 | + |
| 26 | +等等。 |
| 27 | + |
| 28 | +##Skillock模块化 |
| 29 | + |
| 30 | +在SkillTree的源码里,大致分为三部分: |
| 31 | + |
| 32 | + - namespace函数: 故名思意 |
| 33 | + - Calculator也就是TalentTree,主要负责解析、生成url,头像,依赖等等 |
| 34 | + - Skill 主要是tips部分。 |
| 35 | + |
| 36 | +而这一些都在一个js里,对于一个库来说,是一件好事,但是对于一个项目来说,并非如此。 |
| 37 | + |
| 38 | +依赖的库有 |
| 39 | + |
| 40 | + - jQuery |
| 41 | + - Knockout |
| 42 | + |
| 43 | +好在Knockout可以用Require.js进行管理,于是,使用了``Require.js``进行管理: |
| 44 | + |
| 45 | +```html |
| 46 | +<script type="text/javascript" data-main="app/scripts/main.js" src="app/lib/require.js"></script> |
| 47 | +``` |
| 48 | + |
| 49 | +``main.js``配置如下: |
| 50 | + |
| 51 | +```javascript |
| 52 | +require.config({ |
| 53 | + baseUrl: 'app', |
| 54 | + paths:{ |
| 55 | + jquery: 'lib/jquery', |
| 56 | + json: 'lib/json', |
| 57 | + text: 'lib/text' |
| 58 | + } |
| 59 | +}); |
| 60 | + |
| 61 | +require(['scripts/ko-bindings']); |
| 62 | + |
| 63 | +require(['lib/knockout', 'scripts/TalentTree', 'json!data/web.json'], function(ko, TalentTree, TalentData) { |
| 64 | + 'use strict'; |
| 65 | + var vm = new TalentTree(TalentData); |
| 66 | + ko.applyBindings(vm); |
| 67 | +}); |
| 68 | +``` |
| 69 | + |
| 70 | +text、json插件主要是用于处理web.json,即用json来处理技能,于是不同的类到了不同的js文件。 |
| 71 | + |
| 72 | + . |
| 73 | + |____Book.js |
| 74 | + |____Doc.js |
| 75 | + |____ko-bindings.js |
| 76 | + |____Link.js |
| 77 | + |____main.js |
| 78 | + |____Skill.js |
| 79 | + |____TalentTree.js |
| 80 | + |____Utils.js |
| 81 | + |
| 82 | +加上了后来的推荐阅读书籍等等。而Book和Link都是继承自Doc。 |
| 83 | + |
| 84 | +```javascript |
| 85 | +define(['scripts/Doc'], function(Doc) { |
| 86 | + 'use strict'; |
| 87 | + function Book(_e) { |
| 88 | + Doc.apply(this, arguments); |
| 89 | + } |
| 90 | + Book.prototype = new Doc(); |
| 91 | + |
| 92 | + return Book; |
| 93 | +}); |
| 94 | +``` |
| 95 | + |
| 96 | +而这里便是后面对其进行重构的内容。Doc类则是Skillock中类的一个缩影 |
| 97 | + |
| 98 | +```javascript |
| 99 | +define([], function() { |
| 100 | + 'use strict'; |
| 101 | + var Doc = function (_e) { |
| 102 | + var e = _e || {}; |
| 103 | + var self = this; |
| 104 | + |
| 105 | + self.label = e.label || (e.url || 'Learn more'); |
| 106 | + self.url = e.url || 'javascript:void(0)'; |
| 107 | + }; |
| 108 | + |
| 109 | + return Doc; |
| 110 | +}); |
| 111 | +``` |
| 112 | + |
| 113 | +或者说这是一个AMD的Class应该有的样子。考虑到this的隐性绑定,作者用了self=this来避免这个问题。最后Return了这个对象,我们在调用的就需要new一个。大部分在代码中返回的都是对象,除了在Utils类里面返回的是函数: |
| 114 | + |
| 115 | +```javascript |
| 116 | +return { |
| 117 | + getSkillsByHash: getSkillsByHash, |
| 118 | + getSkillById: getSkillById, |
| 119 | + prettyJoin: prettyJoin |
| 120 | +}; |
| 121 | +``` |
| 122 | + |
| 123 | +当然函数也是一个对象。 |
| 124 | + |
| 125 | +##Skillock测试 |
| 126 | + |
| 127 | +###自动化测试 |
| 128 | + |
| 129 | +一直习惯用Travis CI,于是也继续用Travis Ci,``.travis.yml``配置如下所示: |
| 130 | + |
| 131 | +```yml |
| 132 | +language: node_js |
| 133 | +node_js: |
| 134 | + - "0.10" |
| 135 | + |
| 136 | +notifications: |
| 137 | + email: false |
| 138 | + |
| 139 | +branches: |
| 140 | + only: |
| 141 | + - gh-pages |
| 142 | +``` |
| 143 | +
|
| 144 | +使用gh-pages的原因是,我们一push代码的时候,就可以自动测试、部署等等,好处一堆堆的。 |
| 145 | +
|
| 146 | +接着我们需要在``package.json``里面添加脚本 |
| 147 | +
|
| 148 | +```javascript |
| 149 | +"scripts": { |
| 150 | + "test": "mocha" |
| 151 | + } |
| 152 | +``` |
| 153 | + |
| 154 | +这样当我们push代码的时候便会自动跑所有的测试。因为mocha的主要配置是用``mocha.opts``,所以我们还需要配置一下``mocha.opts`` |
| 155 | + |
| 156 | + --reporter spec |
| 157 | + --ui bdd |
| 158 | + --growl |
| 159 | + --colors |
| 160 | + test/spec |
| 161 | + |
| 162 | +最后的``test/spec``是指定测试的目录。 |
| 163 | + |
| 164 | +##Jshint |
| 165 | + |
| 166 | +> JSLint定义了一组编码约定,这比ECMA定义的语言更为严格。这些编码约定汲取了多年来的丰富编码经验,并以一条年代久远的编程原则 作为宗旨:能做并不意味着应该做。JSLint会对它认为有的编码实践加标志,另外还会指出哪些是明显的错误,从而促使你养成好的 JavaScript编码习惯。 |
| 167 | +
|
| 168 | +当我们的js写得不合理的时候,这时测试就无法通过: |
| 169 | + |
| 170 | + line 5 col 25 A constructor name should start with an uppercase letter. |
| 171 | + line 21 col 62 Strings must use singlequote. |
| 172 | + |
| 173 | +这是一种驱动写出更规范js的方法。 |
| 174 | + |
| 175 | + |
| 176 | +###Mocha |
| 177 | + |
| 178 | +> Mocha 是一个优秀的JS测试框架,支持TDD/BDD,结合 should.js/expect/chai/better-assert,能轻松构建各种风格的测试用例。 |
| 179 | +
|
| 180 | +最后的效果如下所示: |
| 181 | + |
| 182 | + Book,Link |
| 183 | + Book Test |
| 184 | + ✓ should return book label & url |
| 185 | + Link Test |
| 186 | + ✓ should return link label & url |
| 187 | + |
| 188 | +###测试用例 |
| 189 | + |
| 190 | +简单地看一下Book的测试: |
| 191 | + |
| 192 | +```javascript |
| 193 | +/* global describe, it */ |
| 194 | + |
| 195 | +var requirejs = require("requirejs"); |
| 196 | +var assert = require("assert"); |
| 197 | +var should = require("should"); |
| 198 | +requirejs.config({ |
| 199 | + baseUrl: 'app/', |
| 200 | + nodeRequire: require |
| 201 | +}); |
| 202 | + |
| 203 | +describe('Book,Link', function () { |
| 204 | + var Book, Link; |
| 205 | + before(function (done) { |
| 206 | + requirejs(['scripts/Book'、], function (Book_Class) { |
| 207 | + Book = Book_Class; |
| 208 | + done(); |
| 209 | + }); |
| 210 | + }); |
| 211 | + |
| 212 | + describe('Book Test', function () { |
| 213 | + it('should return book label & url', function () { |
| 214 | + var book_name = 'Head First HTML与CSS'; |
| 215 | + var url = 'http://www.phodal.com'; |
| 216 | + var books = { |
| 217 | + label: book_name, |
| 218 | + url: url |
| 219 | + }; |
| 220 | + |
| 221 | + var _book = new Book(books); |
| 222 | + _book.label.should.equal(book_name); |
| 223 | + _book.url.should.equal(url); |
| 224 | + }); |
| 225 | + }); |
| 226 | +}); |
| 227 | +``` |
| 228 | + |
| 229 | +因为我们用``require.js``来管理浏览器端,在后台写测试来测试的时候,我们也需要用他来管理我们的依赖,这也就是为什么这个测试这么从的原因,多数情况下一个测试类似于这样子的。(用Jasmine似乎会是一个更好的主意,但是用习惯Jasmine了) |
| 230 | + |
| 231 | +```javascript |
| 232 | +describe('Book Test', function () { |
| 233 | +it('should return book label & url', function () { |
| 234 | + var book_name = 'Head First HTML与CSS'; |
| 235 | + var url = 'http://www.phodal.com'; |
| 236 | + var books = { |
| 237 | + label: book_name, |
| 238 | + url: url |
| 239 | + }; |
| 240 | + |
| 241 | + var _book = new Book(books); |
| 242 | + _book.label.should.equal(book_name); |
| 243 | + _book.url.should.equal(url); |
| 244 | +}); |
| 245 | +}); |
| 246 | +``` |
| 247 | + |
| 248 | +最后的断言,也算是测试的核心,保证测试是有用的。 |
| 249 | + |
| 250 | +##Code Climate来clean code与重构 |
| 251 | + |
| 252 | + - 当你写了一大堆代码,你没有意识到里面有一大堆重复。 |
| 253 | + - 当你写了一大堆测试,却不知道覆盖率有多少。 |
| 254 | + |
| 255 | +这就是个问题了,于是偶然间看到了一个叫code climate的网站。 |
| 256 | + |
| 257 | +##Code Climate |
| 258 | + |
| 259 | +> 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. |
| 260 | +
|
| 261 | +Code Climate整合一组静态分析工具的结果到一个单一的,实时的报告,让您的团队需要识别热点,探讨新的方法,提高代码质量的信息。 |
| 262 | + |
| 263 | +简单地来说: |
| 264 | + |
| 265 | +- 对我们的代码评分 |
| 266 | +- 找出代码中的坏味道 |
| 267 | + |
| 268 | +于是,我们先来了个例子 |
| 269 | + |
| 270 | +Rating | Name | Complexity | Duplication | Churn | C/M | Coverage | Smells |
| 271 | +--------|------|--------------|-------------|----------|---------|--------------------- |
| 272 | +A | lib/coap/coap_request_handler.js | 24 | 0 | 6 | 2.6 | 46.4% | 0 |
| 273 | +A | lib/coap/coap_result_helper.js | 14 | 0 | 2 | 3.4 | 80.0% | 0 |
| 274 | +A | lib/coap/coap_server.js | 16 | 0 | 5 | 5.2 | 44.0% | 0 |
| 275 | +A | lib/database/db_factory.js | 8 | 0 | 3 | 3.8 | 92.3% | 0 |
| 276 | +A | lib/database/iot_db.js | 7 | 0 | 6 | 1.0 | 58.8% | 0 |
| 277 | +A | lib/database/mongodb_helper.js | 63 | 0 | 11 | 4.5 | 35.0% | 0 |
| 278 | +C | lib/database/sqlite_helper.js | 32 | 86 | 10 | 4.5 | 35.0% | 2 |
| 279 | +B | lib/rest/rest_helper.js | 19 | 62 | 3 | 4.7 | 37.5% | 2 |
| 280 | +A | lib/rest/rest_server.js | 17 | 0 | 2 | 8.6 | 88.9% | 0 |
| 281 | +A | lib/url_handler.js | 9 | 0 | 5 | 2.2 | 94.1% | 0 |
| 282 | + |
| 283 | +分享得到的最后的结果是: |
| 284 | + |
| 285 | +![Coverage][1] |
| 286 | + |
| 287 | +##代码的坏味道 |
| 288 | + |
| 289 | +于是我们就打开``lib/database/sqlite_helper.js``,因为其中有两个坏味道 |
| 290 | + |
| 291 | +###Similar code found in two :expression_statement nodes (mass = 86) |
| 292 | + |
| 293 | +在代码的 ``lib/database/sqlite_helper.js:58…61 < >`` |
| 294 | + |
| 295 | +```javascript |
| 296 | + SQLiteHelper.prototype.deleteData = function (url, callback) { |
| 297 | + 'use strict'; |
| 298 | + var sql_command = "DELETE FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url); |
| 299 | + SQLiteHelper.prototype.basic(sql_command, callback); |
| 300 | +``` |
| 301 | + |
| 302 | +lib/database/sqlite_helper.js:64…67 < > |
| 303 | +
|
| 304 | +与 |
| 305 | +
|
| 306 | +```javascript |
| 307 | +SQLiteHelper.prototype.getData = function (url, callback) { |
| 308 | + 'use strict'; |
| 309 | + var sql_command = "SELECT * FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url); |
| 310 | + SQLiteHelper.prototype.basic(sql_command, callback); |
| 311 | +``` |
| 312 | +
|
| 313 | +只是这是之前修改过的重复。。 |
| 314 | +
|
| 315 | +原来的代码是这样的 |
| 316 | +
|
| 317 | +```javascript |
| 318 | +SQLiteHelper.prototype.postData = function (block, callback) { |
| 319 | + 'use strict'; |
| 320 | + var db = new sqlite3.Database(config.db_name); |
| 321 | + var str = this.parseData(config.keys); |
| 322 | + var string = this.parseData(block); |
| 323 | + |
| 324 | + var sql_command = "insert or replace into " + config.table_name + " (" + str + ") VALUES (" + string + ");"; |
| 325 | + db.all(sql_command, function (err) { |
| 326 | + SQLiteHelper.prototype.errorHandler(err); |
| 327 | + db.close(); |
| 328 | + callback(); |
| 329 | + }); |
| 330 | +}; |
| 331 | + |
| 332 | +SQLiteHelper.prototype.deleteData = function (url, callback) { |
| 333 | + 'use strict'; |
| 334 | + var db = new sqlite3.Database(config.db_name); |
| 335 | + var sql_command = "DELETE FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url); |
| 336 | + db.all(sql_command, function (err) { |
| 337 | + SQLiteHelper.prototype.errorHandler(err); |
| 338 | + db.close(); |
| 339 | + callback(); |
| 340 | + }); |
| 341 | +}; |
| 342 | + |
| 343 | +SQLiteHelper.prototype.getData = function (url, callback) { |
| 344 | + 'use strict'; |
| 345 | + var db = new sqlite3.Database(config.db_name); |
| 346 | + var sql_command = "SELECT * FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url); |
| 347 | + db.all(sql_command, function (err, rows) { |
| 348 | + SQLiteHelper.prototype.errorHandler(err); |
| 349 | + db.close(); |
| 350 | + callback(JSON.stringify(rows)); |
| 351 | + }); |
| 352 | +}; |
| 353 | +``` |
| 354 | +说的也是大量的重复,重构完的代码 |
| 355 | +
|
| 356 | +```javascript |
| 357 | +SQLiteHelper.prototype.basic = function(sql, db_callback){ |
| 358 | + 'use strict'; |
| 359 | + var db = new sqlite3.Database(config.db_name); |
| 360 | + db.all(sql, function (err, rows) { |
| 361 | + SQLiteHelper.prototype.errorHandler(err); |
| 362 | + db.close(); |
| 363 | + db_callback(JSON.stringify(rows)); |
| 364 | + }); |
| 365 | + |
| 366 | +}; |
| 367 | + |
| 368 | +SQLiteHelper.prototype.postData = function (block, callback) { |
| 369 | + 'use strict'; |
| 370 | + var str = this.parseData(config.keys); |
| 371 | + var string = this.parseData(block); |
| 372 | + |
| 373 | + var sql_command = "insert or replace into " + config.table_name + " (" + str + ") VALUES (" + string + ");"; |
| 374 | + SQLiteHelper.prototype.basic(sql_command, callback); |
| 375 | +}; |
| 376 | + |
| 377 | +SQLiteHelper.prototype.deleteData = function (url, callback) { |
| 378 | + 'use strict'; |
| 379 | + var sql_command = "DELETE FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url); |
| 380 | + SQLiteHelper.prototype.basic(sql_command, callback); |
| 381 | +}; |
| 382 | + |
| 383 | +SQLiteHelper.prototype.getData = function (url, callback) { |
| 384 | + 'use strict'; |
| 385 | + var sql_command = "SELECT * FROM " + config.table_name + " where " + URLHandler.getKeyFromURL(url) + "=" + URLHandler.getValueFromURL(url); |
| 386 | + SQLiteHelper.prototype.basic(sql_command, callback); |
| 387 | +}; |
| 388 | +``` |
| 389 | +
|
| 390 | +重构完后的代码比原来还长,这似乎是个问题~~ |
0 commit comments