前端开发者学习后端(二)——koa源码阅读

1、代码结构

下面是application.js代码结构,并不是完整的源码,方便读者阅读node

class Application extends Emitter{
    <!--初始化 new Koa()-->
    constructor() {
        this.proxy  //若是为 true,则解析 "Host" 的 header 域,并支持 X-Forwarded-Host
        this.subdomainOffset //表示 .subdomains 所忽略的字符偏移量
        this.env // 默认为 NODE_ENV or "development"
        this.middleware = [] // 中间件集合
        this.context
        this.request
        this.response
        this[util.inspect.custom] = this.inspect //将对象转换为字符串的方法,一般用于调试和错误输出,这里的用到node中util,this inspect调用 toJson方法中的only方法,返回对象
    }
    <!--listen方法,启动服务-->
    listen(...args) {
        <!--实际调用node.js 中的http.createServer服务-->
        const server = http.createServer(this.callback()) //返回http.server实例
        return server.listen(...args)
        <!--这里面的args是端口号+回调函数,因此根据node官网api规定用于TCP链接-->
    }
    <!--调用中间件的use方法-->
    use(fn){
        <!--第一件事判断是不是generator函数,若是是利用koa-conver(实际这里面利用co插件的wrap方法)转化成async await 函数-->
        if (isGeneratorFunction(fn)) {
          fn = convert(fn)
        }
        <!--第二件事,中间件存储到定义的变量数组中-->
        this.middleware.push(fn)
    }
    <!--http.createServer(callback)-->
    callback() {
        <!--第一步,利用koa-compose,整合全部的中间件-->
        const fn = compose(this.middleware)
        <!--闭包的形式返回-->
        const handleRequest = (req, res) => {
          <!--这里面req是http中IncomingMessage ,res是ServerResponse 类-->
          <!--createContext方法,下面解析-->
          const ctx = this.createContext(req, res)
          <!---->
          return this.handleRequest(ctx, fn)
        }
    
        return handleRequest
    }
}
复制代码

一、启动koa程序

<!--做为调试源码的程序-->
const Koa = require("Koa")
const app = new Koa()

app.use(async(ctx, next) => {
  console.log(1)
  await next()
  console.log(2)
  ctx.body = "这是第一个中间件"
})

app.use(async(ctx, next) => {
  console.log(3)
  await next()
  console.log(4)
  ctx.body = "这是第二个中间件"
})

app.listen(4001, () => { console.log("koa server is starting") })
复制代码

二、整个koa运行流程

koa源码由application.js|context.js|request.js|response.js 文件组成,实际在application.js就引用了其余三个js文件,并引用koa-compose核心文件,下面是本人经过vscode断点调试跟踪上面koa程序的流程图,不过仍是但愿想深刻学习的人,自行断点调试,如图mysql

注意:实心箭头表明是请求后代码的流程,线性箭头是初始化服务加载的流程web

2、主要方法

一、理解createContext方法

createContext(req, res) {
    <!--this.context 引用个是require("./context"),下面的1.1章节-->
    const context = Object.create(this.context)
    <!--request、response都是先引用对应库的基础方法,而后从新建立个新的对象-->
    const request = context.request = Object.create(this.request)
    const response = context.response = Object.create(this.response)
    <!--context对象包含request、response两个对象-->
    context.app = request.app = response.app = this
    context.req = request.req = response.req = req
    context.res = request.res = response.res = res
    <!--request、response两个对象又包含context-->
    request.ctx = response.ctx = context
    request.response = response
    response.request = request
    context.originalUrl = request.originalUrl = req.url
    context.state = {}
    return context
}
复制代码
1.1 查看context.js源码主要部分
const delegate = require('delegates')
const proto = module.exports = {
    <!--有个跟上述application.js文件中inspect相似的方法,省略-->
    <!--set cookie 和 get cookie 方法-->
}
delegate(proto, 'response')
  .method('set')
  ...
  .access('body')
  ...

delegate(proto, 'request')
  .method('get')
  ...
  .access('querystring')
  ...
<!--根据npm官网对delegates的解释就是委托事件,那么context.js重点的做用是res,req两个方法能加载这些委托的方法,换言之给context.js 返回的对象proto 对于request 、response 属性增长增、读取、设置、改变等基础操做方法-->
复制代码
1.2 查看context的返回的结果

利用上述的koa的启动程序,查看createContext的返回结果,也就是context对象的方法属性sql

{
	"request": {
		"method": "GET",
		"url": "/favicon.ico",
		"header": {
			"host": "localhost:4001",
			"connection": "keep-alive",
			"pragma": "no-cache",
			"cache-control": "no-cache",
			"sec-fetch-mode": "no-cors",
			"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36",
			"accept": "image/webp,image/apng,image/*,*/*;q=0.8",
			"sec-fetch-site": "same-origin",
			"referer": "http://localhost:4001/",
			"accept-encoding": "gzip, deflate, br",
			"accept-language": "zh-CN,zh;q=0.9"
		}
	},
	"response": {
		"status": 200,
		"message": "OK",
		"header": {}
	},
	"app": {
		"subdomainOffset": 2,
		"proxy": false,
		"env": "development"
	},
	"originalUrl": "/favicon.ico",
	"req": "<original node req>",
	"res": "<original node res>",
	"socket": "<original node socket>"
}
复制代码

www.jianshu.com/p/bca3a00f9… 这位博主说的挺详细的,可是我打印出来的结果和上述“request、response是相互包含的关系,两个对象又包含context”这句话相矛盾,若是你经过打断点看,context和request、response是相互包含的关系npm

二、理解koa-compose方法

下面是koa-compose源码主要部分api

function compose (middleware) {
    <!--判断是不是数组和函数,此处省略-->
    return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
    <!--判断 i<==index,若是 true 的话,则说明 next() 方法调用屡次,若是你在同一个中间件中执行屡次next()方法,会出现下面的错误-->
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      <!--记录当前执行中间件的下标-->
      index = i
      <!--当前的中间件-->
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
      <!--递归回调,旧版本的写法是
        {  return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))  }
      -->
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
复制代码

这里面最重要的函数是:数组

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
复制代码

一、上面代码执行了中间件fn(context, next),并传递了 context 和 next 函数两个参数,至于 next 函数则是返回一个 dispatch(i+1) 的执行结果;
二、这里面用bind方式,保证在app.use()方法中使用await next()手动执行下一个中间件,因此旧方法用function next () { return dispatch(i + 1) })这种方式看起来更直观;
三、最后(i+1)这个参数至关于执行了下一个中间件,从而造成递归调用。bash

3、源码中的知识点

阅读koa中的源码你会发现,它涉及最多的是闭包、高阶函数、函数柯里化,好比说callback方法和handleRequest()方法restful

callback() {
    const fn = compose(this.middleware)
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res)
      return this.handleRequest(ctx, fn)
    };

    return handleRequest;
  }
  handleRequest(ctx, fnMiddleware) {
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
复制代码

一、callback方中handleRequest是一个闭包函数,由于它能访问callback函数的做用域变量fn;
二、callback以handleRequest函数做为返回值,而handleRequest又以this.handleRequest(ctx, fn)做为返回值,而且this.handleRequest(ctx, fn)参数是fn也是函数,显而易见这些函数都是高阶函数cookie

又好比说下面一段代码就是“函数柯里化”

<!--简化版的koa-compose,middleware参数名变成fn-->
<!--compose函数柯里化-->
function compose (middleware) {
  return function (context, next) {
    return dispatch(0)
    function dispatch (i) {
        <!--middleware参数名变成fn-->
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
    }
  }
}
<!--callback方法中调用,可是fn没有当即执行,是一个Promise对象-->
const fn = compose(this.middleware)
<!--handleRequest方法中fn名称变成fnMiddleware,此方法执行后,也就执行了fn方法,也就是中间件方法,才有中间件的回调,才有启动程序console.log的输出-->
handleRequest(ctx, fnMiddleware) {
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
复制代码

4、后续更新计划

实现一个koa+mysql的简单项目,可是很规范的restful API

相关文章
相关标签/搜索