博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
前端面试-实现一个简版koa
阅读量:4089 次
发布时间:2019-05-25

本文共 8330 字,大约阅读时间需要 27 分钟。

目录

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原生的req  console.log(ctx.req.url)  console.log(ctx.request.req.url)  console.log('koa request ----') // koa封装了request  console.log(ctx.url)  console.log(ctx.request.url)  // native req ----  // /  // /  // koa request ----  // /  // /// ...

以上,自取。

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

ctx = {}ctx.request = {}ctx.response = {}ctx.req = ctx.request.req = reqctx.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.request = {}ctx.response = {}ctx.req = ctx.request.req = reqctx.res = ctx.response.res = resctx.xxx = ctx.request.xxxctx.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 = parentPrototype  return new F()}

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

// 创建上下文  createContext (req, res) {    let ctx = this.context    // 请求    ctx.request = this.request    ctx.req = ctx.request.req = req    // 响应    ctx.response = this.response    ctx.res = ctx.response.res = res    return 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  })}// 代理 requestdefineGetter('request', 'path')defineGetter('request', 'url')defineGetter('request', 'query')// 代理 responsedefineGetter('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 = reqctx.res = ctx.response.res = resctx.xxx = ctx.request.xxxctx.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) {      // 迭代终止条件 取完中间件      // 然后返回成功的promise      if (index === middlewares.length) return Promise.resolve()      let middleware = middlewares[index]      // 让第一个函数执行完,如果有异步的话,需要看看有没有await      // 必须返回一个promise      return 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) { // 没设置body        res.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
} middlewares * @param {context} ctx */ compose (middlewares, ctx) { let flag = -1 function 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)然后返回成功的promise if (index === middlewares.length) return Promise.resolve() // 1)让第一个函数执行完,如果有异步的话,需要看看有没有await // 1.1)必须返回一个promise let 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文件还需扩展支持更多属性。

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

转载地址:http://jsqni.baihongyu.com/

你可能感兴趣的文章
SEO 相关知识
查看>>
链式前向星
查看>>
首页列表显示全部问答,完成问答详情页布局。
查看>>
spdlog的简单封装和使用
查看>>
tyvj1153/洛谷P1262间谍网络
查看>>
maven阿里云镜像及本地仓库
查看>>
读一个文本Html网页
查看>>
PAT 乙级 1024
查看>>
android emulator 安装中文输入法
查看>>
windowsGDI对屏幕的更新
查看>>
c++学习7 -- 指针,空间的申请与释放
查看>>
MM相关T-code
查看>>
mysql启动问题
查看>>
Mysql储存过程6: in / out / inout
查看>>
如何合理和有效的进行数据库设计
查看>>
Codeforces957 Mahmoud and Ehab and yet another xor task
查看>>
eclipse 设置tomcat 内存
查看>>
Linux常用命令学习
查看>>
洛谷2184 贪婪大陆(树状数组)
查看>>
input 标签禁止输入
查看>>