Koa 学习笔记

1,285 阅读5分钟

源码地址

github- koa-demo-1

$ cd koa-demo-1
$ npm install

Koa 是什么

Koa 是基于 Node.js 的 web 应用开发框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。

安装

$ yarn add koa
$ yarn add --dev @types/koa //安装 ts 依赖
$ tsc --init //创建 ts配置文件
$ touch server.ts //创建 server.ts文件

简易的 server 服务器

//server.js
import Koa from "koa";
const app = new Koa();
//中间件
app.use(async (ctx) => {
  ctx.body = "hello";//返回hello
});
app.listen(3000);//监听3000端口

ts-node-dev自动运行(如想看果没有 ts-node-dev 需要安装一下)

$ ts-node-dev server.ts

此时浏览器打开 localhost:3000就可以看到页面已经有hello出现了。

context 对象

Koa 提供了一个 context 对象来表示一次对话的上下文(包括 HTTP 请求和 HTTP 回复),通过这个对象,可以控制返回给用户的内容。

上面代码中的 ctx就是 context上下文对象的简写。

通过 ctx.body可以发送用户内容,ctx.body 属性就是 ctx.response.body的简写,只是 Koa 通过代理使得我们可以不写response 属性。

中间件

const logger = (ctx, next) => {
  console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
  next();
}
app.use(logger);

上面代码中的 logger函数是一个中间件(middleware),因为它处于 request 和 response 之间,用来实现中间功能。而 app.use函数用来加载中间件。

基本上,Koa 所有的功能都是通过中间件实现的,每个中间件默认接受两个参数,第一个参数是 Context 对象,第二个参数是next函数。只要调用next函数,就可以把执行权转交给下一个中间件。

多个中间件情况

在 Koa 框架中,多个中间件的情况下支持 async 和 await 语法,我们来看一下中间件的执行顺序

//1.ts
import Koa from "koa";
const app = new Koa();
//第一个中间件
app.use(async (ctx, next) => {
  //顺序1
  ctx.body = "hello";
  //跑到下一个中间件,此时当前函数进入等待期
  await next();
  //顺序3
  ctx.body = "qiuyanxi";
});
//第二个中间件
app.use(async (ctx, next) => {
  //顺序2
  ctx.set("Content-Type", "text/html;charset=utf-8");
  //跑回第一个中间件
  await next();
});
app.listen(3000);

然后再次请求 localhost:3000,就可以看到打印内容

>> one
>> two
>> three
<< three
<< two
<< one

最后我们可以看到页面上是qiuyanxi

中间件栈

多个中间件会形成一个栈结构(middle stack),以"先进后出"(first-in-last-out)的顺序执行。

  • 最外层中间件首先执行
  • 调用 next 函数
  • 最内层的中间件最后执行
  • 执行结束后,将执行权返回给上一层中间件
  • 最外层的中间件收回执行权之后,执行next函数后面的代码。

中间件栈执行顺序

为了更好地看出中间件栈的执行顺序,我将代码写成如下

//2.ts
import Koa from "koa";
const app = new Koa();
const one = (ctx: unknown, next: () => void) => {
  console.log(">> one");
  next();
  console.log("<< one");
};

const two = (ctx: unknown, next: () => void) => {
  console.log(">> two");
  next();
  console.log("<< two");
};

const three = (ctx: unknown, next: () => void) => {
  console.log(">> three");
  next();
  console.log("<< three");
};

app.use(one);
app.use(two);
app.use(three);
app.listen(3000);

异步中间件

在 Nodejs 中包含大量异步I/O,比如读取文件,那么这种异步操作就必须结合 asyncawait

//3.ts
import Koa from "koa";
const fs = require("fs/promises");
const app = new Koa();

const main = async function (ctx: any, next: () => void) {
  ctx.response.type = "html";
  ctx.response.body = await fs.readFile("./index.html", "utf-8");
};

app.use(main);
app.listen(3000);

上面代码中的 fs.readFile就是异步的,需要配合await语法使用,而整个中间件则需要配合async关键字

中间件的合成

通过koa-compose模块可以将多个中间件合成为一个。

//4.ts
import Koa from "koa";
const app = new Koa();

const compose = require("koa-compose");

const logger = (ctx: any, next: () => void) => {
  console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
  next();
};

const main = (ctx: any) => {
  ctx.response.body = "Hello World";
};

const middleWares = compose([logger, main]);
app.use(middleWares);
app.listen(3000);

理解Koa 模型

如果把中间件和代码执行的顺序做一张图,我觉得用这张挺合适

不过根据查的资料,Koa 还有一张著名的洋葱图模型

await next()

next表示进入下一个函数,下一个函数会返回 promise 对象,假设为 p。等到下一个函数执行完成,那么 p 的状态就会被设置为成功,await 会等待 p 成功后,再执行剩下的代码。

这跟我们平常写 async 和 await 是一样的,只是在 Koa 中,跨函数跳动的语法着实有点惊艳。

路由

原生路由

网站一般都有多个页面。通过ctx.request.path可以获取用户请求的路径,由此实现简单的路由。

//5.ts
import Koa from "koa";
const app = new Koa();

app.use((ctx: any, next: () => void) => {
  if (ctx.request.path === "/") {
    ctx.response.body = "hello";
  }
  if (ctx.request.path === "/app") {
    ctx.response.body = "world";
  }
});

app.listen(3000);

当我们分别访问localhost:3000localhost:3000/app时,就可以看到不同的返回内容。

koa-route 模块

为了方便我们做路由,可以使用 router 模块,更方便地管理路由。

首先我们需要安装并引入它

$ yarn add koa-route
const route = require("koa-route");

然后用 route 的get 方法来获取路径,然后加载中间件

//6.ts
import Koa from "koa";
const app = new Koa();
const route = require("koa-route");

const main = (ctx, next) => {
  ctx.response.body = "main 加载";
};

const index = (ctx, next) => {
  ctx.response.body = "index 加载";
};

app.use(route.get("/", index));
app.use(route.get("/main", main));
app.listen(3000);

上面的代码中,需要获取到/,才会加载 index 中间件。需要获取到/main,才会加载 main 中间件。

koa-router 模块

刚刚发现一个更好用的路由模块,star 比上面的 route 多,这里补充一下。

下载

$ yarn add koa-router

使用

//6.1.ts
import Koa from "koa";
const Router = require("koa-router");

const app = new Koa();
const router = new Router();

router
  .get("/", (ctx, next) => {
    ctx.body = "index";
  })
  .get("/main", (ctx, next) => {
    ctx.body = "main";
  });
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000);

添加前缀

可以使用prefix属性增加前缀,需要在实例化时传入 option。

//6.2.ts
var router = new Router({
  prefix: "/users",
});

这样就可以增加一个路径层级,原先比如说访问/的,现在是直接访问/users/

嵌套路由

//6.3.ts
//父路由
var forums = new Router();
//子路由
var posts = new Router();

posts.get("/", (ctx, next) => {
  ctx.body = "index";
});
posts.get("/:pid", (ctx, next) => {
  ctx.body = "id";
});
forums.use("/forums/:fid/posts", posts.routes(), posts.allowedMethods());

// responds to "/forums/123/posts" and "/forums/123/posts/123"

捕获动态路径

//6.4.ts
import Koa from "koa";
const Router = require("koa-router");
const app = new Koa();
const router = new Router();
router.get("/:title/:id", (ctx, next) => {
  ctx.body = ctx.params;
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000);

访问localhost:3000/main/123就可以看到{"title":"main","id":"123"}

访问静态资源

我现在建立static的目录名,里面放三个不同的文件

如果这时候访问/static/koa.jpg肯定无效,假如我们希望访问所有静态资源,可以使用 koa-static 这个中间件来访问静态资源。

安装

$ yarn add koa-static

使用

import Koa from "koa";
const statics = require("koa-static");
const path = require("path");
const app = new Koa();

const staticPath = "./static";
//把当前文件的目录名和./static 做拼接
app.use(statics(path.join(__dirname, staticPath)));
app.use(async (ctx) => {
  ctx.body = "hello";
});
app.listen(3000);

这样就可以访问到static 目录下的任何文件了。

打开http://localhost:3000/koa.jpg就可以看到静态图片已加载。

重定向

页面重定向可以使用ctx.response.redirect()完成。

//7.ts
import Koa from "koa";
const route = require("koa-route");

const app = new Koa();
const redirect = (ctx, next) => {
  ctx.response.redirect("/");
};
const index = (ctx, next) => {
  ctx.response.body = "我是 index 页面";
};

app.use(route.get("/main", redirect));
app.use(route.get("/", index));
app.listen(3000);

上面的代码不管访问的路径是/还是/main,都会进入到 index 这个中间件中,打开页面,会发现页面中显示的是我是 index 页面

错误处理

ctx.throw 函数

Koa 提供 throw 函数来抛出错误给用户。比如下面的代码,我会抛出一个500错误给用户。

//8.ts
import Koa from "koa";
const route = require("koa-route");

const app = new Koa();

const index = (ctx, next) => {
  ctx.throw(500);
};

app.use(route.get("/", index));
app.listen(3000);

访问localhost:3000,你会看到一个500错误页"Internal Server Error"。

返回 status

我们也可以设置 status 来返回HTTP 码,使用 ctx.response.status属性直接设置就行

//9.ts
import Koa from "koa";
const route = require("koa-route");

const app = new Koa();

const index = (ctx, next) => {
  ctx.response.status = 404;
};

app.use(route.get("/", index));
app.listen(3000);

访问localhost:3000,你会看到一个404错误页"Not Found"。

处理错误中间件

为了处理错误,我们很有可能使用try``catch来帮助我们处理错误,但是每个中间件都这样写非常麻烦,我们可以在最外层专门写一个处理错误的中间件。

//10.ts
import Koa from "koa";
const app = new Koa();

const handler = async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.response.status = err.statusCode || err.status || 500;
  }
};
const index = async (ctx, next) => {
  ctx.response.body = "你好";
  await next();
};

const main = (ctx, next) => {
  ctx.response.status = 404;
};
app.use(handler);
app.use(index);
app.use(main);
app.listen(3000);

上面的代码中,一旦有抛出 error,那么最外层的中间件会捕获到并执行对应的操作。

现在我们打开localhost:3000就可以看到

error 事件的监听

当发生错误时,我们可以通过监听error事件来处理错误,比如上面的代码我可以这样修改。

//11.ts
import Koa from "koa";
const app = new Koa();
const main = (ctx) => {
  ctx.throw(500);
};
app.use(main);
app.on("error", (err, ctx) => console.log("err", err));
app.listen(3000, () => {
  console.log("server is start");
});

当发生错误时,会执行监听事件的回调。

释放 error 事件

try、catcherror事件一起用的时候,会发现error 事件没办法触发。

//12.ts
import Koa from "koa";
const app = new Koa();
const handler = async (ctx, next) => {
  try {
    await next();
  } catch (e) {
    ctx.response.status = e.status;
    ctx.response.body = "报错了";
  }
};
const main = (ctx) => {
  ctx.throw(500);
};
app.use(handler);
app.use(main);
app.on("error", (err, ctx) => console.log(" 报错信息", err));
app.listen(3000, () => {
  console.log("server is start");
});

你会发现命令行没有出现报错信息,需要再使用app.emit手动执行

app.emit("error", e);

常用 API

ctx.request

ctx.req 和 ctx.request的区别

ctx.request是 Koa 封装的请求对象,而 ctx.req 是 node.js 中原生的 http 模块的请求对象。

ctx.request看起来更加直观,而 ctx.req 会有更多的请求内容。

获取参数

在实际开发中,经常用到 get 请求,这时候我们可以通过 ctx.request 或者 ctx.req 来请求信息,最常用的是获得请求参数,代码可以这样写。

//13.ts
import Koa from "koa";

const app = new Koa();

app.use((ctx, netx) => {
  let url = ctx.url;
  let request = ctx.request;
  let req_query = request.query; //查询字符串对象
  let req_querystring = request.querystring; //查询字符串
  ctx.body = {
    url,
    req_query,
    req_querystring,
  };
});
app.listen(3000);

然后在命令行输入

$ ts-node-dev 13.ts

最后访问http://localhost:3000/?user=qiuyanxi&age=18

现在浏览器是这样的

获取请求方式

通过ctx.request.method可以获取到请求方法,比如下面的代码

//14.ts
import Koa from "koa";
const app = new Koa();

app.use(async (ctx, next) => {
  switch (ctx.request.method) {
    case "GET":
      //显示表单页面
      let html = `
      <form action="/" method='POST'>
      <p>姓名</p>
    <input name="useName"/>
    <p>age</p>
    <input name="age"/>
    <button>提交</button>
    </form>
    `;
      ctx.body = html;
      break;
    case "POST":
      ctx.body = "接收到 POST";
      break;
    default:
      ctx.body = "<h1>404</h1>";
  }
});
app.listen(3000);

现在浏览器是这样的 当我点击后,浏览器就会发送一个请求,然后变成 POST请求,浏览器就会这样显示

获取表单数据

原生方法

Koa 没有获取表单数据的封装方法,所以我们需要通过原生 Node.js 的方式来获取表单数据。

1.首先,我们需要获取到表单数据,这里可以使用 ctx.req.addListener监听数据发送情况,然后用 ctx.req.on('end')事件来触发数据完成,把数据发送出去。

由于数据请求是异步的,所以我们需要用 await 配合 promise 来封装一个获取数据的函数。

//15.ts
const getPostData = (ctx) => {
  return new Promise((resolve, reject) => {
    try {
      let postData = "";
      ctx.req.addListener("data", (data) => {
        postData += data;
      });
      ctx.req.on("end", () => {
        resolve(postData);
      });
    } catch (error) {
      reject(error);
    }
  });
};

现在我们可以看看表单数据的结构。我在浏览器是这样输入的 现在在服务端看看发送过来的数据是怎样的 上面可以看到中文邱彦兮已经被encodeURIComponent()编码了,而且整个数据就是字符串,我们需要把它变成一个对象。

2.封装函数处理数据

const parsePostData = (string: string) => {
  const data = {};
  const array = string.split("&");
  //由于 for of 无法获取的 index,所以配合 entries
  for (let [index, item] of array.entries()) {
    let arr = item.split("=");
    //对encodeURIComponent解码
    data[arr[0]] = decodeURIComponent(arr[1]);
  }
  return data;
};

3.将获取到的数据对象打印到浏览器上

const postData = (await getPostData(ctx)) as string;
const data = parsePostData(postData);
ctx.body = data;

现在浏览器视图是这样的,说明已经完成。

使用轮子koa-bodyparser

这个轮子非常方便,可以帮助我们处理上传的数据。

$ yarn add koa-bodyparser

使用

//16.ts
const Koa = require("koa"); //这里注意,用 import 引入会报错。
const bodyParser = require("koa-bodyparser");
app.use(bodyParser());

获取数据

ctx.body = ctx.request.body;

ctx.cookies.get/set 读取 cookie

//17.ts
import Koa from "koa";
const app = new Koa();
app.use(async (ctx) => {
  if (ctx.url === "/") {
    ctx.body = "设置好 cookie 了";
    //使用 ctx.cookies.set 设置 cookie
    ctx.cookies.set("name", "qiuyanxi", {
      domain: "127.0.0.1", //域名
      path: "index", //路径
      maxAge: 60000, //有效时间
      httpOnly: true, //能否被 js 获取
      overwrite: false, //是否允许重写
    });
  }
});
app.listen(3000);