brunoyang / blog

134 stars 13 forks source link

Koa 在 Macaca 中的实践 #18

Open brunoyang opened 8 years ago

brunoyang commented 8 years ago

macaca-clireliable-master 中,都使用了 koa。koa 是一款优秀的,面向未来的 web 框架,若你还在使用 express,一定要试试 koa。

koa@1.x

要想了解 koa 的运行原理,最好看看它的源码,只有4个文件,每个文件也不长,但却可以支撑起一个 web 应用,真正的麻雀虽小,五脏俱全

koa 同 express 一样,都继承自 events。暴露出的 Application 上有7个属性,envsubdomainOffsetmiddlewareproxycontextrequestresponse。我们分别来看这几个属性。

1. env

为了区分生产环境和开发环境,一般会在环境变量里加上export NODE_ENV=productionexport 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 应用。

const app = require('koa')();

app.use(function *(next) {
  console.log(1);
  yield next;
  console.log(4);
});

app.use(function *(next) {
  console.log(2);
  yield next;
  console.log(3);
});

app.use(function *() {
  this.body = 'Hello World';
});

app.listen(3000);

浏览器访问 http://localhost:3000, 就能看到 Hello World,在终端会看到1 2 3 4。

调用 use 方法,将 generator 函数 push 入 middleware 数组。随后,调用 listen 方法启动 http 服务器。

var server = http.createServer(this.callback());

callback 只要返回一个function(req, res) {}函数即可,这就是高阶函数的应用。深入研究 callback 函数,可以看到里面有这么一句:

if (!this.listeners('error').length) this.on('error', this.onerror);

继承自 events 的好处,便于捕捉错误。

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

如果你对一个请求什么都不做,就会拿到一个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(有改动):

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 控制流的核心,代码很短,也很精辟。

  1. 逆序 while,从最后一个中间件开始,next 就是空函数 noop;
  2. (function *() {})()的结果不是开始执行 generator,而是返回一个对象,称为 next 对象,包含 next、throw 方法。将这个对象传入上一个中间件,上一个中间件执行到 yield next 时,koa-compose 外包着的 co 将会自动执行 next 对象上的 next 方法,哈哈是不是有点绕;
  3. while 执行完后,next 指向第一个中间件;
  4. 执行到return yield *next;,koa-compose 把yield *next交给 co,co 就会开始启动中间件,完成链式调用;
  5. 何时停止呢,当 co 执行 next 返回的 done 为 true 时,是时候结束了。

扩展阅读:我的blog

macaca-cli 中的 koa

webdriver-server 模块是 macaca-cli 中重要的一部分,起到承上启下,类似于代理服务器的作用。如果你还尚不清楚 macaca-cli 是怎么工作的,我们先通过一个最简单的例子了解一下。

首先我们先来写一个测试用例(取自编写移动端 Macaca 测试用例 [单步调试]):

const wd = require('macaca-wd');
const driver = wd.promiseChainRemote({
  host: 'localhost',
  port: 3456
});

driver.init({
  platformName: 'ios',
  platformVersion: '9.3',
  deviceName: 'iPhone 6s',
  app: '/Users/XXX/Code/macaca/macaca-ios-test-sample/app/ios-app-bootstrap.zip'
});

driver
  .waitForElementByXPath('//UIATextField[1]')
  .sendKeys('loginName')
  .waitForElementByXPath('//UIASecureTextField[1]')
  .sendKeys('123456')
  .sleep(1000)
  .sendKeys('\n')
  .waitForElementByName('Login')
  .click();

然后,在终端输入macaca run --verbose,启动监听3456端口的 koa,接收从 macaca-wd 模块发来的 http 请求,转发至 driver 层(如macaca-iosmacaca-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 层返回的结果(点击成功/没有该元素……等等),随后将结果包装成一个对象,形如:

{
  sessionId, // session,从请求中取得
  status, // wd 定义的状态码,0为正常,其他数字均为报错
  value // 返回的结果
}

返回响应给 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-routerkoa-generic-sessionkoa-static 等。

reliable-master 中,koa 的作用就是 web 框架,为用户提供访问入口和可视化的管理平台。

用户权限校验

一般来说,网站会有不同的用户等级,每个等级拥有不同的权限,如超级管理员、管理员、普通用户等。而区别各个用户权限最简单的做法,就是在用户信息里加上某个特定字段,标识该用户的等级。如普通用户的 role 为1,管理员为10,超级管理员为100,这都是可以随意定的。

reliable-master 的用户权限校验中间件中,有三级校验,游客,注册用户和管理员,分别对应不同的操作。如游客因为没有 session 信息,会被引导至登录页;而管理员可以访问一些普通用户不能访问的页面。这都是在 router 里结合 koa-router 做的,将校验中间件放在路由对应的 controller 之前,就可以方便地进行校验。

i18n

i18n(internationalization),国际化,因 i 和 n 之间有18个字母而得名。一个面向国际的网站,至少要同时给出中文版和英文版,方便弘扬民族文化精神(雾)。要实现国际化,思路很简单:

  1. 在服务器上放一个配置文件,内有默认的语言信息(默认中文),并在 footer 或 header 上加个选择语言的按钮,再准备两个版本的网页;
  2. 增加一个 i18n 中间件,依此按照查询串、cookie 和网站配置返回某个语言版本的网页;
  3. 用户第一次访问时,往响应的 cookie 带上中文语言版本信息;
  4. 用户是英语用户,手动选择英语版本时,会向网站发起请求,查询串里带lang=en
  5. i18n 中间件得到查询串信息,给 cookie 打上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 哦~