koa & Toa

  • 框架原理
  • 异步流程控制和异常处理机制
  • 组件开发模式

github.com/zensh | @ZENSH严清

  • Joined GitHub: 21 Jun 2011
  • 第一行代码:24 Dec 2012
  • 目前开源项目:40+
  • 早期项目:jsgen,thenjs 等
  • 目前活跃项目:thunks 系列,Toa 系列
  • 2013 ~ 2014:前端开发
  • 2015:后端开发,基于 thunks 和 Toa 的系统重构: 用户社区, 文件系统, 消息系统
  • 关注:前后端架构,高扩展性、高可用性、高可维护性

next generation web framework for node.js

pithy and powerful web framework for node.js

造轮子,非山寨!

俗人造轮子,高手发明轮子,大神设计轮子

庸人混日子

var app = express()

app.use(function (req, res, next) {
	/* ... */
  next()
})
app.use(function (req, res, next) {
	/* ... */
	res.send('Hello World\n')
})

app.listen(3000)
						

express: req, res, next

express

  • 处理流程简洁明了
  • 缺乏异步流程控制(异常处理)能力
  • 历史包袱重,集成功能多,可扩展性相对差

var app = koa()

app.use(function *(next) {
	/* this.req... */
	/* this.res... */
  yield next
	/* will back */
})

app.use(function *(next) {
  /* ... */
	this.body = 'Hello World\n'
	/* not res.send('Hello World\n') */
})

app.listen(3000)
						

koa: this, next, yield

koa 级联(Cascading)特点

  1. 每一个中间件的逻辑分成了前处理和后处理两部分
  2. 每一个中间件都可以终止,不进入后续(下游)中间件处理流程

koa 四大核心

  1. Application: 异步流程控制 + 级联特性 ...
  2. Context: 挂载各种对象和方法 ...
  3. Request: 定义处理 request 的语法糖 ...
  4. Response: 定义处理 response 的语法糖 ...

app.createContext = function(req, res){
	var context = Object.create(this.context);
	var request = context.request = Object.create(this.request);
	var response = context.response = Object.create(this.response);
	context.app = request.app = response.app = this;
	context.req = request.req = response.req = req;
	context.res = request.res = response.res = res;
	request.ctx = response.ctx = context;
	request.response = response;
	response.request = request;
	/* ... */
};
						

生成 context, request, response,
全部挂载 app 对象

koa

  1. 砍掉业务功能逻辑,API 简洁,实用
  2. 完美的异步流程控制(异常处理)来组合业务逻辑
  3. 级联模式处理流程复杂
  4. 中间件模式,对第三方暴露太多

var app = toa(function *() {
	/* Body */
	this.body = 'Hello World\n'
})

app.use(function *() {
	/* ... */
	/* this.req... */
	/* this.res... */
})

app.listen(3000)
						

toa: this, yield

没有级联怎么办?

  1. 后处理:context.onPreEnd
  2. 终止业务流程:context.end

Toa 的背后是 koa!

异步基础

三种异步原语:promise, thunk, generator

promise 三大特征之一:标准接口


promise.then(function resolved (res) {
	/* ... */
}, function rejected (err) {
	/* ... */
})
						

各种 promise 实现之间,或 promise 与其它异步库之间无缝对接

promise 三大特征之二:总是返回新 promise


promise.then(resolved).then(resolved).then(resolved).catch(rejected)
						

每一链返回新的 promise 对象,展平异步逻辑

promise 三大特征之三:对内部 `return promise` 求值


var promiseX = promiseA.then(function (resA) {
	/* ... */
	return promiseB
}).then(function (resB) {
	/* ... */
	return promiseC
}).then(resolved)
						

乐高积木:任意多的 promise 组合成一个最终的 promise !

thunk 是 callback 的特别形式


fs.readFile(filePath, function (err, data) {
  if (err) return console.error(err)
  console.log(data)
})

function readFile(path) {
	/* 返回 thunk */
  return function(callback) {
    fs.readFile(path, callback)
  }
}
						

CPS -> callback -> thunk

thunk:标准化接口 thunk(callback)


thunk(function (err, res, ...) {
	/* ... */
})
						

thunk 是一个封装了特定任务的函数,运行该函数时任务才开始执行(惰性求值)

thunks:function + promise + co


thunk(cb)(cb)(cb)(cb)(cb)(cb) /* thunk 链*/
						

thunk(1)(function (error, value) {
	should(error).be.equal(null)
	should(value).be.equal(1)
	return Promise.resolve(2)
})(function (error, value) {
	should(error).be.equal(null)
	should(value).be.equal(2)
	return thunk(x)
})(function (error, value) {
	should(error).be.equal(null)
	should(value).be.equal(x)
})(done)
						

generator, 神奇的异步原语


thunk(function *() {
  var p = yield promise
  var t = yield thunk
  var x = yield generator
	/* ... */
})
						

co 或 thunks 异步库使 generator 函数成为了 JS 中最强大的异步流程控制工具

关于 promise, thunk, generator 的入门可参考
阮一峰老师的博客

http://www.ruanyifeng.com

异步流程控制

异常处理机制


app.callback = function(){
  var mw = [respond].concat(this.middleware);
  var fn = co.wrap(compose(mw));
  var self = this;

  return function(req, res){
    res.statusCode = 404;
    var ctx = self.createContext(req, res);
    onFinished(res, ctx.onerror);
    fn.call(ctx).catch(ctx.onerror);
  }
};
						

koa / co 组合异步(同步)业务逻辑原理示意


proto.toListener = function () {
	var body = this.body
  var middleware = this.middleware.slice()
  return function requestListener (req, res) {
    var ctx = createContext(app, req, res, onerror, thunks({
      debug: debug,
      onstop: onstop,
      onerror: onerror
    }))

    ctx.thunk.seq.call(ctx, middleware)(function () {
      return body.call(this, this.thunk)
    })(function () {
      return this.thunk.seq.call(this, this.onPreEnd)(respond)
    })
	}
}
						

Toa / thunks 组合异步(同步)业务逻辑原理示意


issueAPI.createIssue = function *() {
  if (this.token.hasRight < 0) this.throw(401)
  var issue = yield this.parseBody()
  issue = validateIssue(issue)
  if (!issue.content) this.throw(422, 'Issue content required')

  issue = yield issueDao.createIssue({
    title: issue.title,
    content: issue.content,
    createdBy: this.token._id
  })
  this.body = yield fillCreatorAndUpdater(issue)
}
						

Teambition 用户社区代码示意

用同步代码书写异步情怀

灵活的异常处理


userAPI.findOrCreate = function *(user) {
	/* 从 cookie 解出的 user 信息*/
  try {
    user = yield userDao.findById(user._id)
  } catch (err) {
    /* 用户不存在,自动创建用户 */
    user = yield userDao.create(user)
    user.isNew = true
  }
  return yield findUserPreference(user)
}
						

主动 try catch 异常并处理,
或由 koa / Toa 自动处理

koa / Toa 异步小结

  • 业务逻辑总是在 co / thunks 中运行
  • 异步业务应该是 thunk 或 generator 函数,或返回 promise 的函数
  • 但只有 thunk 或 generator 能绑定 `context` 对象,业务逻辑、中间件、模块一般就是 thunk 或 generator 函数
  • 无需关注异常,co / thunks 能捕捉异常,koa / Toa 会自动处理异常。需要主动处理的异常自行 try catch 即可

koa / Toa 组件开发模式


// koa-favicon
module.exports = function(path, options){
  /* 省略代码 */
  return function *favicon(next){
    if ('/favicon.ico' != this.path) return yield next;
    if (!path) return;
    if ('GET' !== this.method && 'HEAD' !== this.method) {
      this.status = 'OPTIONS' == this.method ? 200 : 405;
      this.set('Allow', 'GET, HEAD, OPTIONS');
      return;
    }
    if (!icon) icon = yield fs.readFile(path);
    this.set('Cache-Control', 'public, max-age=' + (maxAge / 1000 | 0));
    this.type = 'image/x-icon';
    this.body = icon;
  };
};
						

中间件模式:处理业务流程


// toa-token
module.exports = function toaToken(app, secretOrPrivateKeys, options) {
	/* 省略代码 */
  app.verifyToken = app.context.verifyToken = verifyToken
  app.signToken = app.context.signToken = function (payload) {
    return jwt.sign(payload, secretOrPrivateKeys[0], options)
  }
  app.decodeToken = app.context.decodeToken = function(token, options) {
    return jwt.decode(token, options)
  }
  Object.defineProperty(app.context, 'token', {
    enumerable: true,
    configurable: false,
    get: function () {/* 省略代码 */}
  })
	/* 省略代码 */
}
						

原型方法模式:提供便捷方法


app.context.someCtxMethod = someCtxMethod(options)
app.request.someReqMethod = someReqMethod(options)
app.response.someResMethod = someResMethod(options)
						

更安全的原型方法模式


// generator function
exports.someServiceA = function *(req) {
	/*service code*/
}
// return thunk
exports.someServiceB = function (user) {
	return function (callback) {
		/*service code*/
	}
}

// use it
var result1 = yield services.someServiceA(this.req)

yield services.someServiceA(this.state.user)
						

小模块组件模式,一个功能模块就是一个 generator 或 thunk 函数

Toa 推荐模式,提供仅需的参数即可,完全控制模块访问权限,更安全

Q / A

https://github.com/toajs