前言
如果不使用koa,用原生的node来创建一个简单的 HTTP 服务器,并定义端口号为3000
const http = require('http') const server = http.createServer((req, res) => { res.end('hello world') }) server.listen(3000, () => { console.log('server is running on 3000 port'); })
接下来我们用对此进行二次封装的KOA来创建这个demo
KOA
“Koa”通常指的是 Koa 框架,它是一个基于 Node.js 的 Web 应用框架,由 Express 原班人马打造。
Koa 致力于成为一个更小、更富有表现力、更健壮的 Web 框架。它使用 async/await 语法来处理异步流程,使得代码更加简洁和易读。
Koa 具有轻量、灵活、中间件机制等特点,能够帮助开发者更高效地构建 Node.js 应用。
首先安装依赖
npm i koa
小试牛刀
const Koa = require('koa'); const app = new Koa(); app.listen(3000, () => { console.log('server is running at port 3000'); });
能够看出来其实这就是对刚刚的代码做了一个封装。接下来我们创建一个函数体,然后用app给use掉,携带koa的上下文对象context
,事实上他集成了我们最初始的res和req两个对象。
const Koa = require('koa'); const app = new Koa(); const main = (ctx) => { console.log(ctx); } app.use(main); app.listen(3000, () => { console.log('server is running at port 3000'); });
const main = (ctx) => { console.log(ctx); }
:定义了一个名为main
的中间件函数,它接收一个ctx
参数,即上下文对象,并将其打印到控制台。app.use(main);
:使用app.use
方法注册中间件函数main
,使其在每次请求处理时被调用。
ctx
ctx
是 Koa 中的上下文(Context)对象,它封装了 Node.js 中的原生 req
(请求)和 res
(响应)对象。可以将 ctx
看做是一次 HTTP 请求和响应过程中的相关信息的集合。
通过 ctx
,开发者可以方便地访问和操作请求及响应的各种属性和方法。
ctx
对象具有以下主要属性:
ctx.req
:原生的req
对象。ctx.res
:原生的res
对象。ctx.request
:Koa 自己封装的请求对象,该对象不仅包含原生req
对象的属性,还有一些额外的便捷方法和属性。例如,可以更方便地获取查询参数(ctx.query
)、解析 URL(ctx.path
)等。ctx.response
:Koa 自己封装的响应对象,同样具有一些额外的方法和属性,方便设置响应的相关信息。ctx
本身还代理了ctx.request
和ctx.response
身上的属性,这意味着可以直接通过ctx
来访问ctx.request
和ctx.response
的部分属性,例如直接使用ctx.query
来获取查询参数,而无需使用ctx.request.query
。
我们把ctx打印出来是这样的:
{ request: { method: 'GET', url: '/', header: { host: 'localhost:3000', connection: 'keep-alive', 'cache-control': 'max-age=0', 'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126", "Microsoft Edge";v="126"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"Windows"', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0', accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'sec-fetch-site': 'none', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6' } }, response: { status: 404, message: 'Not Found', header: [Object: null prototype] {} }, app: { subdomainOffset: 2, proxy: false, env: 'development' }, originalUrl: '/', req: '<original node req>', res: '<original node res>', socket: '<original node socket>' }
因此此时我们想向前端响应一个hello world,甚至可以像最开始那样,直接使用他封装好了的ctx里面的res.end。
const main = (ctx) => { // console.log(ctx); ctx.res.end('hello koa'); }
除此之外,我们还可以给response响应体上挂一个body属性
const main = (ctx) => { // console.log(ctx); // ctx.res.end('hello koa'); ctx.response.body = 'hello koa'; } app.use(main);
但是这么写属实是比原生更加麻烦了,因此koa做了一个代理,允许我们直接省略response
const main = (ctx) => { // console.log(ctx); // ctx.res.end('hello koa'); ctx.body = 'hello koa'; } app.use(main);`js
同理,想获取url,就可以直接使用原生的req.url也可以用封装好的,再直接可以省略。
const main = (ctx) => { // console.log(ctx); // ctx.res.end('hello koa'); // ctx.response.body = 'hello koa'; ctx.body = 'hello koa'; console.log(ctx.req.url); console.log(ctx.request.url); console.log(ctx.url); } app.use(main);
不由感叹,开源的力量,不用大家重复造轮子,这些封装都很优雅,支持各种写法,帮助大家省了很多麻烦。
我们来看看,koa还封装了个accepts
const main = (ctx) => { if (ctx.request.header.accept === 'xml') { ctx.body = '<data>hello xml </data>'; } else if (ctx.request.accepts('html')) { ctx.body = '<p>hello html</p>'; } } app.use(main);
我们发现原生的拿accept无法匹配到,除非把请求头全部拿过来(text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7)
为此koa封装了ctx.request.accepts方法,可以直接匹配。
如果我们想读取一个html文件然后给他响应到浏览器上
const fs = require('fs'); const Koa = require('koa'); const app = new Koa(); const main = (ctx) => { ctx.response.type = 'text/html'; const context = fs.readFileSync('./template.html', 'utf-8') //toString() console.log(context); ctx.body = context; } app.use(main); app.listen(3000, () => { console.log('server is running at port 3000'); });
要让浏览器读得懂,要设置响应头,告诉浏览器这是一个html,如果没有toString或者没有设置utf8编码,那么会拿到一个buffer流,如果响应一个buffer流就会自动触发下载,因此此时让你去做一个触发下载某个东西的操作你也可以做了。
我们能够理解,这里ctx.response.type可以直接省略response了。
接下来再换一种方式来写
const fs = require('fs'); const Koa = require('koa'); const app = new Koa(); const main = (ctx) => { // ctx.response.type = 'text/html'; // const context = fs.readFileSync('./template.html', 'utf-8') //toString() // console.log(context); // ctx.body = context; // ctx.response.type = 'text/html'; ctx.res.writeHead(200,{'Content-type':'text/html'}) // 原生的方式不爱用就用上面的 const content = fs.createReadStream('./template.html'); console.log(content); ctx.body = content; } app.use(main); app.listen(3000, () => { console.log('server is running at port 3000'); });
koa路由
用最开始的办法写路由
const fs = require('fs'); const Koa = require('koa'); const app = new Koa(); const main = (ctx) => { if (ctx.url === '/') { ctx.type = 'text/html'; ctx.body = '<h2>home</h2>' }else{ ctx.type = 'text/html'; ctx.body = '<a href="/">go home</a>' } }
可以看出,这种写法也是相当的恶心。因此我们使用koa-route,因为这个框架非常简单,因此很多团队都可以自己做封装,大家可以自行选择使用什么koa-route/koa-router...
安装依赖
npm i koa-route
小试牛刀
const router = require('koa-route'); const Koa = require('koa'); const app = new Koa(); const main = (ctx) => { ctx.type = 'html'; ctx.body = '<h2>home</h2>' } const about = (ctx) => { ctx.type = 'html'; ctx.body = '<a href="/">about,go home</a>' } app.use(router.get('/', main)) app.use(router.get('/about', about)) // app.use(main); app.listen(3000, () => { console.log('server is running at port 3000'); });
注意:事实上koa只能use一个,这里的路由能use两个是因为封装过了,此时我们自己写一个打印日志方法如果不写next,后面的use就无法使用,因此:
const router = require('koa-route'); const Koa = require('koa'); const app = new Koa(); const main = (ctx) => { ctx.type = 'html'; ctx.body = '<h2>home</h2>' } const about = (ctx) => { ctx.type = 'html'; ctx.body = '<a href="/">about,go home</a>' } const logger = (ctx, next) => { console.log(`${ctx.method} - ${ctx.url} - ${Date.now()}`); next(); } app.use(logger) app.use(router.get('/', main)) app.use(router.get('/about', about)) // app.use(main); app.listen(3000, () => { console.log('server is running at port 3000'); });
next
const Koa = require('koa'); const app = new Koa(); const one = (next) =>{ // 中间件 console.log(1); console.log(2); } const two = () => { console.log(3); console.log(4); } const three = () => { console.log(5); console.log(6); } app.use(one); app.use(two); app.use(three); app.listen(3000, () => { console.log('server is running at port 3000'); });
如果是这样一份代码,势必只会打印1,2,因此要通过next的调用进入下一个中间件,一碰到next就会进入下一个中间件,如果走完了,就会回头走没走完的。
const Koa = require('koa'); const app = new Koa(); const one = (next) =>{ // 中间件 console.log(1); next() console.log(2); } const two = (next) => { console.log(3); next() console.log(4); } const three = () => { console.log(5); console.log(6); } app.use(one); app.use(two); app.use(three); app.listen(3000, () => { console.log('server is running at port 3000'); });
因此,答案势必是:1,3,5,6,4,2,看得出来,这底层一定是用递归
写的。不难理解,koa的中间件执行过程就是个洋葱模型,用一根筷子插入一个洋葱,进入第一层,第二层,第三层,然后到了中间,继续深入就回到了第二层,第一层。
小结
“Koa”是一个基于 Node.js 的 Web 应用框架,它以简洁、高效和灵活著称。
本文介绍了一下Koa的基本使用方法,以及其中的一些细节内容,ctx包括什么东西,next怎么使用,如何使用koa做路由...
Koa主要特点包括:
基于 async/await :使用 ES6 的异步函数语法,让异步流程的控制更加直观和简洁。
轻量级:核心模块非常小,只包含最基本的功能,开发者可以根据需求选择和添加扩展。
中间件机制:通过
app.use
方法来添加中间件,中间件可以按照添加的顺序依次执行,方便对请求和响应进行处理和修改。