前端面试题 - 45. koa洋葱模型以及相关问题(附koa简易实现源码)
koa了解吗
Koa 框架的洋葱模型是一种中间件处理流程的设计模式,通过将请求和响应对象传递给一系列中间件函数,每个中间件函数都可以对请求和响应进行处理和修改,最终返回响应结果。
const http = require('http')
class MyKoa {
constructor () {
this.middlewares = []
this.routes = []
}
use (fn) {
this.middlewares.push(fn)
}
listen (...args) {
const server = http.createServer(this.handleRequest.bind(this))
server.listen(...args)
}
handleRequest (req, res) {
const ctx = { req, res }
const middlewares = this.middlewares.slice()
// 执行中间件
const next = () => {
const middleware = middlewares.shift()
if (middleware) {
middleware(ctx, next)
}
}
next()
// 处理路由
const url = req.url
const route = this.routes.find(route => route.path === url && route.method === req.method)
if (route) {
route.handler(ctx)
} else {
res.statusCode = 404
res.end('Not Found')
}
}
get (path, handler) {
this.routes.push({ path, method: 'GET', handler })
}
post (path, handler) {
this.routes.push({ path, method: 'POST', handler })
}
}
module.exports = MyKoa
中间件的异常处理是怎么做的?
在 Koa 中,中间件的异常处理也是通过 try-catch
语句来实现的。当一个中间件函数发生异常时,Koa 会自动将控制权转移到一个专门用来处理异常的中间件函数(也称为错误处理中间件)。这个中间件函数可以通过 try-catch
语句来捕获异常,并根据需要进行处理。 在 Koa 应用中,我们可以通过向 app.use()
方法中传递一个中间件函数来注册中间件。当请求进入应用时,Koa 会按照注册的顺序依次执行这些中间件函数。如果某个中间件函数发生异常,Koa 会自动将控制权转移到下一个错误处理中间件函数,如果没有注册任何错误处理中间件函数,Koa 会将异常信息打印到控制台。 下面是一个简单的示例,演示了如何在 Koa 中使用错误处理中间件来捕获和处理异常:
const Koa = require('koa')
const app = new Koa()
// 中间件函数1:用于打印请求日志
app.use(async (ctx, next) => {
console.log(`Received request: ${ctx.request.method} ${ctx.request.url}`)
await next()
})
// 中间件函数2:用于模拟一个异步操作
app.use(async (ctx, next) => {
console.log('Performing async operation...')
await new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Something went wrong'))
}, 1000)
})
console.log('Async operation completed successfully')
await next()
})
// 错误处理中间件:用于处理异常
app.use(async (ctx, next) => {
try {
await next()
} catch (err) {
console.error(`Error occurred: ${err.message}`)
ctx.status = 500
ctx.body = 'Internal Server Error'
}
})
app.listen(3000)
console.log('Server started at http://localhost:3000')
在上面的示例中,我们注册了两个中间件函数和一个错误处理中间件函数。第一个中间件函数用于打印请求日志,第二个中间件函数模拟了一个异步操作,并故意抛出了一个异常。第三个中间件函数用于处理异常,它会将异常信息打印到控制台,并将响应状态码设置为 500,响应内容设置为 "Internal Server Error"。当我们启动应用并访问 http://localhost:3000 时,控制台上会输出如下的错误信息:
Error occurred: Something went wrong
同时,浏览器上会显示 "Internal Server Error"。这说明我们的错误处理中间件成功捕获了异常,并进行了处理。
在没有async await 的时候, koa是怎么实现的洋葱模型?
在没有 async/await
的情况下,Koa 仍然可以实现洋葱模型,只不过需要使用 JavaScript 中的 Generator 函数来实现。 在 Generator 函数中,通过使用 yield
关键字,可以将函数的执行过程暂停,等待下一次调用时再继续执行。这个特性可以用来实现中间件的洋葱模型。 下面是一个使用 Generator 函数实现的 Koa 应用示例:
const Koa = require('koa')
const app = new Koa()
// 中间件函数1:用于打印请求日志
app.use(function* (next) {
console.log(`Received request: ${this.request.method} ${this.request.url}`)
yield next
})
// 中间件函数2:用于模拟一个异步操作
app.use(function* (next) {
console.log('Performing async operation...')
yield new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 1000)
})
console.log('Async operation completed successfully')
yield next
})
// 中间件函数3:用于响应请求
app.use(function* () {
this.body = 'Hello, World!'
})
app.listen(3000)
console.log('Server started at http://localhost:3000')
在这个示例中,我们使用 Generator 函数来定义中间件函数,其中 yield next
用于调用下一个中间件函数。这样,当请求进入应用时,会依次执行所有中间件函数,并在执行完最后一个中间件函数后返回响应。在这个过程中,中间件函数的执行顺序和洋葱模型相同。 需要注意的是,在使用 Generator 函数时,需要手动处理错误和异常,否则异常会被静默地忽略掉。另外,由于 Generator 函数的语法较为复杂,也不易于理解和维护,因此在 ES7 中引入了 async/await
语法糖来简化异步编程。
body-parser 中间件了解过吗
是的,body-parser
是一个常用的 Node.js 中间件,用于处理 HTTP 请求中的请求体(request body
),主要用于解析 JSON
、x-www-form-urlencoded
和 multipart/form-data
格式的请求体。 在使用 body-parser
中间件之前,我们通常需要手动处理请求体,例如通过读取 request
对象的 data
事件来获取请求体数据,然后进行解析和处理。这种方式比较繁琐,而且需要处理很多细节,例如流式数据的处理、编码转换等,因此不利于开发效率和代码的可读性。 使用 body-parser
中间件可以大大简化请求体的处理过程,例如:
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.json()) // 解析 application/json 类型的请求体
app.use(bodyParser.urlencoded({ extended: false })) // 解析 x-www-form-urlencoded 类型的请求体
app.post('/api/users', (req, res) => {
const { name, email } = req.body // 从请求体中获取数据
// 处理数据...
res.send('Data received!')
})
在上面的示例中,我们通过调用 bodyParser.json()
和 bodyParser.urlencoded()
方法来分别解析 JSON
和 x-www-form-urlencoded
格式的请求体。然后,在处理 POST
请求时,我们可以通过 req.body
属性来获取请求体中的数据,从而避免了手动解析请求体的麻烦。 需要注意的是,body-parser
中间件只能解析请求体中的数据,并不能防止 CSRF 攻击等安全问题,因此在实际开发中还需要采取其他措施来确保应用的安全性。
如果浏览器端用post接口上传图片和一些其他字段, header里会有什么? koa里如果不用body-parser,应该怎么解析?
如果浏览器端使用 POST 接口上传图片和其他字段,HTTP 请求的 header 中通常会包含以下信息:
- Content-Type:用于指定请求体的 MIME 类型,对于上传图片等二进制数据,通常是
multipart/form-data
,对于其他字段,通常是application/x-www-form-urlencoded
或application/json
。 - Content-Disposition:用于指定请求体中的每个部分的内容描述,例如
Content-Disposition: form-data; name="avatar"; filename="avatar.png"
表示请求体中的一个名为avatar
的字段,值为一个名为avatar.png
的文件。 如果在 Koa 中不使用body-parser
中间件解析请求体,可以使用stream
对象的方式来手动解析请求体数据。例如,可以监听request
对象的data
和end
事件来读取请求体数据,并将其解析为相应的格式。以下是一个示例代码:
// 解析 x-www-form-urlencoded 格式的请求体
function parseUrlencoded(ctx) {
return new Promise((resolve, reject) => {
const chunks = []
ctx.req.on('data', chunk => chunks.push(chunk))
ctx.req.on('end', () => {
const buffer = Buffer.concat(chunks)
const body = {}
buffer.toString().split('&').forEach(item => {
const [key, value] = item.split('=')
body[key] = decodeURIComponent(value)
})
resolve(body)
})
ctx.req.on('error', err => reject(err))
})
}
// 解析 multipart/form-data 格式的请求体
function parseMultipart(ctx) {
return new Promise((resolve, reject) => {
const busboy = new Busboy({ headers: ctx.req.headers })
const body = {}
busboy.on('field', (fieldname, val) => {
body[fieldname] = val
})
busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
// 处理文件上传
})
busboy.on('finish', () => resolve(body))
busboy.on('error', err => reject(err))
ctx.req.pipe(busboy)
})
}
// 使用上述函数解析请求体
app.use(async ctx => {
if (ctx.is('application/x-www-form-urlencoded')) {
ctx.request.body = await parseUrlencoded(ctx)
} else if (ctx.is('multipart/form-data')) {
ctx.request.body = await parseMultipart(ctx)
} else {
ctx.throw(400, 'Unsupported Media Type')
}
})
上述代码中使用了 parseUrlencoded
和 parseMultipart
两个函数来手动解析 x-www-form-urlencoded
和 multipart/form-data
两种格式的请求体。对于 x-www-form-urlencoded
,我们首先读取请求体数据,然后将其解析为键值对的形式;对于 multipart/form-data
,我们使用了 busboy
模块来解析请求体数据,同时也处理了上传的文件。最后,我们将解析后的请求体数据挂载到 ctx.request.body
上,供后续中间件和路由使用。
busbody是什么?
Busboy
是一个用于处理 multipart/form-data
数据的 Node.js 模块,可以用于上传文件等二进制数据。它可以将请求体拆分成多个部分,每个部分都有自己的标识符和类型,支持上传多个文件以及其他复杂的数据类型。
在处理文件上传时,一般需要执行以下操作:
- 为每个上传的文件创建一个可写流,并将其写入到磁盘文件中,同时记录文件的元信息,如文件名、文件大小、文件类型等。
- 监听可写流的
data
事件,将写入的数据保存到一个缓存区中,直到全部写入完成。 - 监听可写流的
end
事件,表示文件写入已经完成,可以关闭可写流,并将文件元信息返回给客户端。 以下是一个示例代码,展示了如何使用Busboy
模块处理文件上传:
const Busboy = require('busboy')
const path = require('path')
const fs = require('fs')
function handleFile(fieldname, file, filename, encoding, mimetype) {
const saveTo = path.join(__dirname, '../uploads', filename)
const ws = fs.createWriteStream(saveTo)
file.pipe(ws)
let size = 0
file.on('data', data => {
size += data.length
})
file.on('end', () => {
console.log(`File ${filename} saved to ${saveTo}, size: ${size}`)
})
}
function parseMultipart(ctx) {
return new Promise((resolve, reject) => {
const busboy = new Busboy({ headers: ctx.req.headers })
const files = []
busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
handleFile(fieldname, file, filename, encoding, mimetype)
files.push({ fieldname, filename, encoding, mimetype })
})
busboy.on('finish', () => {
resolve(files)
})
busboy.on('error', err => reject(err))
ctx.req.pipe(busboy)
})
}
app.use(async ctx => {
if (ctx.is('multipart/form-data')) {
const files = await parseMultipart(ctx)
console.log(files)
} else {
ctx.throw(400, 'Unsupported Media Type')
}
})
在上述代码中,我们定义了 handleFile
函数来处理每个上传的文件,它会根据文件名创建一个可写流,并将文件写入到磁盘文件中。同时,我们也监听了可写流的 data
和 end
事件,以便在文件写入完成后关闭可写流,并记录文件的元信息。在 parseMultipart
函数中,我们创建了一个 Busboy
实例,并监听了 file
和 finish
事件。当 file
事件触发时,我们调用 handleFile
函数来处理上传的文件,并将文件元信息保存到 files
数组中;当 finish
事件触发时,表示所有的文件上传已经完成,可以将 files
数组返回给客户端。最后,在 Koa 应用中,我们可以将解析后的文件信息保存到 ctx.request.files
对象中,以便后续中间件和路由处理。
转载自:https://juejin.cn/post/7217655750850609189