源码地址
$ 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,比如读取文件,那么这种异步操作就必须结合 async 和 await。
//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:3000和localhost: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、catch和 error事件一起用的时候,会发现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);