Koa2-入门教程

3次阅读

共计 13219 个字符,预计需要花费 34 分钟才能阅读完成。

完整 Demo 地址

里面 demo 都是自己写的,保证能跑,至於环境问题我就不敢保证了。懒得写就去上面搬走看,懒得搬就直接看文章,大部分代码连输出信息都给你们了。
koa-demo

官网介绍

koa 是由 Express 原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的 Web 框架。使用 koa 编写 web 应用,通过组合不同的 generator,可以免除重复繁琐的回调函数嵌套,并极大地提升错误处理的效率。koa 不在内核方法中绑定任何中间件,它仅仅提供了一个轻量优雅的函数库,使得编写 Web 应用变得得心应手。

前期准备

我们首先安装一些必要库先,根据个人选择可以使用 yarn,cnpm,或者 npm 都行。

KOA 框架

yarn add koa

这还不够,因为 Koa 依赖 node v7.6.0 或 ES2015 及更高版本和 async 方法支持. 你们可以根据自身需要安装

Babel register

transform-async-to-generator 或 transform-async-to-module-method

因为我用到 Nodejs10.0,所以不需要安装这些东西,就不说了。

简单入门

惯例拿创建应用程序作为一个框架的入门例子。

const Koa = require('koa'),
  app = new Koa();

app
  .use(async ctx => {ctx.body = '暗号:Hello World';})
  .listen(3000);

console.log('已建立连接,效果请看 http://127.0.0.1:3000/');

代码一目了然,不废话了。
(完整代码可以执行 koa-demo 的 lesson1 查看效果)

Favicon.ico

所谓 favicon,即 Favorites Icon 的缩写,顾名思义,便是其可以让浏览器的收藏夹中除显示相应的标题外,还以图标的方式区别不同的网站。当然,这不是 Favicon 的全部,根据浏览器的不同,Favicon 显示也有所区别:在大多数主流浏览器如 FireFox 和 Internet Explorer (5.5 及以上版本)中,favicon 不仅在收藏夹中显示,还会同时出现在地址栏上,这时用户可以拖曳 favicon 到桌面以建立到网站的快捷方式;除此之外,标签式浏览器甚至还有不少扩展的功能,如 FireFox 甚至支持动画格式的 favicon 等。
问题在於这裡代码浏览器会自动发起请求网站根目录的这个图标,干扰测试,所以接下来的打印结果大家无视 Favicon.ico 请求就好。

级联

Koa 应用程序是一个包含一组中间件函数的对象,它是按照类似堆栈的方式组织和执行的。
当一个中间件调用 next() 则该函数暂停并将控制传递给定义的下一个中间件。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为。(用一种比较相似的比喻就是中间件相当於一次 DOM 事件,从事件捕捉到事件冒泡的过程)。

const Koa = require('koa'),
  app = new Koa();

// 一层中间件
app.use((ctx, next) => {console.log('请求资源:' + ctx.url);
  console.log('一层中间件控制传递下去');
  next();
  console.log('一层中间件控制传递回来');
  ctx.body = '暗号:Day Day Up';
});

// 二层中间件
app.use((ctx, next) => {console.log('二层中间件控制传递下去');
  next();
  console.log('二层中间件控制传递回来');
});

// response
app.use(ctx => {console.log('输出 body');
  ctx.body = '暗号:Good Good Study';
});

app.listen(3000);
console.log('已建立连接,效果请看 http://127.0.0.1:3000/');

// 一层中间件控制传递下去
// 二层中间件控制传递下去
// 输出 body
// 二层中间件控制传递回来
// 一层中间件控制传递回来

(完整代码可以执行 koa-demo 的 lesson2 查看效果)

从上面结果可以看出每请求一次资源都会经过所有中间件,并且在执行到最尾部中间件时候会将控制权反向传递,输出结果是头部的 body 覆盖尾部的 body。
说实话没试过这种方式,有点不习惯。

上下文(Context)

Koa Context 将 Nodejs 的 requestresponse 对象封装到单个对象中,每个请求都将创建一个 Context,并在中间件中作为接收器引用,或者 ctx 标识符,许多上下文的访问器和方法直接委托给它们的 ctx.requestctx.response
我们可以直接打印出来 ctx 对象看看有什么。

const Koa = require('koa'),
  app = new Koa();

// response
app.use(async ctx => {console.log('ctx:', ctx);
});

app.listen(3000);
console.log('已建立连接,效果请看 http://127.0.0.1:3000/');

/*
ctx:{ request:
   { method: 'GET',
     url: '/',
     header:
      { host: 'localhost:3000',
        connection: 'keep-alive',
        'cache-control': 'max-age=0',
        'upgrade-insecure-requests': '1',
        'user-agent':
         'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.89 Safari/537.36',
        accept:
         'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*!/!*;q=0.8',
        'accept-encoding': 'gzip, deflate, sdch',
        'accept-language': 'zh-CN,zh;q=0.8',
        cookie:
         'loginInfo={"username":"abc","password":"MjIyMjIy","rememberMe":1}' } },
  response: {status: 404, message: 'Not Found', header: {} },
  app: {subdomainOffset: 2, proxy: false, env: 'development'},
  originalUrl: '/',
  req: '<original node req>',
  res: '<original node res>',
  socket: '<original node socket>' }*/

(完整代码可以执行 koa-demo 的 lesson3 查看效果)

请求(Request)

Koa Request 对象是在 Nodejs 的 vanilla 请求对象之上的抽象,提供了诸多对 HTTP 服务器开发有用的功能。Koa 的 Request 对象包括由 acceptsnegotiator 提供的有用的内容协商实体。

  • request.accepts(types)
  • request.acceptsEncodings(types)
  • request.acceptsCharsets(charsets)
  • request.acceptsLanguages(langs)
  1. 如果没有提供类型,则返回所有可接受的类型;
  2. 如果提供多种类型,将返回最佳匹配;
  3. 如果没有找到匹配项,则返回一个 false;

因为用法都一个样,挑选一个来讲解。

request.accepts(types)

检查给定的类型是否可以接受,type 值可能是一个或多个 mime 类型的字符串或数组,如果可以就返回最佳匹配类型字符串,否则返回 false。

const Koa = require('koa'),
  app = new Koa();

app
  .use(async ctx => {switch (ctx.accepts('json', 'html', 'text')) {
      case 'json':
        ctx.type = 'json';
        ctx.body = '<p> 匹配类型 json</p>';
        break;
      case 'html':
        ctx.type = 'html';
        ctx.body = '<p> 匹配类型 html</p>';
        break;
      case 'text':
        ctx.type = 'text';
        ctx.body = '<p> 匹配类型 text</p>';
        break;
      default:
        ctx.throw(406, 'json, html, or text only');
    }
  })
  .listen(3000);
console.log('已建立连接,效果请看 http://127.0.0.1:3000/');

(完整代码可以执行 koa-demo 的 lesson4 查看效果)

实际开发中需要更多不同的处理细节,所以我们可以把内容设置成模板 template1.html 引用。

<!DOCTYPE html>
<html lang="en" dir="ltr">

  <head>
    <meta charset="utf-8">
    <title></title>
  </head>

  <body>
    <p> 匹配类型 html</p>
  </body>

</html>

(完整代码可以执行 koa-demo 的 template1 查看效果)

const Koa = require('koa'),
  fs = require('fs'),
  app = new Koa();

app
  .use(async ctx => {switch (ctx.accepts('json', 'html', 'text')) {
      case 'html':
        ctx.type = 'html';
        ctx.body = fs.createReadStream('./template1.html');
        break;
      default:
        ctx.throw(406, 'json, html, or text only');
    }
  })
  .listen(3000);
console.log('已建立连接,效果请看 http://127.0.0.1:3000/');

(完整代码可以执行 koa-demo 的 lesson5 查看效果)

路由

其实我们上面的代码已经是原始路由的用法了,我们增加请求资源的判断就可以了,另外新增一个 template2.html 模板切换看效果

<!DOCTYPE html>
<html lang="en" dir="ltr">

  <head>
    <meta charset="utf-8">
    <title></title>
  </head>

  <body>
    <p>template2</p>
  </body>

</html>

(完整代码可以执行 koa-demo 的 template2 查看效果)
然后我们去掉类型判断等代码看效果,直接写死 html 即可,不然 type 默认为空,打开页面会触发下载的,不信你们去掉设置类型那行代码看看。

const Koa = require('koa'),
  fs = require('fs'),
  app = new Koa();

app
  .use(async ctx => {console.log('type:' + ctx.type);
    switch (ctx.url) {
      case '/':
        ctx.type = 'html';
        ctx.body = fs.createReadStream('./template1.html');
        break;
      case '/template2':
        ctx.type = 'html';
        ctx.body = fs.createReadStream('./template2.html');
        break;
      default:
        ctx.throw(406, 'json, html, or text only');
    }
  })
  .listen(3000);
console.log('已建立连接,效果请看 http://127.0.0.1:3000/');

(完整代码可以执行 koa-demo 的 lesson6 查看效果)
执行脚本之后会默认看到 template2.html 模板内容,手动换成 http://127.0.0.1:3000/template2 如果没设置 type 在 Chrome 会看到下载弹窗,其他浏览器没试过。

实际开发我们不会这么繁琐的去区分路由,上面说过 koa 不在内核方法中绑定任何中间件,它仅仅提供了一个轻量优雅的函数库,所以我们需要安装一个路由中间件。
koa-route3.2.0,上次推送已经是两年前了,如果不是放弃维护就是已经很稳定了。

yarn add koa-route

如果你需要使用完整特性的路由库可以看 koa-router
这里简单展示 koa-route 用法。

const Koa = require('koa'),
  _ = require('koa-route'),
  fs = require('fs'),
  app = new Koa();

const route = {
  index: ctx => {
    //doSomethings
    ctx.type = 'html';
    ctx.body = fs.createReadStream('./template1.html');
  },
  template2: ctx => {
    //doSomethings
    ctx.type = 'html';
    ctx.body = fs.createReadStream('./template2.html');
  },
};

app
  .use(_.get('/', route.index))
  .use(_.get('/template2', route.template2))
  .listen(3000);
console.log('已建立连接,效果请看 http://127.0.0.1:3000/');

(完整代码可以执行 koa-demo 的 lesson7 查看效果)

响应状态

如果代码运行过程中发生错误,我们需要把错误信息返回给用户。

ctx.throw([status], [msg], [properties])

等价于

const err = new Error(msg);
err.status = status;
err.expose = true;
throw err;

注意:这些是用户级错误,并用 err.expose 标记,这意味着消息适用于客户端响应,这通常不是错误消息的内容,因为您不想泄漏故障详细信息。

100 “continue”
101 “switching protocols”
102 “processing”
200 “ok”
201 “created”
202 “accepted”
203 “non-authoritative information”
204 “no content”
205 “reset content”
206 “partial content”
207 “multi-status”
208 “already reported”
226 “im used”
300 “multiple choices”
301 “moved permanently”
302 “found”
303 “see other”
304 “not modified”
305 “use proxy”
307 “temporary redirect”
308 “permanent redirect”
400 “bad request”
401 “unauthorized”
402 “payment required”
403 “forbidden”
404 “not found”
405 “method not allowed”
406 “not acceptable”
407 “proxy authentication required”
408 “request timeout”
409 “conflict”
410 “gone”
411 “length required”
412 “precondition failed”
413 “payload too large”
414 “uri too long”
415 “unsupported media type”
416 “range not satisfiable”
417 “expectation failed”
418 “I’m a teapot”
422 “unprocessable entity”
423 “locked”
424 “failed dependency”
426 “upgrade required”
428 “precondition required”
429 “too many requests”
431 “request header fields too large”
500 “internal server error”
501 “not implemented”
502 “bad gateway”
503 “service unavailable”
504 “gateway timeout”
505 “http version not supported”
506 “variant also negotiates”
507 “insufficient storage”
508 “loop detected”
510 “not extended”
511 “network authentication required”

状态码错误

有两种写法,ctx.throw(状态码) 或者 ctx.status = 状态码,它们都会自动返回默认文字信息,区别在于两者设置返回信息的方式。
注意:默认情况下,response.status 设置为 404 而不是像 node 的 res.statusCode 那样默认为 200。

const Koa = require('koa'),
  _ = require('koa-route'),
  app = new Koa();

const router = {
  '403': ctx => {
    //doSomethings
    ctx.throw(403, '403 啦!');
  },
  '404': ctx => {
    //doSomethings
    ctx.status = 404;
    ctx.body = `<p>404 啦!</p>`;
  },
};

app
  .use(_.get('/403', router[403]))
  .use(_.get('/404', router[404]))
  .listen(3000);
console.log('已建立连接,效果请看 http://127.0.0.1:3000/');

(完整代码可以执行 koa-demo 的 lesson8 查看效果)
你们可以分别打开 http://localhost:3000/403http://localhost:3000/404 看输出结果。

错误监听

const Koa = require('koa'),
  _ = require('koa-route'),
  app = new Koa();

const router = {
  index: ctx => {
    //doSomethings
    ctx.throw(500, '我是故意的!');
  },
};

app
  .use(_.get('/', router.index))
  .on('error', (err, ctx) => {console.error('error', err);
  })
  .listen(3000);
console.log('已建立连接,效果请看 http://127.0.0.1:3000/');
/*
error { InternalServerError: 我是故意的!at Object.throw (C:\project\test\koa-demo\node_modules\koa\lib\context.js:93:11)
    at Object.index (C:\project\test\koa-demo\lesson9.js:8:18)
    at C:\project\test\koa-demo\node_modules\koa-route\index.js:39:44
    at dispatch (C:\project\test\koa-demo\node_modules\koa-compose\index.js:42:32)
    at C:\project\test\koa-demo\node_modules\koa-compose\index.js:34:12
    at Application.handleRequest (C:\project\test\koa-demo\node_modules\koa\lib\application.js:150:12)
    at Server.handleRequest (C:\project\test\koa-demo\node_modules\koa\lib\application.js:132:19)
    at Server.emit (events.js:182:13)
    at parserOnIncoming (_http_server.js:654:12)
    at HTTPParser.parserOnHeadersComplete (_http_common.js:109:17) message: '我是故意的!' }*/

(完整代码可以执行 koa-demo 的 lesson9 查看效果)

错误捕捉

你也能直接使用 try...catch() 直接处理,但是“error”监听事件就不会再接收该错误信息。

const Koa = require('koa'),
  app = new Koa();

const err = async (ctx, next) => {
    try {await next();
    } catch (err) {
      ctx.status = 404;
      ctx.body = `<p> 你看看终端有没打印错误!</p>`;
    }
  },
  index = ctx => {ctx.throw(500);
  };

app
  .use(err)
  .use(index)
  .on('error', function(err) {console.error('error', err);
  })
  .listen(3000);
console.log('已建立连接,效果请看 http://127.0.0.1:3000/');

(完整代码可以执行 koa-demo 的 lesson10 查看效果)
注意,这裡的错误处理如果使用 ctx.throw()方法的话能被“error”事件监听到,不是因為该方法会再拋出新的错误。

const Koa = require('koa'),
  app = new Koa();

const err = async (ctx, next) => {
    try {await next();
    } catch (err) {ctx.throw(404, '你看看终端有没打印错误!');
    }
  },
  index = ctx => {ctx.throw(500);
  };

app
  .use(err)
  .use(index)
  .on('error', function(err) {console.error('error', err);
  })
  .listen(3000);
console.log('已建立连接,效果请看 http://127.0.0.1:3000/');

(完整代码可以执行 koa-demo 的 lesson1 查看效果)1
如果想同时触发错误监听,KOA 也提供了 emit 方法可以实现。

const Koa = require('koa'),
  app = new Koa();

const err = async (ctx, next) => {
    try {await next();
    } catch (err) {
      ctx.status = 404;
      ctx.body = `<p> 你看看终端有没打印错误!</p>`;
      ctx.app.emit('error', err, ctx);
    }
  },
  index = ctx => {ctx.throw(500);
  };

app
  .use(err)
  .use(index)
  .on('error', function(err) {console.error('error', err);
  })
  .listen(3000);
console.log('已建立连接,效果请看 http://127.0.0.1:3000/');

/*
error { InternalServerError: Internal Server Error
    at Object.throw (C:\project\test\koa-demo\node_modules\koa\lib\context.js:93:11)
    at index (C:\project\test\koa-demo\lesson12.js:14:18)
    at dispatch (C:\project\test\koa-demo\node_modules\koa-compose\index.js:42:32)
    at err (C:\project\test\koa-demo\lesson12.js:6:19)
    at dispatch (C:\project\test\koa-demo\node_modules\koa-compose\index.js:42:32)
    at C:\project\test\koa-demo\node_modules\koa-compose\index.js:34:12
    at Application.handleRequest (C:\project\test\koa-demo\node_modules\koa\lib\application.js:150:12)
    at Server.handleRequest (C:\project\test\koa-demo\node_modules\koa\lib\application.js:132:19)
    at Server.emit (events.js:182:13)
    at parserOnIncoming (_http_server.js:654:12) message: 'Internal Server Error' }
*/

(完整代码可以执行 koa-demo 的 lesson11 查看效果)

静态资源

聪明的人在上面代码就能看出一些问题,还记得我们说过忽略 Favicon.ico 的请求麼。我们不仅仅有页面的请求,还有其他资源的请求。
我们现在还是通过 url 判断返回页面,如果是其他静态资源如图片那些又怎么办?这裡介绍一下依赖库 koa-static5.0.0,

yarn add koa-static
--------------------------
require('koa-static')(root, opts)

通过设置根目录和可选项会配置静态资源查找路径,我们先创建一个 img 目录存放一张图片,然后在 template3.html 引用,再设置路径 require('koa-static')(__dirname + '/img/'),它会自动到指定目录下查找资源。

<!DOCTYPE html>
<html lang="en" dir="ltr">

  <head>
    <meta charset="utf-8">
    <title></title>
  </head>

  <body>
    <p> 没错,我就是首页 </p>
    <img src="./1.gif"/>
  </body>

</html>

(完整代码可以执行 koa-demo 的 template3 查看效果)

const Koa = require('koa'),
  _ = require('koa-route'),
  serve = require('koa-static')(__dirname + '/img/'),
  fs = require('fs'),
  app = new Koa();

const router = {
  index: ctx => {
    //doSomethings
    ctx.type = 'html';
    ctx.body = fs.createReadStream('./template3.html');
  },
};

app
  .use(serve)
  .use(_.get('/', router.index))
  .listen(3000);
console.log('已建立连接,效果请看 http://127.0.0.1:3000/');

(完整代码可以执行 koa-demo 的 lesson13 查看效果)
如果你还是有些不懂的话修改下路径,require(‘koa-static’)(__dirname),然后图片地址换成 “./img/1.gif”。你就看到还是能找到对应资源。

中间件管理

随着项目开发你可能会安装越来越多的中间件,所有可以使用 koa-compose 做中间件管理。这个很多库都有类似的中间件,用於简化中间件的使用。
上面我们用来讲解 koa 级联的那个例子可以直接拿来修改使用。

const Koa = require('koa'),
  compose = require('koa-compose'),
  app = new Koa();

// 一层中间
const mid1 = (ctx, next) => {console.log('请求资源:' + ctx.url);
  console.log('一层中间件控制传递下去');
  next();
  console.log('一层中间件控制传递回来');
};

// 二层中间
const mid2 = (ctx, next) => {console.log('二层中间件控制传递下去');
  next();
  console.log('二层中间件控制传递回来');
};

// response
const mid3 = ctx => {console.log('输出 body');
  ctx.body = '暗号:Hello World';
};

app.use(compose([mid1, mid2, mid3])).listen(3000);
console.log('已建立连接,效果请看 http://127.0.0.1:3000/');
// 请求资源:/
// 一层中间件控制传递下去
// 二层中间件控制传递下去
// 输出 body
// 二层中间件控制传递回来
// 一层中间件控制传递回来

(完整代码可以执行 koa-demo 的 lesson14 查看效果)
可以看出大概原理就是把引用多个中间件的使用方式改成将多个中间件组装成一个使用。

请求处理

我们处理请求的时候可以用 koa-body 解析请求体。

A full-featured koa body parser middleware. Support multipart, urlencoded and json request bodies. Provides same functionality as Express’s bodyParser – multer. And all that is wrapped only around co-body and formidable.

一个功能丰富的 body 解析中间件,支持多部分,urlencoded,json 请求体,提供 Express 里 bodyParse 一样的函数方法

直接安装依赖

yarn add koa-body

新建一个提交页面

<!DOCTYPE html>
<html lang="en" dir="ltr">

  <head>
    <meta charset="utf-8">
    <title></title>
  </head>

  <body>
    <form class=""action="/upload"method="post">
      <input type="text" name="name" value="">
      <button type="submit" name="button"> 提交 </button>
    </form>
  </body>

</html>

(完整代码可以执行 koa-demo 的 template4 查看效果)

可以输出格式看看效果

const Koa = require('koa'),
  koaBody = require('koa-body'),
  _ = require('koa-route'),
  fs = require('fs'),
  app = new Koa();

const router = {
    index: ctx => {
      //doSomethings
      ctx.type = 'html';
      ctx.body = fs.createReadStream('./template4.html');
    },
  },
  upload = ctx => {console.log(ctx.request.body);
    ctx.body = `Request Body: ${JSON.stringify(ctx.request.body)}`;
  };

app
  .use(koaBody())
  .use(_.get('/', router.index))
  .use(_.post('/upload', upload))
  .listen(3000);
console.log('已建立连接,效果请看 http://127.0.0.1:3000/');

(完整代码可以执行 koa-demo 的 lesson15 查看效果)
提交内容之后在页面和终端都能看到 body 内容。

参考资料

Koa (koajs)
Koa examples
Koa 框架教程

正文完
 0