## Maintainable Node.js
## About Me - 不四 @ [天猫前端](https://github.com/tmallfe/tmallfe.github.io/issues) - [dead-horse](https://github.com/dead-horse) @ github - [逗B码农死小马](http://weibo.com/deadhorse) @ 微博 ![](images/tmall.jpg)
## Why do we care about ___maintainable___?
> Most of our time is spent maintaining code

对别人好一点

对未来的自己好一点

## Code Style
## TAB vs SPACE ![](images/tab_vs_space.png)

To semicolon or not to semicolon ;

that is the question

#### [Standard](https://github.com/feross/standard) ![](images/standard.png)

Brendan Eich 在 1995 年用了十天写出了 javascript

[] + {} === '[object Object]'

{} + [] === 0

[] - {} === NaN

{} - [] === -0

![](images/stare.gif)

陷阱重重的 Node.js


		var http = require('http');
		var url = require('url');

		http.createServer(function (req, res) {
			var query = url.parse(req.url, true).query;
			var name = query.name || '';
			res.send(name.trim());
		}).listen(3000);
					

GET http://localhost:3000?name=foo 'foo'

GET http://localhost:3000 ''

GET http://localhost:3000?name=foo&name=bar Boom!

koa-qs

koa-qs


	http.createServer(function (req, res) {
	  parseBody(req, function onBody(err, body) {
	    if (err) {
	      res.statusCode = 500;
	      return res.end(err.message);
	    }
	    var keys = Object.keys(body);
	    res.end(keys.join(','));
	  }
	}).listen(3000);

	function parseBody(req, callback) {
	  // 获取原始的请求 body
	  rawBody(req, function (err, content) {
	    if (err) return callback(err);

	    var body = {};
	    try {
	      body = JSON.parse(content);
	    } catch (err) {
	      return callback(err);
	    }
	    return callback(null, body);
	  });
	}
					

POST '{"foo": "bar"}' 'foo'

POST '{}' ''

POST 'null' Boom! (JSON.parse('null') => null)

POST '"hello"' Boom! (JSON.parse('"hello"') => 'hello')

co-body


	function parseBody(req, callback) {
	  rawBody(req, function (err, content) {
	    if (err) return callback(err);
	    var body = {};
	    try {
	      body = JSON.parse(content);
	      // 严格的只接受 object 作为 body
	      if (!body || typeof body !== 'object') {
	        throw new Error('request body must be object');
	      }
	    } catch (err) {
	      return callback(err);
	    }
	    return callback(null, body);
	  });
	}
					

不信任任意的输入,提供固定的输出

不信任任意的输入,提供固定的输出

不要信任任意的输入,提供固定的输出

Node.js !== callback hell

面向未来的异步编程

Callback


	http.createServer(function (req, res) {
	  parseBody(req, function onBody(err, body) {
	    if (err) {
	      res.statusCode = 500;
	      return res.end(err.message);
	    }
	    var keys = Object.keys(body);
	    res.end(keys.join(','));
	  }
	}).listen(3000);

	function parseBody(req, callback) {
	  rawBody(req, function (err, content) {
	    if (err) return callback(err);

	    var body;
	    try {
	      body = parse(content);
	    } catch (err) {
	      return callback(err);
	    }
	    return callback(null, body);
	  });
	}

	function parse(content) {
	  var body = JSON.parse(content);
	  if (!body || typeof body !== 'object') {
	    throw new Error('request body must be object');
	  }
	}

Callback Hell


http.createServer(function (req, res) {
  rawBody(req, function (err, content) {
    if (err) {
      res.status = 500;
      res.end(err.message);
    } else {
      var body;
      try {
        body = JSON.parse(content);
        if (!body || typeof body !== 'object') {
          throw new Error('request body must be object');
        }
      } catch (err) {
        res.statusCode = 500;
        res.end(err.message);
        return;
      }
      var keys = Object.keys(body);
      res.end(keys.join(','));
    }
  });
}).listen(3000);

Promise


	http.createServer(function (req, res) {
	  parseBody(req).then(onbody, onerror);
	  function onbody(body) {
	    var keys = Object.keys(body);
	    res.end(keys.join(','));
	  }
	  function onerror(err) {
	    res.statusCode = 500;
	    res.end(err.message);
	  }
	}).listen(3000);

	function parseBody(req) {
	  return rawBody(req).then(parse);
	}
	function parse(content) {
	  var body = JSON.parse(content);
	  if (!body || typeof body !== 'object') {
	    throw new Error('request body must be object');
	  }
	  return body;
	}

Generator


	http.createServer(function (req, res) {
	  var _parseBody = co.wrap(parseBody);
	  _parseBody(req).then(onbody, onerror);

	  function onbody(body) {
	    var keys = Object.keys(body);
	    res.end(keys.join(','));
	  }
	  function onerror(err) {
	    res.statusCode = 500;
	    res.end(err.message);
	  }
	}).listen(3000);

	function* parseBody(req) {
	  var content = yield rawBody(req);
	  var body = JSON.parse(content);
	  if (!body || typeof body !== 'object') {
	    throw new Error('request body must be object');
	  }
	  return body;
	}

Async & Await


	http.createServer(function (req, res) {
	  parseBody(req).then(onbody, onerror);

	  function onbody(body) {
	    var keys = Object.keys(body);
	    res.end(keys.join(','));
	  }
	  function onerror(err) {
	    res.statusCode = 500;
	    res.end(err.message);
	  }
	}).listen(3000);

	async function parseBody(req) {
	  var content = await rawBody(req);
	  var body = JSON.parse(content);
	  if (!body || typeof body !== 'object') {
	    throw new Error('request body must be object');
	  }
	  return body;
	}
- 最新版 node 和 iojs 皆已支持 Promise 和 Generator。 - [bluebird](https://github.com/petkaantonov/bluebird) - 高性能的 Promise 实现。 - [mz](https://github.com/normalize/mz) - 让所有 Node 原生库的异步接口支持 Promise。 - [co](https://github.com/tj/co) - 基于 generator 的流程控制库。 - [babel](https://github.com/babel/babel) - 让你编写未来的 javascript。

Error Handle

Callback 第一个参数必须是 Error


		fs.readFile('foo.txt', function (err, content) {
			if (err) return onerror(err);
			return onfile(content);
		});
					

字符串不是异常


		if (!body) throw 'body not exist'; // wrong!

		if (!body) {
		  var error = new Error('body not exist');
		  error.status = 404;
		  throw error;
		}
					

没有 Callback 如何处理异常?


	function Checker(url, path) {
	  this.url = url;
	  this.path = path;
	  this.start();
	};

	Checker.prototype.shoot = function () {
	  var self = this;
	  return urllib.request(self.url)
	    .then(function (res) {
	      return fs.writeFile(self.path, res.data);
	    });
	};
	Checker.prototype.start = function () {
	  var self = this;
	  setInterval(function () {
	    self.shoot().then(noop);
	  }, ms('1s'));
	};

	var checker = new Checker('http://www.tmall.com', 'tmall.htm');
					

直接输出异常?


	function Checker(url, path) {
	  this.url = url;
	  this.path = path;
	  this.onerror = this.onerror.bind(this);
	  this.start();
	};

	Checker.prototype.shoot = function () {
	  // ...
	};
	Checker.prototype.start = function () {
	  var self = this;
	  setInterval(function () {
	    self.shoot().catch(self.onerror); // 捕获 error
	  }, ms('1s'));
	};
	Checker.prototype.onerror = function (err) {
	  console.error(err.stack);	 // 打印到 stderr
	};

	var checker = new Checker('http://www.tmall.com', 'tmall.html');
					

通过事件抛出异常


	function Checker(url, path) {
	  this.url = url;
	  this.path = path;
	  this.onerror = this.onerror.bind(this);
	  this.start();
	  EventEmitter.call(this);  // 继承 EventEmitter
	};
	util.inherits(Checker, EventEmitter);

	Checker.prototype.shoot = function () {
	  // ...
	};
	Checker.prototype.start = function () {
	  var self = this;
	  setInterval(function () {
	    self.shoot().catch(self.onerror); // 捕获 error 时间
	  }, ms('1s'));
	};
	Checker.prototype.onerror = function (err) {
	  this.emit('error', err);  // 触发 error 事件
	};

	var checker = new Checker('http://www.tmall.com', 'tmall.html');
					

默认的错误监听函数


	function Checker(url, path) {
	  this.url = url;
	  this.path = path;
	  this.onerror = this.onerror.bind(this);
	  this.start();
	  EventEmitter.call(this);
	  // 添加默认的错误监听函数
	  setImmediate(function () {
	    if (!this.listeners('error').length) {
	      this.on('error', defaultErrorHandler);
	    }
	  }.bind(this));
	};

	util.inherits(Checker, EventEmitter);

	Checker.prototype.shoot = function () {
	  // ...
	};
	Checker.prototype.start = function () {
	  // ...
	};
	Checker.prototype.onerror = function (err) {
	  this.emit('error', err);  // 触发 error 事件
	};
	function defaultErrorHandler(err) {
	  console.error(err.stack);
	}

	var checker = new Checker('http://www.tmalxzcsdfl.com/', 'tmall.html');
					
  • 不要忽视任何一个异常
  • 无处返回的错误通过事件让外层感知
  • error 事件有默认的监听函数

One More Thing...

单元测试


	describe('ctx.path', function(){
	  it('should return the pathname', function(){
	    var ctx = context();
	    ctx.url = '/login?next=/dashboard';
	    ctx.path.should.equal('/login');
	  })
	})

	describe('ctx.path=', function(){
	  it('should set the pathname', function(){
	    var ctx = context();
	    ctx.url = '/login?next=/dashboard';

	    ctx.path = '/logout';
	    ctx.path.should.equal('/logout');
	    ctx.url.should.equal('/logout?next=/dashboard');
	  })

	  it('should change .url but not .originalUrl', function(){
	    var ctx = context({ url: '/login' });
	    ctx.path = '/logout';
	    ctx.url.should.equal('/logout');
	    ctx.originalUrl.should.equal('/login');
	    ctx.request.originalUrl.should.equal('/login');
	  })
	})
					

持续集成

懂这么多道理,却依然写不好 node

天猫前端正在招聘!

busi.hyy@alibaba-inc.com

console.log('Thanks!');