前端面试-实现一个简版koa

2023-12-18 15:38

本文主要是介绍前端面试-实现一个简版koa,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • koa的使用
  • 简单阅读下koa源码
  • ctx挂载了什么东西
  • next构建的洋葱模型

    • 中间件含异步代码如何保证正确执行
    • 解决多次调用next导致混乱问题
  • 基于事件驱动去处理异常

koa的使用

koa的使用非常简单,引入依赖后编写

const Koa = require('koa')
let app = new Koa()
app.use((ctx, next) => {console.log(ctx)
})
app.listen(4000)

然后在浏览器端打开http://127.0.0.1:4000即可访问

若没有指定返回body,koa默认处理成了Not Found

ctx

再进一步扩展代码,看看ctx上面有哪些东西

// ...console.log(ctx)console.log('native req ----') // node原生的reqconsole.log(ctx.req.url)console.log(ctx.request.req.url)console.log('koa request ----') // koa封装了requestconsole.log(ctx.url)console.log(ctx.request.url)// native req ----// /// /// koa request ----// /// /
// ...

以上代码存放在仓库,自取。

在koa官网有说明在ctx挂载了一系列requestresponse的属性别名。

ctx = {}
ctx.request = {}
ctx.response = {}
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
// ctx.url 代理了 ctx.request.url

next

以下代码存放在仓库,自取。

使用next看看作用

const Koa = require('koa')
let app = new Koa()
app.use((ctx, next) => {console.log(1)next()console.log(2)
})
app.use((ctx, next) => {console.log(3)next()console.log(4)
})
app.use((ctx, next) => {console.log(5)next()console.log(6)
})
app.listen(4000)
// 1
// 3
// 5
// 6
// 4
// 2

从上面代码打印结果可以看出,next的作用就是做一个占位符。可以看成以下形式

app.use((ctx, next) => {console.log(1)app.use((ctx, next) => {console.log(3)app.use((ctx, next) => {console.log(5)next()console.log(6)})console.log(4)})console.log(2)
})

这即是洋葱模型。

如果某个中间件有异步代码呢?

const Koa = require('koa')
let app = new Koa()
// 异步函数
const logger = () => {return new Promise((resolve, reject) => {setTimeout(_ => {console.log('logger')resolve()}, 1000)})
}
app.use((ctx, next) => {console.log(1)next()console.log(2)
})
app.use(async (ctx, next) => {console.log(3)await logger()next()console.log(4)
})
app.use((ctx, next) => {console.log(5)next()console.log(6)
})
app.listen(4000)
// 1
// 3
// 2
// 等待1s
// logger
// 5
// 6
// 4

此时打印结果并不是我们预期的结果,我们期望的是1 -> 3 -> 1s logger -> 5-> 6-> 4 ->2

此时我们需要在next前面加一个await

// ...
app.use(async (ctx, next) => {console.log(1)await next()console.log(2)
})
// ...

简单阅读下koa源码

koa致力于成为一个更小、更富有表现力、更健壮的web开发框架。

其源码也是非常轻量且易读。

核心文件四个

  • application.js:简单封装http.createServer()并整合context.js
  • context.js:代理并整合request.jsresponse.js
  • request.js:基于原生req封装的更好用
  • response.js:基于原生res封装的更好用

开始撸源码

下面涉及到的代码存放到仓库中,需要的自取。

koa是用ES6实现的,主要是两个核心方法app.listen()app.use((ctx, next) =< { ... })

先来在application.js中实现app.listen()

const http = require('http')
class Koa {constructor () {// ...}  // 处理用户请求handleRequest (req, res) {// ...}  listen (...args) {let server = http.createServer(this.handleRequest.bind(this))server.listen(...args)}  
}
module.exports = Koa

ctx挂载了什么东西

从上面的简单使用ctx中可以看出

ctx = {}
ctx.request = {}
ctx.response = {}
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
ctx.xxx = ctx.request.xxx
ctx.yyy = ctx.response.yyy

我们需要以上几个对象,最终都代理到ctx对象上。

创建context.js/request.js/response.js三个文件

request.js内容

const url = require('url')
let request = {}
module.exports = request

response.js内容

let response = {}
module.exports = response

context.js内容

let context = {}module.exports = context

application.js中引入上面三个文件并放到实例上

const context = require('./context')
const request = require('./request')
const response = require('./response')
class Koa extends Emitter{constructor () {super()// Object.create 切断原型链this.context = Object.create(context)this.request = Object.create(request)this.response = Object.create(response)}
}

由于不能直接用等号为其赋值,不然在修改变量属性时会直接篡改原始变量,因为对象引用了同一内存空间。

所以使用Object.create方法切断依赖,此方法相当于

function create (parentPrototype) {function F () {}F.prototype = parentPrototypereturn new F()
}

然后处理用户请求并在ctx上代理request / response

  // 创建上下文createContext (req, res) {let ctx = this.context// 请求ctx.request = this.requestctx.req = ctx.request.req = req// 响应ctx.response = this.responsectx.res = ctx.response.res = resreturn ctx}handleRequest (req, res) {let ctx = this.createContext(req, res)return ctx}

context.js中,使用__defineGetter__ / __defineSetter__实现代理,他是Object.defineProperty()方法的变种,可以单独设置get/set,不会覆盖设置。

let context = {}
// 定义获取器
function defineGetter (key, property) {context.__defineGetter__ (property, function () {return this[key][property]})
}
// 定义设置器
function defineSetter (key, property) {context.__defineSetter__ (property, function (val) {this[key][property] = val})
}
// 代理 request
defineGetter('request', 'path')
defineGetter('request', 'url')
defineGetter('request', 'query')
// 代理 response
defineGetter('response', 'body')
defineSetter('response', 'body')
module.exports = context

request.js中,使用ES5提供的属性访问器实现封装

const url = require('url')
let request = {get url () {return this.req.url // 此时的this为调用的对象 ctx.request},get path () {let { pathname } = url.parse(this.req.url)return pathname},get query () {let { query } = url.parse(this.req.url, true)return query}// ...更多待完善
}
module.exports = request

response.js中,使用ES5提供的属性访问器实现封装

let response = {set body (val) {this._body = val},get body () {return this._body // 此时的this为调用的对象 ctx.response}// ...更多待完善
}
module.exports = response

以上实现了封装request/response并代理到ctx

ctx = {}
ctx.request = {}
ctx.response = {}
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
ctx.xxx = ctx.request.xxx
ctx.yyy = ctx.response.yyy

next构建的洋葱模型

接下来实现koa中第二个方法app.use((ctx, next) =< { ... })

use中存放着一个个中间件,如cookie、session、static...等等一堆处理函数,并且以洋葱式的形式执行。

  constructor () {// ...// 存放中间件数组this.middlewares = []}// 使用中间件use (fn) {this.middlewares.push(fn)}

当处理用户请求时,期望执行所注册的一堆中间件

  // 组合中间件compose (middlewares, ctx) {function dispatch (index) {// 迭代终止条件 取完中间件// 然后返回成功的promiseif (index === middlewares.length) return Promise.resolve()let middleware = middlewares[index]// 让第一个函数执行完,如果有异步的话,需要看看有没有await// 必须返回一个promisereturn Promise.resolve(middleware(ctx, () => dispatch(index + 1)))}return dispatch(0)}// 处理用户请求handleRequest (req, res) {let ctx = this.createContext(req, res)this.compose(this.middlewares, ctx)return ctx}

以上的dispatch迭代函数在很多地方都有运用,比如递归删除目录,也是koa的核心。

中间件含异步代码如何保证正确执行

返回的promise主要是为了处理中间件中含有异步代码的情况

在所有中间件执行完毕后,需要渲染页面

  // 处理用户请求handleRequest (req, res) {let ctx = this.createContext(req, res)res.statusCode = 404 // 默认404 当设置body再做修改let ret = this.compose(this.middlewares, ctx)ret.then(_ => {if (!ctx.body) { // 没设置bodyres.end(`Not Found`)} else if (ctx.body instanceof Stream) { // 流res.setHeader('Content-Type', 'text/html;charset=utf-8')ctx.body.pipe(res)} else if (typeof ctx.body === 'object') { // 对象res.setHeader('Content-Type', 'text/josn;charset=utf-8')res.end(JSON.stringify(ctx.body))} else { // 字符串res.setHeader('Content-Type', 'text/html;charset=utf-8')res.end(ctx.body)}})return ctx}

需要考虑多种情况做兼容。

解决多次调用next导致混乱问题

通过以上代码进行以下测试

执行结果将是

// 1 => 3 =>1s,logger => 4
//   => 3 =>1s,logger => 4  => 2

并不满足我们的预期

因为执行过程如下

在第 2 步中, 传入的 i 值为 1, 因为还是在第一个中间件函数内部, 但是 compose 内部的 index 已经是 2 了, 所以 i < 2, 所以报错了, 可知在一个中间件函数内部不允许多次调用 next 函数。

解决方法就是使用flag作为洋葱模型的记录已经运行的函数中间件的下标, 如果一个中间件里面运行两次 next, 那么 index 是会比 flag 小的。

  /*** 组合中间件* @param {Array<Function>} middlewares * @param {context} ctx */ compose (middlewares, ctx) {let flag = -1function dispatch (index) {// 3)flag记录已经运行的中间件下标// 3.1)若一个中间件调用两次next那么index会小于flag// if (index <= flag) return Promise.reject(new Error('next() called multiple times'))flag = index// 2)迭代终止条件:取完中间件// 2.1)然后返回成功的promiseif (index === middlewares.length) return Promise.resolve()// 1)让第一个函数执行完,如果有异步的话,需要看看有没有await// 1.1)必须返回一个promiselet middleware = middlewares[index]return Promise.resolve(middleware(ctx, () => dispatch(index + 1)))}return dispatch(0)}

基于事件驱动去处理异常

如何处理在中间件中出现的异常呢?

Node是以事件驱动的,所以我们只需继承events模块即可

const Emitter = require('events')
class Koa extends Emitter{// ...// 处理用户请求handleRequest (req, res) {// ...let ret = this.compose(this.middlewares, ctx)ret.then(_ => {// ...}).catch(err => { // 处理程序异常this.emit('error', err)})return ctx}  
}

然后在上面做捕获异常,使用时如下就好

const Koa = require('./src/index')let app = new Koa()app.on('error', err => {console.log(err)
})

测试用例代码存放在仓库中,需要的自取。

总结

通过以上我们实现了一个简易的KOArequest/response.js文件还需扩展支持更多属性。

完整代码以及测试用例存放在@careteen/koa,感兴趣可前往调试。

这篇关于前端面试-实现一个简版koa的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/508947

相关文章

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

部署Vue项目到服务器后404错误的原因及解决方案

《部署Vue项目到服务器后404错误的原因及解决方案》文章介绍了Vue项目部署步骤以及404错误的解决方案,部署步骤包括构建项目、上传文件、配置Web服务器、重启Nginx和访问域名,404错误通常是... 目录一、vue项目部署步骤二、404错误原因及解决方案错误场景原因分析解决方案一、Vue项目部署步骤

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景