网站的建设和维护成本,wordpress 模板 管理,什么网站能免费做简历,网站建设费用如何列支前言 koa是一个非常流行的Node.js http框架。本文我们来学习下它的使用和相关源码 来自官网的介绍#xff1a; Koa 是一个新的 web 框架#xff0c;由 Express 幕后的原班人马打造#xff0c; 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。…前言 koa是一个非常流行的Node.js http框架。本文我们来学习下它的使用和相关源码 来自官网的介绍 Koa 是一个新的 web 框架由 Express 幕后的原班人马打造 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数Koa 帮你丢弃回调函数并有力地增强错误处理。 Koa 并没有捆绑任何中间件 而是提供了一套优雅的方法帮助您快速而愉快地编写服务端应用程序 为什么使用koa
使用koa而不直接使用Node.js的http模块 高度可定制性koa中实现了一套中间件机制以及在koa中万物皆中间件我们通过中间件处理请求和响应并可以按需自由添加和修改中间件并且koa的中间件生态非常丰富。而使用http需要自己编写全部的请求处理逻辑 异步编程koa基于async/await语法可以让异步编程变得更加简单和优雅。而直接使用http模块则需要使用回调函数或事件监听的方式进行异步编程不够直观 错误处理koa内置的错误处理机制可以很好的捕获和处理错误让代码更加健壮和可靠。而使用http模块则需要自己编写错误处理逻辑容易出现漏洞 扩展性koa内置的扩展机制可以让开发者在不改变核心代码的情况下轻松地扩展和定制koa的功能。而使用http模块则需要自己编写全部的扩展逻辑不够便捷
使用
koa的使用非常简单引入koa后只需要6行代码即可访问3000端口的http服务返回一个Hello koa
const Koa require(koa);
const app new Koa();app.use(ctx {ctx.body Hello Koa;
});app.listen(3000);中间件
koa本身几乎没有封装任何进一步处理http请求的能力而是实现了一套中间件的机制所有的逻辑均由相关的中间件进行实现中间件可以说是koa的灵魂
koa的中间件本质是一个函数接收一个上下文对象context和一个next函数作为参数然后对请求和响应进行处理并将控制权传递给下一个中间件。中间件可以实现各种功能例如路由、请求处理、错误处理等
const myMiddleware async (ctx, next) {// 处理请求// ...// 调用下一个中间件await next();// 处理响应// ...
}例如我们实现一个错误处理中间件在服务端发生任何错误时给客户端返回一个500的状态码可以以下实现即可
const errorHandler async (ctx, next) {try {// 处理请求// ...// 调用下一个中间件await next();// 处理响应// ...} catch (err) {// 处理错误ctx.status 500;ctx.body err.message;}
}app.use(errorHandler)以两个最常用的中间件为例
koa-router
koa默认也是没有封装对于特定的请求方法进行处理的功能像很多http中处理路由相关的逻辑则需要引入koa-router 进行使用。koa router提供了基础的路由路径处理、嵌套路由等一些基础路由能力 var Koa require(koa);
var Router require(koa-router);var app new Koa();
var router new Router();router.get(/, (ctx, next) {// ctx.router available
});app.use(router.routes()).use(router.allowedMethods());koa-router的源码就不展开了原理基本上在中间件中读取req.url 、 req.method 和相关req上的一些属性进行分发到相应的路由注册的回调返回中进行处理
koa-body
另一个常用的功能就是将请求的请求体数据解析成js对象方便代码进行消费 对于node原生的http服务我们需要监听请求对象的data和end事件在data 事件中接收二进制buffer数据在end事件中将buffer转成字符串再序列化成js对象
const Koa require(koa);
const bodyParser require(koa-bodyparser);const app new Koa();
app.use(bodyParser());app.use(async ctx {// the parsed body will store in ctx.request.body// if nothing was parsed, body will be an empty object {}ctx.body ctx.request.body;
});这样对于这类请求我们通过ctx.request.body就能获取到json请求的数据无需关心从请求流关心如何获取请求体。koa-body不止处理json类型它还会对form、text、xml等类型做相应的处理 源码实现
koa的源码非常简洁一共只有4个文件 application
application.js定义了Koa类用于创建koa app对象下面是koa类的构造函数
// ...
const Emitter require(events)
const compose require(koa-compose);
const http require(http);
const context require(./context);
const request require(./request);
const response require(./response);
// ...class Koa extends Emitter {constructor() {super();this.middleware [];this.context Object.create(context);this.request Object.create(request);this.response Object.create(response);// constructor中其它的逻辑忽略}// ...
}Koa类继承了Emitter类用于实现事件的发布和订阅。还定义了一些属性主要包括middleware、context、request和response。其中middleware是中间件函数数组用于存储所有的中间件函数context是koa的请求上下文对象、request是请求对象实例、response是响应对象实例
koa实例上也暴露了几个对外使用的方法
app.listen
上面的使用demo可以看到调用listen后就是监听指定端口运行起我们的http服务 通过查看app.listen 的实现本质是调用了app.callback获取到回调函数处理逻辑再传给http.createSerever。所以也等价于以下调用
const http require(http);
const Koa require(koa);
const app new Koa();
http.createServer(app.callback()).listen(3000);app.callback
返回可以直接传递给 http.createServer() 方法的回调函数来处理请求 callback () {const fn this.compose(this.middleware)if (!this.listenerCount(error)) this.on(error, this.onerror)const handleRequest (req, res) {const ctx this.createContext(req, res)return this.handleRequest(ctx, fn)}return handleRequest}handleRequest (ctx, fnMiddleware) {const res ctx.resres.statusCode 404const onerror err ctx.onerror(err)const handleResponse () respond(ctx)onFinished(res, onerror)return fnMiddleware(ctx).then(handleResponse).catch(onerror)}主要有以下几个逻辑 判断我们是否有监听错误事件进行处理this.listenerCount是继承的EventEmiter上用于获取某个事件监听次数的方法如果没有则使用koa自带的默认错误处理 使用回调入参的request对象和response对象构造请求上下文对象并传递给this.handleRequest函数进行处理 在handleRequest中就是调用了被compose完成后的中间件函数在处理完成后调用respond进行结束整个请求的流程 在koa中我们无需像Node.js中http需要显式调用res.end或者res.pipe进行响应的结束发送因为在handleResponse的respond函数中处理了。它会根据我们在业务逻辑设置的不同的body的类型进行相关调用例如如果是一个流则调用pipe进行流式返回、特定状态码不返回body、非buffer和string的body序列化成字符串等 洋葱模型
koa的洋葱模型是一种中间件处理机制其核心是将请求和响应对象传递给一系列中间件函数每个中间件函数都可以对请求和响应进行处理并将控制权传递给下一个中间件函数最终将响应返回给客户端。中间件函数在请求处理过程中像是一个个套在一起的“洋葱”请求从外层中间件函数开始处理逐层深入直到最内层中间件函数然后逐层返回最终响应从最外层中间件函数返回给客户端
在洋葱模型中每个中间件函数都是一个异步async函数。在处理请求时每个中间件函数都接收一个context对象和一个next函数作为参数context对象包含了请求和响应的信息next函数可以调用下一个中间件函数
处理顺序如下 请求从外层中间件函数开始处理先经过第一个中间件函数 第一个中间件函数处理请求然后调用next函数将控制权传递给下一个中间件函数 下一个中间件函数也处理请求然后调用next函数将控制权传递给下一个中间件函数直到最内层中间件函数 最内层中间件函数处理请求完成后逐层返回每个中间件函数在返回时可以对响应进行处理 最后响应从最外层中间件函数返回给客户端
洋葱模型的优点是可以将请求和响应的处理逻辑分解成多个模块每个模块只需关注自己的逻辑提高了代码的可维护性。由于每个中间件函数都可以对请求和响应进行处理因此可以实现一些复杂的功能例如身份验证、日志记录、错误处理等
主要是koa-compose包的实现将中间件函数组合在一起compoose实现代码如下
function compose (middleware) {return function (context, next) {// last called middleware #let index -1return dispatch(0)function dispatch (i) {if (i index) return Promise.reject(new Error(next() called multiple times))index ilet fn middleware[i]if (i middleware.length) fn nextif (!fn) return Promise.resolve()try {return Promise.resolve(fn(context, dispatch.bind(null, i 1)))} catch (err) {return Promise.reject(err)}}}
}// 使用const fn this.compose(this.middleware)compose函数接收一个中间件函数数组作为参数返回一个新的中间件。新的中间件函数接收context和next对应于常规中间件的入参
函数内部实现了dispatch用于递归调用中间件数组中的每个函数。 dispatch函数接收一个参数i表示当前调用的中间件函数在数组中的索引。如果i小于等于上一次调用的索引index则表示next函数被多次调用koa中间件中next只能被调用一次调用多次会抛出一个错误。然后dispatch将index赋值为i表示当前调用的中间件函数已经被执行。然后dispatch函数会从中间件数组中取出当前索引对应的函数fn如果当前索引i等于数组长度则说明已经到达中间件函数数组的末尾然后将fn设置为next函数。如果fn不存在则直接返回一个已经resolve的Promise。最后dispatch函数通过Promise.resolve调用当前中间件函数并将dispatch.bind(null, i 1)作为下一个中间件函数的next参数传入以便递归调用下一个中间件函数。如果当前中间件函数抛出了一个错误则通过Promise.reject将错误传递给下一个中间件函数
总结原理是通过递归调用中间件函数数组中的每个函数并将next函数作为参数传入实现洋葱模型中间件的处理顺序。在递归调用的过程中如果某个中间件函数抛出了错误则通过Promise.reject将错误逐层传递给下一个中间件函数直到最终返回错误响应或者成功响应
context
请求上下文对象对应中间件的ctx入参 context.js文件主要是对外导出了一个对象以及执行了一系列delegate操作 导出的对象主要是封装了cookie的读取逻辑 delegate方法是从delegates npm包进行导入这个包的解读见# 每天阅读一个 npm 模块(7)- delegates - 掘金
简单来说就是将对context对象上的操作代理到koa封装的request和response对象中去
// proto这里是context
delegate(proto, response).method(append).access(body)这个执行后的结果就是 context.append方法调用实际调用的是context.response.append context.body的读写实际调的是context.response.body的读写 而context.response则在下面的createContext时将koa的response对象设置在context对象中去
在application中通过createContext方法构造后传入请求处理回调函数
class Koa extends Emitter {constructor() {//....this.context Object.create(context);//....}// ...callback () {const fn this.compose(this.middleware)if (!this.listenerCount(error)) this.on(error, this.onerror)const handleRequest (req, res) {const ctx this.createContext(req, res)return this.handleRequest(ctx, fn)}return handleRequest}createContext (req, res) {/** type {Context} */const context Object.create(this.context)/** type {KoaRequest} */const request context.request Object.create(this.request)/** type {KoaResponse} */const response context.response Object.create(this.response)// 挂载context.app request.app response.app thiscontext.req request.req response.req reqcontext.res request.res response.res resrequest.ctx response.ctx contextrequest.response responseresponse.request requestcontext.originalUrl request.originalUrl req.urlcontext.state {}return context}
}主要是将我们koa中常用的几个对象挂载到相应的地方经过createContext的操作我们可以得到可以通过以下方式获取相关对象 koa app实例 app context.app context.request.app context.response.app koa 请求context对象 context context.request.ctx context.response.ctx koa request对象 ctx.request ctx.response.request koa response对象 ctx.response ctx.request.response 原生req对象 context.req context.request.req context.response.req 原生res对象 context.res context.response.res context.request.res
request
koa中的请求对象封装。基本上都是基于Node.js的http请求的request做一些便捷使用的二次封装的属性和方法并挂载在ctx.request中
一个例子就是Node.js 的http server回调函数入参的req对象http.ImcomingMessage 是没有提供便捷的获取query参数信息它只有一个url属性
而koa的request对象则实现了query的解析、获取、设置等
// request.js
get query () {const str this.querystringconst c this._querycache this._querycache || {}return c[str] || (c[str] qs.parse(str))}get querystring () {if (!this.req) return return parse(this.req).query || }response
koa中的响应对象封装基于Node.js的http请求的response做一些封装的属性和方法挂载在ctx.response中
一个比较常用到的就是会有根据我们的ctx.body设置的值会delegate到ctx.response.body中帮我们去设置response的Content-Type的值例如给ctx.body设置一个普通js对象的话会将Content-Type设置为json类型并将js对象json序列化序列化逻辑在上面提到的respond函数中 最近更新
作为一个代码实现非常精简且已经非常稳定的广泛使用的框架一般来说不会有什么更新了2.x也已经稳定了很久。但是在1/2却更新了3.0.0-alpha.0版本翻看更新记录这个大版本目前只更新了一个功能 可以直接使用app.currentContext来获取当前的请求上下文对象这个功能可以方便不少我们的代码开发
通过上面我们知道koa的contxt对象是每次请求维度的一个新对象如果我们想在一些封装的方法中获拿到当前请求的context对象必须层层传递context对象会比较麻烦
// fn.js
const fn (ctx) {console.log(ctx.url)
}
exports.fn fn// app.js
const app new Koa();
app.use(ctx {ctx.body Hello Koa;fn(ctx)
}).listen(3000);而支持了app.currentContext后我们在任意地方想获取当前的请求上下文对象直接app.currentContext即可无需再多层透传context对象
// fn.js
const fn () {console.log(app.currentContext.url)
}这个功能的实现利用了Node.js的async_hooks模块提供的AsyncLocalStorage。AsyncLocalStorage 是 Node.js 在v14.8.0 版本中引入的一个模块是官方推荐的在异步代码中管理数据的方式之一会将我们保存的数据与异步操作所在的上下文关联起来确保在异步操作中访问到相应正确的数据
AsyncLocalStorage 有两个主要的方法 run()用于在异步操作中保存数据。接收一个回调函数作为参数该回调函数会在异步操作执行期间被调用并且在该回调函数中保存的数据会与异步操作所在的上下文关联起来 getStore()用于在异步操作中获取数据。它会返回与异步操作所在的上下文关联的数据
所以在koa中实现app.currentContext功能主要就是以下代码
// application.js
class Application extends Emitter {constructor (options) {//....if (options.asyncLocalStorage) {const { AsyncLocalStorage } require(async_hooks)this.ctxStorage new AsyncLocalStorage()this.use(this.createAsyncCtxStorageMiddleware())}}// ...createAsyncCtxStorageMiddleware () {const app thisreturn async function asyncCtxStorage (ctx, next) {await app.ctxStorage.run(ctx, async () {return await next()})}}// ....get currentContext () {if (this.ctxStorage) return this.ctxStorage.getStore()}
}如果初始化时配置了option.asyncLocalStorage就注册一个放在第一位的koa中间件 在请求进入中间件时会执行ctxStorage.run 存入当前的context对象并马上在回调函数中执行next即请求后续所有的操作 在后续获取即可通过getStore()获取到当前请求的context对象
总结
通过本文的学习我们了解到了koa的一些使用和实现koa的源码是非常精简的没有太多耦合功能但是设计了巧妙的中间件机制设计来方便让我们开发各种功能