Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Koa 在 Macaca 中的实践 #18

Open
brunoyang opened this issue May 4, 2016 · 0 comments
Open

Koa 在 Macaca 中的实践 #18

brunoyang opened this issue May 4, 2016 · 0 comments

Comments

@brunoyang
Copy link
Owner

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

[email protected]

要想了解 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 控制流的核心,代码很短,也很精辟。
0. 逆序 while,从最后一个中间件开始,next 就是空函数 noop;

  1. (function *() {})()的结果不是开始执行 generator,而是返回一个对象,称为 next 对象,包含 next、throw 方法。将这个对象传入上一个中间件,上一个中间件执行到 yield next 时,koa-compose 外包着的 co 将会自动执行 next 对象上的 next 方法,哈哈是不是有点绕;
  2. while 执行完后,next 指向第一个中间件;
  3. 执行到return yield *next;,koa-compose 把yield *next交给 co,co 就会开始启动中间件,完成链式调用;
  4. 何时停止呢,当 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式』的。[email protected] 中依托于 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个字母而得名。一个面向国际的网站,至少要同时给出中文版和英文版,方便弘扬民族文化精神(雾)。要实现国际化,思路很简单:
0. 在服务器上放一个配置文件,内有默认的语言信息(默认中文),并在 footer 或 header 上加个选择语言的按钮,再准备两个版本的网页;
0. 增加一个 i18n 中间件,依此按照查询串、cookie 和网站配置返回某个语言版本的网页;
0. 用户第一次访问时,往响应的 cookie 带上中文语言版本信息;
0. 用户是英语用户,手动选择英语版本时,会向网站发起请求,查询串里带lang=en
0. 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 也会推出 [email protected][email protected] 基于 async/await,提供更好的异步编程体验。现在借助 babel 也可以运行,但不推荐运行在生产环境中。

总结

除了 Koa,在 Macaca 中,还有很多有意思的技术细节。本文抛砖引玉,感兴趣的同学可以向我们提交 pr,或者点点 Star 哦~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant