前端面试题 - 45. koa洋葱模型以及相关问题(附koa简易实现源码)

952 阅读3分钟

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),主要用于解析 JSONx-www-form-urlencodedmultipart/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() 方法来分别解析 JSONx-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-urlencodedapplication/json
  • Content-Disposition:用于指定请求体中的每个部分的内容描述,例如 Content-Disposition: form-data; name="avatar"; filename="avatar.png" 表示请求体中的一个名为 avatar 的字段,值为一个名为 avatar.png 的文件。 如果在 Koa 中不使用 body-parser 中间件解析请求体,可以使用 stream 对象的方式来手动解析请求体数据。例如,可以监听 request 对象的 dataend 事件来读取请求体数据,并将其解析为相应的格式。以下是一个示例代码:
// 解析 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')
  }
})

上述代码中使用了 parseUrlencodedparseMultipart 两个函数来手动解析 x-www-form-urlencodedmultipart/form-data 两种格式的请求体。对于 x-www-form-urlencoded,我们首先读取请求体数据,然后将其解析为键值对的形式;对于 multipart/form-data,我们使用了 busboy 模块来解析请求体数据,同时也处理了上传的文件。最后,我们将解析后的请求体数据挂载到 ctx.request.body 上,供后续中间件和路由使用。

busbody是什么?

Busboy 是一个用于处理 multipart/form-data 数据的 Node.js 模块,可以用于上传文件等二进制数据。它可以将请求体拆分成多个部分,每个部分都有自己的标识符和类型,支持上传多个文件以及其他复杂的数据类型。

在处理文件上传时,一般需要执行以下操作:

  1. 为每个上传的文件创建一个可写流,并将其写入到磁盘文件中,同时记录文件的元信息,如文件名、文件大小、文件类型等。
  2. 监听可写流的 data 事件,将写入的数据保存到一个缓存区中,直到全部写入完成。
  3. 监听可写流的 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 函数来处理每个上传的文件,它会根据文件名创建一个可写流,并将文件写入到磁盘文件中。同时,我们也监听了可写流的 dataend 事件,以便在文件写入完成后关闭可写流,并记录文件的元信息。在 parseMultipart 函数中,我们创建了一个 Busboy 实例,并监听了 filefinish 事件。当 file 事件触发时,我们调用 handleFile 函数来处理上传的文件,并将文件元信息保存到 files 数组中;当 finish 事件触发时,表示所有的文件上传已经完成,可以将 files 数组返回给客户端。最后,在 Koa 应用中,我们可以将解析后的文件信息保存到 ctx.request.files 对象中,以便后续中间件和路由处理。