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

2019-10-10 admin

目录

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 ----
  // /
  // /
// ...

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

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封装的更好用

开始撸源码

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

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 = 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
  })
}
// 代理 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) {
      // 迭代终止条件 取完中间件
      // 然后返回成功的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<Function>} 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文件还需扩展支持更多属性。

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

[转载]原文链接:https://segmentfault.com/a/1190000020641411

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-77063.html

文章标题:前端面试-实现一个简版koa

相关文章
2014年最流行前端开发框架对比评测
如今,各种开发框架层出不穷,各有千秋。哪些是去年较受开发者关注的呢?前不久,云适配根据Github上的流行程度整理了2014年最受欢迎的6个前端开发框架,并进行对比说明,希望帮助有需要的朋友选择合适自己的前端框架。 1. Bootstrap...
2015-11-12
javaScript+turn.js实现图书翻页效果实例代码
为了实现图书翻页的效果我们在网上可以看到很多教程 在这里推荐turn.js 网上的turn.js 有api 不过是英文的  很多人看起来不方便 .关于代码也是奇形怪状在这里我将详细讲解如何使用turn.js实现翻页效果 ,本篇文章只是讲解 ...
2017-03-16
梳理前端开发使用eslint-prettier检查和格式化代码
问题痛点 在团队的项目开发过程中,代码维护所占的时间比重往往大于新功能的开发。因此编写符合团队编码规范的代码是至关重要的,这样做不仅可以很大程度地避免基本语法错误,也保证了代码的可读性。 对于代码版本管理系统(svn 和 git或者其他)...
2018-05-07
js实现手机拍照上传功能
在前段时间的项目开发中,用到了拍照上传的地方,后来发现了最为简单的一种方法,现总结如下: &lt;form id=&quot;form&quot; method=&quot;post&quot; action=&quot;http:&#x2...
2017-03-06
Web前端开发与iOS终端开发的异同
毕业之前一直在做前端开发,毕业后就转成做iOS开发,这两者有很多挺有意思的对比,尝试写下我能想到的它们的一些相同点和不同点。 语言 前端和终端作为面向用户端的程序,有个共同特点:需要依赖用户机器的运行环境,所以开发语言基本上是没有选择的,...
2016-01-13
纯JS实现旋转图片3D展示效果
CSS: &lt;style type=&quot;text&#x2F;css&quot;&gt; #show{position:relative;margin:20px auto;width:800px;} .item{position:...
2017-03-22
js实现鼠标左右移动,图片也跟着移动效果
效果:鼠标往左移,图片对应右移,鼠标往右移,图片就左移动。图片距离越远偏移距离越大。 思路:首先获取图片原先的距离。设置一个变化值,图片的最终距离等于原先的距离加上变化值 布局:大盒子里面是图片,大盒子position:relative;图...
2017-02-17
vue.js实现请求数据的方法示例
vue2.0示例代码如下: var vm = new Vue({ el:&quot;#list&quot;, data:{ gridData: &quot;&quot;, }, ...
2017-03-20
vuejs通过filterBy、orderBy实现搜索筛选、降序排序数据
直接贴代码了: 先上输入前的样子: &lt;style&gt; #example{margin:100px auto;width:600px;} .show{margin:10px;} #searchText{display: block...
2017-03-17
前端MV*框架的意义
经常有人质疑,在前端搞MV有什么意义?也有人提出这样的疑问:以AngularJS,Knockout,BackBone为代表的MV框架,它跟jQuery这样的框架有什么区别?我jQuery用得好好的,有什么必要再引入这种框架? 回答这些问题之...
2016-03-11
回到顶部