重学Node.js及其框架(Express, Koa, egg.js) 之 Koa框架

1,826 阅读8分钟

总结自 Coderwhy的nodejs课程。

官网 koa.bootcss.com/#applicatio…

Koa的介绍

  • node.js的下一代web框架;
  • Koa旨在为Web应用程序和API提供更小、更丰富和更强大的能力;
  • 相对于express具有更强的异步处理能力;
  • Koa的核心代码只有1600+行,是一个更加轻量级的框架,我们可以根据需要安装和使用中间件;

Koa初体验

    const Koa = require('koa');

    const app = new Koa();

    app.use((ctx, next) => {
      ctx.response.body = "Hello Koa";
    });

    app.listen(8000, () => {
      console.log("koa初体验服务器启动成功~");
    });

通过上面的例子,我们来介绍一下中间件中的两个参数。

  • ctx: 上下文(Context)对象;
    • koa并没有像express一样,将req和res分开,而是将它们作为ctx的属性
    • ctx.req 为Node 的 request 对象.
    • ctx.res 为Node 的 response 对象.
    • ctx.response 为Koa 的 response 对象.
    • ctx.request 为Koa 的 request 对象.
  • next: 终止该中间件使用,调用下一个中间件。

Koa中使用中间件

Koa中注册中间件只能通过use()来注册,而且只能传递一个中间件,不能匹配路径。

    app.use((ctx, next) => {
      ctx.response.body = "Hello Koa";
    });

使用路由

由于koa中未给我们提供路由处理,如果想要做复杂的路由匹配,我们就需要安装第三方库来帮助我们处理路由。 安装koa-router库。

注册路由

    // users.js
    const Router = require('koa-router');

    const userRouter = new Router({prefix: "/users"});

    userRouter.get('/', (ctx, next) => {
      ctx.response.body = "User Lists~";
    });

    userRouter.put('/', (ctx, next) => {
      ctx.response.body = "put request~";
    });


    module.exports = userRouter;

使用路由

    // index.js
    const Koa = require('koa');

    const userRouter = require('./router/user');

    const app = new Koa();

    app.use(userRouter.routes());
    app.use(userRouter.allowedMethods());

    app.listen(8000, () => {
      console.log("koa路由服务器启动成功~");
    });

从上面的例子可以看出

  • 当如路由文件,在app中将router.routes()注册为中间件
  • router.allowedMethods(): 他是用来判断我们设置了那些请求方法。如果只设置get请求,那么我们请求post,patch,delete等方法将会报错。Method Not Allowed,状态码:405;

解析请求传递的参数

解析params参数

我们直接通过ctx.request.params就可以获取到。当未使用路由时,他不会被解析。因为他不知道怎么解析,未给出解析对应的键名。

    const Koa = require('koa');
    const app = new Koa();
    const Router = require('koa-router');

    const userRouter = new Router({ prefix: '/users' });
    // http://127.0.0.1:8000/users/9?name=zh&age=20
    userRouter.get('/:id', (ctx, next) => {
      console.log(ctx.request.url);
      console.log(ctx.request.params);
      console.log(ctx.request.query);
      /**
      * /users/9?name=zh&age=20
      *  { id: '9' }
      *  [Object: null prototype] { name: 'zh', age: '20' }
      */
    })

    app.use(userRouter.routes());

    app.listen(8000, () => {
      console.log("参数处理服务器启动成功~");
    });

解析query参数

我们直接通过ctx.request.query就可以获取到。

    const Koa = require('koa');
    const app = new Koa();
    const Router = require('koa-router');

    const userRouter = new Router({ prefix: '/users' });
    // http://127.0.0.1:8000/users/9?name=zh&age=20
    userRouter.get('/:id', (ctx, next) => {
      console.log(ctx.request.url);
      console.log(ctx.request.params);
      console.log(ctx.request.query);
      /**
      * /users/9?name=zh&age=20
      *  { id: '9' }
      *  [Object: null prototype] { name: 'zh', age: '20' }
      */
    })

    app.use(userRouter.routes());

    app.listen(8000, () => {
      console.log("参数处理服务器启动成功~");
    });

解析请求中的json格式数据

Koa没有像Express中那样提供内置的中间件,所以我们需要使用第三方库koa-bodyparser解析。将解析后的参数放在ctx.request.body上。

    const bodyParser = require('koa-bodyparser');
    app.use((ctx, next) => {
      console.log("body===json", ctx.request.body);
      ctx.response.body = "Hello World";
    });

解析请求中的x-www-from-urlencoded格式数据

Koa没有像Express中那样提供内置的中间件,所以我们需要使用第三方库koa-bodyparser解析。将解析后的参数放在ctx.request.body上。

    const bodyParser = require('koa-bodyparser');
    app.use((ctx, next) => {
      console.log("body===json", ctx.request.body);
      ctx.response.body = "Hello World";
    });

解析请求中的form-data格式数据

他们的用法就等同于expres中的multer使用

我们需要安装koa-multer库来帮助我们解析。这个库和Express中解析文件数据差不多。不要将它作为全局中间件来使用,上传文件和非文件的form-data格式的数据,可能会产生冲突。 并且它将数据挂载到的是nodejs中的request.body上的。通过ctx.req.body获取

github.com/koajs/multe…

解析非文件的表单数据

    const Koa = require('koa');
    const Router = require('koa-router');
    const multer = require('koa-multer');

    const app = new Koa();
    const uploadRouter = new Router({prefix: '/upload'});

    const upload = multer();

    uploadRouter.post('/', upload.any(), (ctx, next) => {
      console.log(ctx.req.body);
      ctx.response.body = "解析成功~";
    });

    app.use(uploadRouter.routes());

    app.listen(8000, () => {
      console.log("koa初体验服务器启动成功~");
    });

解析文件的表单数据

我们可以指定上传的文件名和后缀。也可以上系统自动分配文件名。

    const Koa = require('koa');
    const Router = require('koa-router');
    const multer = require('koa-multer');

    const app = new Koa();
    const uploadRouter = new Router({prefix: '/upload'});

    const uploadSingle = multer({
      dest: './uploads/avatar'
    });

    const uploadArray = multer({
      dest: './uploads/picture'
    });

    // 上传单个文件,而且他只能上传单文件。
    uploadRouter.post('/avatar', uploadSingle.single('avatar'), (ctx, next) => {
      console.log(ctx.req.file);
      ctx.response.body = "上传头像成功~";
    });

    // 上传多个文件
    uploadRouter.post('/picture', uploadArray.array('picture'), (ctx, next) => {
      console.log(ctx.req.files);
      ctx.response.body = "上传多文件成功~";
    });

    app.use(uploadRouter.routes());

    app.listen(8000, () => {
      console.log("koa初体验服务器启动成功~");
    });

image.png image.png

响应请求

其他方法请访问官网: koa.bootcss.com/#response

  • response.body: 用来响应请求的数据。
    • string :字符串数据
    • Buffer :Buffer数据
    • Stream :流数据
    • Object || Array:对象或者数组
    • null :不输出任何内容
    • 如果response.status尚未设置,Koa会自动将状态设置为200或204。
  • response.type: 设置 Content-Type字段
  • response.set: 设置响应头字段。可以传入一个对象或者传入key, value
  • response.status: 设置状态码。

app常用方法和属性

  • app.context。可以通过编辑 app.context 为 ctx 添加其他属性。
    app.context.db = db();

    app.use(async ctx => {
      console.log(ctx.db);
    });
  • app.keys。设置cookie的签名 image.png
// 这个值为数组
app.keys = ['im a newer secret'];

// 这些密钥可以倒换,并在使用 `{ signed: true }` 参数签名 Cookie 时使用。
ctx.cookies.set('name', 'tobi', { signed: true });

ctx常用方法和属性

  • ctx.cookies.set(name, value, [options]): 设置cookie
  • ctx.cookies.get(name, [options]): 获取cookie
  • ctx.get(): 获取请求头中的字段名
  • ctx.body: ctx.response.body的别名。返回响应内容。
  • ctx.status: ctx.response.status的别名。返回或者设置状态码。
  • ctx.type: ctx.response.type的别名。返回或者设置返回值类型。
  • ctx.set(): ctx.response.set的别名。设置响应头字段。可以设置多个字段或者单个字段
    ctx.set('Cache-Control', 'no-cache');
    
    ctx.set({
      'Etag': '1234',
      'Last-Modified': date
    });

ctx.request对象中的简写

image.png

ctx.response对象中的简写

image.png

部署静态服务器

Koa中没有像Express中内置的中间件,需要安装第三方koa-static库。

    const Koa = require('koa');
    const koaStatic = require('koa-static');
    const path = require("path")
    const app = new Koa();

    app.use(koaStatic(path.resolve(__dirname, "./static")));

    app.listen(8000, () => {
      console.log("koa初体验服务器启动成功~");
    });

错误处理

官方可是说过有力地增强错误处理。

通过ctx.app.emit("error", 错误类型, ctx)传递错误。

通过app.on("error", (err, ctx) => {})监听错误,并做出处理。

自定义错误类型

    // 用户名或者密码未输入
    const USERNAME_OR_PASSWORD_NOT_INPUT = "username_or_password_not_input"
    // 用户名重复
    const USER_ALREADY_EXISTS = 'user_already_exists';
    // 用户名错误
    const USERNAME_NOT_EXISTS = 'username_not_exists';
    // 密码错误
    const PASSWORD_ERROR = 'password_error'
    // 未携带授权token
    const NOT_TAKE_AUTHORIZATION_TOKEN = "not_take_authorization_token"
    // 未授权
    const UNAUTHORIZATION = 'unauthorization'
    // 用户没有权限
    const NOT_PERMISSION = 'not_permission'


    module.exports = {
      USERNAME_OR_PASSWORD_NOT_INPUT,
      USER_ALREADY_EXISTS,
      USERNAME_NOT_EXISTS,
      PASSWORD_ERROR,
      NOT_TAKE_AUTHORIZATION_TOKEN,
      UNAUTHORIZATION,
      NOT_PERMISSION
    }

处理错误的中间件函数

    // errorHandle.js
    const {
      USERNAME_OR_PASSWORD_NOT_INPUT,
      USER_ALREADY_EXISTS,
      USERNAME_NOT_EXISTS,
      PASSWORD_ERROR,
      NOT_TAKE_AUTHORIZATION_TOKEN,
      UNAUTHORIZATION,
      NOT_PERMISSION
    } = require("./errorType");

    const errorHandle = (errorMessage, ctx) => {
      let status, message;
      switch (errorMessage) {
        case USERNAME_OR_PASSWORD_NOT_INPUT:
          status = 400; // Bad Request
          message = "用户名或者密码不能为空~";
          break;
        case USER_ALREADY_EXISTS:
          status = 409; // conflict
          message = "用户名已经存在~";
          break;
        case USERNAME_NOT_EXISTS:
          status = 400; // 参数错误
          message = "用户名不存在~";
          break;
        case PASSWORD_ERROR:
          status = 400; // 参数错误
          message = "密码是错误的~";
          break;
        case NOT_TAKE_AUTHORIZATION_TOKEN:
          status = 401; // 参数错误
          message = "未携带token~";
          break;
        case UNAUTHORIZATION:
          status = 401; // 参数错误
          message = "无效的token~";
          break;
        case NOT_PERMISSION:
          status = 401; // 参数错误
          message = "您不具备操作的权限~";
          break;
        default:
          status = 404;
          message = "NOT FOUND";
      }

      ctx.status = status;
      ctx.body = message;
    }


    module.exports = errorHandle

注册错误中间件

    const errorHandle = require("./errorHandle");
    app.on("error", errorHandle)

Koa的源码分析

我们调用 new Koa()其实就是在内部调用Application类的构造函数。其中有一个middleware的数组用于存储中间件。

image.png

我们通过app.listen()来开启服务,底层其实就是调用nodejs中的listen方法

image.png

注册中间件时,调用use方法将中间件加入到middleware数组中。

image.png

通过调用callback函数来返回响应

image.png

并且通过compose函数来处理中间件的调用

image.png

处理完中间件后,我们将中间件传入到handleRequest函数中,来做出响应。

image.png

Express和Koa的区别

从架构设计上来说:

  • express是完整和强大的,其中帮助我们内置了非常多好用的功能;
  • koa是简洁和自由的,它只包含最核心的功能,并不会对我们使用其他中间件进行任何的限制。甚至是在app中连最基本的get、post都没有给我们提供;我们需要通过自己或者路由来判断请求方式或者其他功能;

中间件执行的差异

express和koa框架他们的核心其实都是中间件:它们的中间件的执行机制是不同的,特别是针对某个中间件中包含异步操作时;

所以,接下来,我们再来研究一下express和koa中间件的执行顺序问题;

情景介绍

  • 在middleware1中,在req.message中添加一个字符串 aaa;

  • 在middleware2中,在req.message中添加一个 字符串bbb;

  • 在middleware3中,在req.message中添加一个 字符串ccc;

  • 当所有内容添加结束后,在middleware1中,通过res返回最终的结果; express的中间件没有在异步操作中拼接字符串,然后在第一个中间件中返回响应结果req.message。将会是aaabbbccc。如果有异步操作拼接字符串(假设ccc在异步操作中拼接的), 那么将会返回aaabbb。这是因为express调用中间件的时候,执行到最后一个中间件时,他不会等待异步操作执行完毕。就会返回到上一个中间件未执行完的代码。

Koa的中间件中执行同步拼接的时候,产生的结果和express相同。我们可以通过将中间件设置为async函数,来让next()后的代码异步操作,就可以正确返回aaabbbccc。

image.png