一个请求进来,进入第一个中间件,执行完 yield next 上面的内容后,执行到 yield next ,就会执行第二个中间件,重复上述步骤直至最后一个中间件。最后一个中间件中没有 yield next,就会开始执行倒数第二个 yield next 后的内容,然后再回溯,直到第一个中间件。
我的天哪,这么神奇 (ฅ◑ω◑ฅ)
这么神奇的效果是通过 co + koa-compose 共同完成的,我们先来看 koa-compose(有改动):
function compose(middleware) {
return function *(next) {
if (!next) next = (function *() {})();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
return yield *next;
}
}
这是 koa 控制流的核心,代码很短,也很精辟。
逆序 while,从最后一个中间件开始,next 就是空函数 noop;
(function *() {})()的结果不是开始执行 generator,而是返回一个对象,称为 next 对象,包含 next、throw 方法。将这个对象传入上一个中间件,上一个中间件执行到 yield next 时,koa-compose 外包着的 co 将会自动执行 next 对象上的 next 方法,哈哈是不是有点绕;
在 macaca-cli 和 reliable-master 中,都使用了 koa。koa 是一款优秀的,面向未来的 web 框架,若你还在使用 express,一定要试试 koa。
koa@1.x
要想了解 koa 的运行原理,最好看看它的源码,只有4个文件,每个文件也不长,但却可以支撑起一个 web 应用,真正的麻雀虽小,五脏俱全。
koa 同 express 一样,都继承自 events。暴露出的 Application 上有7个属性,
env
,subdomainOffset
,middleware
,proxy
,context
,request
,response
。我们分别来看这几个属性。1. env
为了区分生产环境和开发环境,一般会在环境变量里加上
export NODE_ENV=production
或export NODE_ENV=development
,这样在调用process.env.NODE_ENV
时就能拿到当前环境了。这是一个约定俗成的『关键字』。或者,在命令行输入NODE_ENV=production node index.js
也有上面的效果,只不过这是一次性,局部的。2. subdomainOffset
这个参数是为了拿到子域名所设置。如域名为
china.asia.news.bbc.com
,subdomainOffset 为2时,调用 request 上的 subdomains,返回的是['news', 'asia', 'china'],因为 com 是顶级域名,bbc 是二级域名,越往前越低。subdomainOffset 为3时,返回的是['asia', 'china']。个人感觉没啥用,只是为了和 express 统一……3. middleware
middleware 数组中存放着多个 generator 函数,middleware 往 koa-compose 模块中传。compose 模块是个典型的 one-function module,作用是顺序执行数组中的函数。
4. proxy
供 request 上的 protocol、host、ips 方法调用。当本应用不是直面用户,而是经过了代理服务器的转发(大部分情况下都是这样的),协议,host,ip 就不能从进入应用的请求中直接获取,只能间接地从一些自定义请求头里取。
5. context
在 context 上挂载了 request 和 response 的方法和少量 context 本身的方法。
6. request & 7. response
request 和 response 上的方法基本上是直接操作http请求本身,比较底层。
我们通过向 koa 发起一条请求来看 koa 是如何工作的,建议对着源码一起读。
先写一个玩具式最简单的 koa 应用。
浏览器访问 http://localhost:3000, 就能看到 Hello World,在终端会看到1 2 3 4。
调用 use 方法,将 generator 函数 push 入 middleware 数组。随后,调用 listen 方法启动 http 服务器。
callback 只要返回一个
function(req, res) {}
函数即可,这就是高阶函数的应用。深入研究 callback 函数,可以看到里面有这么一句:继承自 events 的好处,便于捕捉错误。
如果你对一个请求什么都不做,就会拿到一个404的结果。createContext 函数很简单,把 context,request, response,req(原生请求对象),req(原生响应对象)互相挂载(乱)。在执行完中间件后,再执行 respond 函数返回响应。这就是一次完整的请求和响应。但这当中有很大的一块我们没细讲:koa 的中间件。
koa 中的中间件
中间件类似于 java 中的切面编程,下图能够很好地解释 koa 控制流:
一个请求进来,进入第一个中间件,执行完 yield next 上面的内容后,执行到 yield next ,就会执行第二个中间件,重复上述步骤直至最后一个中间件。最后一个中间件中没有 yield next,就会开始执行倒数第二个 yield next 后的内容,然后再回溯,直到第一个中间件。
我的天哪,这么神奇 (ฅ◑ω◑ฅ)
这么神奇的效果是通过 co + koa-compose 共同完成的,我们先来看 koa-compose(有改动):
这是 koa 控制流的核心,代码很短,也很精辟。
(function *() {})()
的结果不是开始执行 generator,而是返回一个对象,称为 next 对象,包含 next、throw 方法。将这个对象传入上一个中间件,上一个中间件执行到 yield next 时,koa-compose 外包着的 co 将会自动执行 next 对象上的 next 方法,哈哈是不是有点绕;return yield *next;
,koa-compose 把yield *next
交给 co,co 就会开始启动中间件,完成链式调用;扩展阅读:我的blog
macaca-cli 中的 koa
webdriver-server 模块是 macaca-cli 中重要的一部分,起到承上启下,类似于代理服务器的作用。如果你还尚不清楚 macaca-cli 是怎么工作的,我们先通过一个最简单的例子了解一下。
首先我们先来写一个测试用例(取自编写移动端 Macaca 测试用例 [单步调试]):
然后,在终端输入
macaca run --verbose
,启动监听3456端口的 koa,接收从 macaca-wd 模块发来的 http 请求,转发至 driver 层(如macaca-ios
或macaca-android
)上,由 driver 层将 http 请求『翻译』成具体的操作并取得结果后返回给 macaca-wd。旁的不看,单看
.click()
这一句。当 macaca-wd 执行到这句时,会发出一条请求(对应列表请看 macaca-wd 的 api 文档)http://localhost:3456/session/:sessionId/element/:id/click
。在 webdriver-server 中我们写了一长串的协议路由,当 koa 接收到请求后,就会执行对应路由上的 controller,驱动 driver 去试图点击某个元素,等待从 driver 层返回的结果(点击成功/没有该元素……等等),随后将结果包装成一个对象,形如:返回响应给 macaca-wd,由 macaca-wd 判断响应的结果。这就是一句
.click()
的完整历程,而将各种命令串联起来,便可组成一个完整的测试用例。所以,macaca-wd 和 macaca-cli 是完全解耦的,只通过 http 进行沟通。只要你实现了符合 wd 标准的行为,就可以任意调戏 macaca了。
Reliable-master 中的 koa
koa 是一款优秀的 web 框架,优秀在哪儿呢,我们可以把 koa 和它的前辈 express 做一下对比。它们之间最重要的区别在于中间件的调用,这也直接导致了两个框架的风格大相径庭。在 express 中很容易一不小心就写出包含大量回调的代码,不好看不说,调试也费劲,错误捕捉也是『node式』的。koa@1.x 中依托于 co 和 yield,每个异步操作都可以封装在 generator 函数中,从视觉上避免了回调的杂乱,同时,可以方便地捕获异常。
koa 也是一个非常轻量级的框架,轻量到连路由,静态文件,session 管理都没有,非常底层,提供的 api 基本是操作 http 本身。所以,想要运行起一个 koa 应用,必须要依赖其他模块, koa-router,koa-generic-session,koa-static 等。
在 reliable-master 中,koa 的作用就是 web 框架,为用户提供访问入口和可视化的管理平台。
用户权限校验
一般来说,网站会有不同的用户等级,每个等级拥有不同的权限,如超级管理员、管理员、普通用户等。而区别各个用户权限最简单的做法,就是在用户信息里加上某个特定字段,标识该用户的等级。如普通用户的 role 为1,管理员为10,超级管理员为100,这都是可以随意定的。
在 reliable-master 的用户权限校验中间件中,有三级校验,游客,注册用户和管理员,分别对应不同的操作。如游客因为没有 session 信息,会被引导至登录页;而管理员可以访问一些普通用户不能访问的页面。这都是在 router 里结合 koa-router 做的,将校验中间件放在路由对应的 controller 之前,就可以方便地进行校验。
i18n
i18n(internationalization),国际化,因 i 和 n 之间有18个字母而得名。一个面向国际的网站,至少要同时给出中文版和英文版,方便弘扬民族文化精神(雾)。要实现国际化,思路很简单:
lang=en
;lang=en
,这样用户下一次的访问就是英语版本的网页了。但这样的解决方案灵活性肯定是不够的,要是想增加俄语版本,就要多弄一套俄语的网页,浪费生产力。所以,网页里的文字应该是动态获取的,可以根据语言版本信息自动替换文字。原先
<p>登录</p>
、<p>Login</p>
统一成<p>{this.gettext('login')}</p>
(React 实现,其他同理)。再在资源文件里放多个语言文件{login: '登录'}
、{login: 'login'}
。若是想增加一个语言版本,增加一个语言文件即可{login: 'войти'}
。reliable-master 中的实现可以看这里。持续集成服务
自动化测试服务与持续集成,可谓天生一对,持续集成可以提前发现问题,减少开发成本。 我们来看看 reliable-master 是如何做持续集成的。
我们先准备一个 task 表,里面存放测试完成和还未开始测试的任务。还要再准备一个定时任务,每隔几秒扫描 task 表,观察是否有新增任务。若有新增任务,就把任务信息发送给空闲的机器,在该台机器上进行自动化测试。我们为 push 至 repo 的操作增加一个钩子,钩子函数的作用就是将提交代码的仓库和分支放入 task 表(假设跑任务只需要这两个信息)。当代码提交至 repo 时,调用钩子函数,自动触发自动化测试任务。
这当中所涉及到的模块解读,鉴于篇幅原因不展开,可以看这里⬇️
在不久的两三个月后,v8 将会有 async/await,届时 koa 也会推出 koa@2.0,koa@2.0 基于 async/await,提供更好的异步编程体验。现在借助 babel 也可以运行,但不推荐运行在生产环境中。
总结
除了 Koa,在 Macaca 中,还有很多有意思的技术细节。本文抛砖引玉,感兴趣的同学可以向我们提交 pr,或者点点 Star 哦~