koa 入门 -->源码解析 --> 手写源码

在这里插入图片描述

本文分为三个部分:入门源码解析手写源码

入门

什么是 Koa
  • koa是基于nodejs平台的下一代web开发框架
  • express原班人马打造,更精简
  • Async + await 处理异步
  • 洋葱圈型的中间件机制
创建一个最简单的 koa

初始化一个 npm 项目

npm init -y

安装 koa 依赖 npm install koa --save

根目录下 app.js

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx, next) => {
    // ctx 是封装了 request 和 response 的上下文
    ctx.body = 'hello koa'
    // next 是下一个中间件
})

app.listen(3000)

根目录下运行 node app.js ,浏览器输入 localhost:3000,输出 hello koa,服务已经启动成功

const Koa = require('koa')
const app = new Koa()

// 135642
app.use(async (ctx, next) => {
    ctx.body = '1'
    next()
    ctx.body = ctx.body + '2'
})
app.use(async (ctx, next) => {
    ctx.body += '3'
    next()
    ctx.body +=  '4'
})
app.use(async (ctx, next) => {
    ctx.body += '5'
    next()
    ctx.body += '6'
})

// app.use(async (ctx) => {
//     ctx.body = 'hello koa'
// })

app.listen(3000)

浏览器输出 135642
在这里插入图片描述若将 第一个的 next() 注释掉,则只会输出 12

next() 的作用就是执行下面的中间件

(这个很重要,面试很常考👍👍)

解决异步的情况

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx, next) => {
    ctx.body = '1'
    setTimeout(() => {
        next()
    }, 2000)
    ctx.body = ctx.body + '2'
})
app.use(async (ctx, next) => {
    ctx.body += '3'
    next()
    ctx.body +=  '4'
})
app.use(async (ctx, next) => {
    ctx.body += '5'
    next()
    ctx.body += '6'
})

// app.use(async (ctx) => {
//     ctx.body = 'hello koa'
// })

app.listen(3000)

此时浏览器就是输出 12

const Koa = require('koa')
const app = new Koa()

// 135642

function delay () {
    return new Promise((reslove, reject) => {
        setTimeout(() => {
            reslove()
        }, 2000)
    })
}

app.use(async (ctx, next) => {
    ctx.body = '1'
    await next()
    ctx.body += '2'
})
app.use(async (ctx, next) => {
    ctx.body += '3'
    await delay()
    await next()
    ctx.body +=  '4'
})
app.use(async (ctx, next) => {
    ctx.body += '5'
    await next()
    ctx.body += '6'
})

// app.use(async (ctx) => {
//     ctx.body = 'hello koa'
// })

app.listen(3000)

此时浏览器输出 135642

对于请求的处理和响应

koa-router(路由处理)
npm install koa-router --save
var Koa = require('koa')
var Router = require('koa-router')

var app = new Koa()
var router = new Router()
//  实例化

router.get('/', async (ctx) => {
    ctx.body = 'home'
})

router.get('/news', async (ctx) => {
    ctx.body = 'news'
})

//启动路由
app.use(router.routes()).use(router.allowedMethods())
// 设置响应头

// 监听在3000端口
app.listen(3000)

get传值

// get传值
router.get('/newsContent', async (ctx) => {
    // http://localhost:3000/newsContent?aid=123&name=zs

    console.log(ctx.query)  // 推荐方式
    // { aid: '123', name: 'zs' }

    console.log(ctx.querystring)
    // aid=123&name=zs

    // console.log(ctx.request)
    /**
     * {
            method: 'GET',
            url: '/newsContent?aid=123&name=zs',
            header: {
            host: 'localhost:3000',
                connection: 'keep-alive',
                'cache-control': 'max-age=0',
                'upgrade-insecure-requests': '1',
                'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
            (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36',
                'sec-fetch-user': '?1',
                accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,im
            age/apng,*;q=0.8,application/signed-exchange;v=b3;q=0.9',
                'sec-fetch-site': 'none',
                'sec-fetch-mode': 'navigate',
                'accept-encoding': 'gzip, deflate, br',
                'accept-language': 'en,zh;q=0.9,zh-CN;q=0.8'
            }
        }
     */

    console.log(ctx.request.url)
    // /newsContent?aid=123&name=zs

    console.log(ctx.request.query)
    //[Object: null prototype] { aid: '123', name: 'zs' }

    console.log(ctx.request.querystring)
    //aid=123&name=zs

    ctx.body = '新闻详情'
})

动态匹配

router.get('/page/:id', async (ctx) => {
    // http://localhost:3000/page/112
    
    console.log(ctx.params)
    // {id: 112}

    ctx.body = '新闻详情'
})

router.get('/pages/:id/:bid', async (ctx) => {
    // http://localhost:3000/pages/112/qwe
    
    console.log(ctx.params)
    // { id: '112', bid: 'qwe' }

    ctx.body = '新闻详情'
})
gitee地址
koa 中间件应用
var Koa = require('koa')
var Router = require('koa-router')

var app = new Koa()
var router = new Router()

// 运行程序时,要注释掉无关的代码,否则容易报错.

// 应用中间件
/**
 * 会匹配任何的路由,无论访问任何的路由,都会打印出 middleware
 */
// 无 next(),匹配终止
app.use(async (ctx) => {
    ctx.body = 'middleware'
})

// 有 next() 就会接着向下执行
app.use(async (ctx, next) => {
    
    console.log(new Date())
    // 2020-04-06T01:53:02.529Z
    //2020-04-06T01:53:10.619Z
    await next()
})

router.get('/', async (ctx) => {
    console.log('home 01')
    ctx.body = 'home'
})

router.get('/news', async (ctx) => {
    console.log('news')
    ctx.body = 'news'
})

router.get('/login', async (ctx) => {
    ctx.body = 'login'
})

// 应用中间件




// 路由中间件
router.get('/pages', async (ctx, next) => {
    console.log('这是一个页面1')

    // 没有 await next (), ctx.body 没有值 界面会显示 Not Found
    // 程序没有办法继续执行
    await next()

    // next() 会继续向下匹配,之后在界面打印出 page,控制台也会输出 这是一个页面1
})

router.get('/pages', async (ctx) => {
    ctx.body = 'page'
})

// 路由中间件





// 中间件的执行与放置的位置无关

router.get('/', async (ctx) => {
    console.log('home 01')
    ctx.body = 'home'
})

router.get('/news', async (ctx) => {
    console.log('news')
    ctx.body = 'news'
})

router.get('/login', async (ctx) => {
    ctx.body = 'login'
})

app.use(async (ctx, next) => {
    console.log('middleware 01')
    next()

    /**
     * middleware 01
        home 01
     */

     /**
      * 无论放置在哪里,都会先执行中间件,之后继续匹配路由
      */
})

// 中间件的执行与放置的位置无关





// 寻找 404 界面
router.get('/', async (ctx) => {
    console.log('home 01')
    ctx.body = 'home'
})

router.get('/news', async (ctx) => {
    console.log('news')
    ctx.body = 'news'
})

router.get('/login', async (ctx) => {
    ctx.body = 'login'
})

app.use(async (ctx, next) => {
    console.log('middleware 01')
    next()

    /**
     * middleware 01
        home 01
     */

     /**
      * 无论放置在哪里,都会先执行中间件,之后继续匹配路由
      */

    if(ctx.status === 404) {
        ctx.status = 404
        ctx.body = '404 page'
    } else {
        console.log(ctx.url)
    }
    /**
     * 访问 /news 路由
     * 控制台输出
     * middleware 01
        news
        /news

        1.先执行中间件 输出 middleware 01
        2.存在 next (), 继续向下匹配路由
        3.存在 /news 界面,控制台输出 news
        4.ctx.body 有值,控制台输出 /news

        访问 /*** 路由

        1.先执行中间件 输出 middleware 01
        2.存在 next (), 继续向下匹配路由
        3.匹配不到,ctx.body = 404
        4.返回 404 page
     */
})



// 中间件的执行顺序
app.use(async (ctx, next) => {
    console.log('middleware 01')

    await next()

    console.log('return middleware 01')
})

app.use(async (ctx, next) => {
    console.log('middleware 02')

    await next()

    console.log('return middleware 02')
})

router.get('/news', async (ctx) => {
    console.log('匹配到 news page')
    ctx.body = 'news'
})

/**
 *  middleware 01
    middleware 02
    匹配到 news page
    return middleware 02
    return middleware 01

    1.先执行中间件 输出 middleware 01
    2.遇到 next() ,继续执行
    3.碰到中间件 输出 middleware 02
    4.无中间件 匹配路由,匹配到 /news
    5.输出 匹配到 news page
    6. ctx.body 有值,页面输出 news
    7.返回最后执行的中间件 ,输出 return middleware 02
    8.接着返回 输出 return middleware 01
 */

// 中间件的执行顺序





//启动路由
app.use(router.routes()).use(router.allowedMethods())
// 设置响应头

// 监听在3000端口
app.listen(3000)
ejs 模板
npm install koa-views --save npm install ejs --save
var Koa = require('koa')
var Router = require('koa-router')
var views = require('koa-views')

var app = new Koa()
var router = new Router()

// 配置模板引擎
app.use(views(__dirname + '/'), {
    map: {
        html: 'ejs'
    }
})

router.get('/', async (ctx) => {
    await ctx.render('./views/index.ejs')
})

router.get('/news', async (ctx) => {
    ctx.body = 'news'
})

//启动路由
app.use(router.routes()).use(router.allowedMethods())
// 设置响应头

// 监听在3000端口
app.listen(3000)
post 请求
npm install koa-bodyparser --save
var Koa = require('koa')
var Router = require('koa-router')
var views = require('koa-views')
var bodyParser = require('koa-bodyparser')

var app = new Koa()
var router = new Router()

// 配置模板引擎
app.use(views(__dirname + '/'), {
    map: {
        html: 'ejs'
    }
})

app.use(bodyParser())

router.get('/', async (ctx) => {
    await ctx.render('./views/index.ejs')
})

router.post('/doAdd', async (ctx, next) => {
    // 获取表单提交数据
    ctx.body = ctx.request.body
    console.log(ctx.body)
    // { username: 'zasss', password: '123456' }
})

//启动路由
app.use(router.routes()).use(router.allowedMethods())
// 设置响应头

// 监听在3000端口
app.listen(3000)
静态资源
npm install koa-static --save
var Koa = require('koa')
var Router = require('koa-router')
var views = require('koa-views')
var bodyParser = require('koa-bodyparser')
// 引入
const static = require('koa-static')

var app = new Koa()
var router = new Router()

app.use(views(__dirname + '/'), {
    map: {
        html: 'ejs'
    }
})

app.use(bodyParser())

// 静态资源
app.use(static('static'))

// app.use(static('public'))
// 静态资源 可以配置多个

router.get('/', async (ctx) => {
    await ctx.render('./views/index.ejs')
})

router.post('/doAdd', async (ctx, next) => {
    ctx.body = ctx.request.body
    console.log(ctx.body)
})

//启动路由
app.use(router.routes()).use(router.allowedMethods())
// 设置响应头

// 监听在3000端口
app.listen(3000)
koa-cookie
/**
 * 1 保存用户信息
 * 2 浏览器历史记录
 * 3 猜你喜欢的功能
 * 4 10天免登陆
 * 5 多个页面之间的数据传递
 * 6 cookie 实现购物车功能
 * 
 */

var Koa = require('koa')
var Router = require('koa-router')
var views = require('koa-views')
var bodyParser = require('koa-bodyparser')
// 引入
const static = require('koa-static')

var app = new Koa()
var router = new Router()

app.use(views(__dirname + '/'), {
    map: {
        html: 'ejs'
    }
})

app.use(bodyParser())

// 静态资源
app.use(static('static'))

// app.use(static('public'))
// 静态资源 可以配置多个

// 演示时,注释无关代码



// 设置 cookie
router.get('/', async (ctx) => {

    // 访问 / 路由时设置 cookies
    // maxAge: 设置过期时间
    // path 只有访问特定的路由才可以访问
    // domin 域名
    // httpOnly 只有服务器端才可以访问
    ctx.cookies.set('userInfo', 'zhangsan', {
        maxAge:  60 * 1000 * 60,
        // expires: '2020-04-07',
        // path: '/news',
        // domin: '',
        // httpOnly: true
    })
    await ctx.render('./views/index.ejs')
})

router.get('/news', async (ctx) => {

    // 访问 /news 路由的时候,可以取到相对应的 cookies
    console.log(ctx.cookies.get('userInfo'))
    await ctx.render('./views/index.ejs')
})

router.get('/login', async (ctx) => {

    // 访问 /login 路由的时候,可以取到相对应的 cookies
    console.log(ctx.cookies.get('userInfo'))
    ctx.body = ctx.cookies.get('userInfo')
    // await ctx.render('./views/index.ejs')
})
// 设置 cookie






// 设置中文 cookie
router.get('/', async (ctx) => {

    // value 设置为中文 argument value is invalid
    // 无法访问

    // koa 中 设置中文 Cookies

    var name = new Buffer('张三').toString('base64')

    ctx.cookies.set('userInfo', name, {
        maxAge:  60 * 1000 * 60,
    })
    await ctx.render('./views/index.ejs')
})

router.get('/news', async (ctx) => {

    // 访问 /news 路由的时候,可以取到相对应的 cookies

    var data = ctx.cookies.get('userInfo')

    var userinfo = new Buffer(data, 'base64').toString()

    console.log(userinfo)

    // console.log(ctx.cookies.get('userInfo'))
    await ctx.render('./views/index.ejs')
})
// 设置中文 cookie


//启动路由
app.use(router.routes()).use(router.allowedMethods())
// 设置响应头

// 监听在3000端口
app.listen(3000)
koa-session
/**
 * session 是一种记录客户状态的机制
 * 存储在服务端的
 */

var Koa = require('koa')
var Router = require('koa-router')
var views = require('koa-views')
var bodyParser = require('koa-bodyparser')
// 引入
const static = require('koa-static')
const session = require('koa-session')

var app = new Koa()
var router = new Router()

app.use(views(__dirname + '/'), {
    map: {
        html: 'ejs'
    }
})

app.use(bodyParser())

// 静态资源
app.use(static('static'))

// app.use(static('public'))
// 静态资源 可以配置多个

app.keys = ['some secret hurr']

const CONFIG = {
  key: 'koa:sess',
  maxAge: 9000, // 过期时间
  autoCommit: true,
  overwrite: true,
  httpOnly: true,
  signed: true,
  rolling: false,
  renew: true,
  sameSite: null,
}

app.use(session(CONFIG, app))

router.get('/', async (ctx) => {

    console.log(ctx.session.userInfo)

    await ctx.render('./views/index.ejs')
})

router.get('/login', async (ctx) => {

    ctx.session.userInfo = '张三'

    await ctx.render('./views/index.ejs')
})

router.get('/page', async (ctx, next) => {
    console.log(ctx.session.userInfo)
})

//启动路由
app.use(router.routes()).use(router.allowedMethods())
// 设置响应头

// 监听在3000端口
app.listen(3000)

好了,简单的结束了。接下来要进入头脑风暴了。
在这里插入图片描述

koa 源码解析

著名的洋葱圈

👉👉食堂牛肉烩面里的洋葱,我觉得巨好吃🤤🤤🤤
Image

由 package.json 文件的 main 字段可以得出,入口文件为 lib/application.js。

class Application extends Emitter {}

Application 类继承自 Emitter 类。

class Emitter 是 node 内置模块。

class Application extends Emitter {
    
  constructor(options) {
    super();
    options = options || {};
    this.proxy = options.proxy || false;
    this.subdomainOffset = options.subdomainOffset || 2;
    this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
    this.maxIpsCount = options.maxIpsCount || 0;
    this.env = options.env || process.env.NODE_ENV || 'development';
    if (options.keys) this.keys = options.keys;
    // 保存通过app.use(middleware)注册的中间件
    this.middleware = [];
    // Object.create => 都是对象
    // Context对象里就封装了上下文的所有变量和对象。
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }

  /**
   * 调用原生 http 模块开启服务并监听端口
   */

  listen(...args) {
    // this.callback()返回的就是回调函数,所以callback是一个高阶函数。
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }


  /**
   * 挂载中间件
   * use方法很简单,接受一个函数作为参数,并加入middleware数组。由于koa最开始支持使用generator函数作为中间件使用,但将在3.x的版本中放弃这项支持,因此koa2中对于使用generator函数作为中间件的行为给与未来将被废弃的警告,但会将generator函数转化为async函数。返回this便于链式调用。
   * @param {Function} fn
   * @return {Application} self
   * @api public
   * 只是把传入的回调函数先存到middleware数组里。
   */

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    // 用app.use的时候,实际上就是将函数push进middleware数组中,等待之后的调用。 
    this.middleware.push(fn);
    return this;
  }

  /**
   * 执行返回 http 请求的回调函数
   * 
   * 把handleRequest返回出去,供http.createServer回调,注意这里形成了一个闭包,能获得fn
   * @returns fn
   */
  callback() {
    // compose 函数将中间件数组转换成执行链函数 fn
    // 创建洋葱模型的入口函数
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

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

    return handleRequest;
  }
}

koa-compose 源码(洋葱模型实现)

compose 函数将中间件数组转换成执行链函数 fn

(源码部分在 node_modules/koa-compose)

/**
 * 接收一个middleware数组为参数,返回一个函数
 */

function compose (middleware) {
   // 对入参是不是数组和校验数组每一项是不是函数。
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} 传入对象 context
   * @return {Promise} 返回Promise
   */
  return function (context, next) {
    // last called middleware #
    let index = -1
    // 返回第一个 use 的中间件函数
    return dispatch(0)
      
    // 每个 dispatch 的返回值也是一个 Promise,因此我们的中间件中可以方便地使用async函数进行定义,内部使用await next()调用“下游”,然后控制流回“上游”,这是更准确也更友好的中间件模型
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      // 获取当前传入下标的中间件函数
      let fn = middleware[i]
      // 防止最后一个中间件执行next进行无限循坏
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        // 中间件的执行是异步的,并且中间件执行完毕后返回的是一个Promise
        // 洋葱模型的触发方式,先执行当前的中间,并将下一个中间作为第二个参数(next)传入当前中间件中
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

类似于这样的结构

const [fn1, fn2, fn3] = this.middleware;

// 先执行当前的中间,并将下一个中间作为第二个参数(next)传入当前中间件中
const fnMiddleware = function (context) {
  return Promise.resolve(
    fn1(context, function next() {
      return Promise.resolve(
        fn2(context, function next() {
          return Promise.resolve(
            fn3(context, function next() {
              return Promise.resolve();
            })
          )
        })
      )
    })
  );
};

fnMiddleware(ctx).then(handleResponse).catch(onerror);

koa-convert 源码(转换 generator 函数)

koa1中主要是generator函数。koa2中会自动转换generator函数。

app.use时有一层判断,是否是generator函数,如果是则用koa-convert暴露的方法convert来转换重新赋值,再存入middleware,后续再使用。

(源码部分在 node_modules/koa-convert)

function convert (mw) {
  if (typeof mw !== 'function') {
    throw new TypeError('middleware must be a function')
  }
  if (mw.constructor.name !== 'GeneratorFunction') {
    // assume it's Promise-based middleware
    return mw
  }
  const converted = function (ctx, next) {
    // 转换重新赋值,再存入middleware,后续再使用
    return co.call(ctx, mw.call(ctx, createGenerator(next)))
  }
  converted._name = mw._name || mw.name
  return converted
}

function * createGenerator (next) {
  return yield next()
}

co 源码

首先要明白 generator 函数是不会自动执行的,需要一步步调用 next()。 co 就是让 generator向 async、await 函数一样自动执行。

(源码部分在 node_modules/co)

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1)

  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    onFulfilled();


    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }


    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    // 反复执行调用自己
    function next(ret) {

      // 检查当前是否为 Generator 函数的最后一步,如果是就返回
      if (ret.done) return resolve(ret.value);
      // 确保返回值是promise对象
      var value = toPromise.call(ctx, ret.value);
      // 使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      // 在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
  });
}

小结👉👉👉:

koa-compose是将app.use添加到middleware数组中的中间件(函数),通过使用Promise串联起来,next()返回的是一个promise

koa-convert 判断app.use传入的函数是否是generator函数,如果是则用koa-convert来转换,最终还是调用的co来转换。

co源码实现原理:其实就是通过不断的调用generator函数的next()函数,来达到自动执行generator函数的效果(类似async、await函数的自动自行)。


以上是整个 koa 洋葱模型的核心概念。

application.js 文件中还定义了一些用于请求和响应的函数

handleRequest — 请求真正的回调函数
/**
  * 请求真正的回调函数
  * 核心在于执行fnMiddleware,这个其实就是 const fn = compose(this.middleware)
  * compose会串联顺序执行中间件next之前的代码,完成后倒序执行各中间件next之后的代码
 */

handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  // 一开始就将res的statusCode定义为404
  // 如果在我们没有设置body的情况下,默认就会返回404。
  res.statusCode = 404;
  const onerror = err => ctx.onerror(err);
  const handleResponse = () => respond(ctx);
  onFinished(res, onerror);
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
respond — 请求在经过所有中间件全部处理后的自动响应函数
function respond(ctx) {
  // 当 ctx 的 respond 为false 直接返回
  if (false === ctx.respond) return;

  // 当请求是scoket将根据socket的writable,否则都未true
  if (!ctx.writable) return;

  const res = ctx.res;
  let body = ctx.body;
  const code = ctx.status;

  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  // 请求是HEAD的一些处理
  if ('HEAD' === ctx.method) {
    if (!res.headersSent && !ctx.response.has('Content-Length')) {
      const { length } = ctx.response;
      if (Number.isInteger(length)) ctx.length = length;
    }
    return res.end();
  }

  // status body
  if (null == body) {
    if (ctx.response._explicitNullBody) {
      ctx.response.remove('Content-Type');
      ctx.response.remove('Transfer-Encoding');
      return res.end();
    }
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      body = ctx.message || String(code);
    }
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }

  // responses

  // 处理 Buffer 类型返回
  if (Buffer.isBuffer(body)) return res.end(body);
  // 处理字符串类型返回
  if ('string' === typeof body) return res.end(body);
  // 处理 Stream 类型返回
  if (body instanceof Stream) return body.pipe(res);

  // body: json 对象处理,转为JSON字符串返回
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}
createContext — 根据 req,res 创建上下文对象
/**
 * 根据 req,res 创建上下文对象
 * @api private
 * 每次回调过来都是创建新的context、request和response实例,这样本质上也对应了HTTP是无状态的。
 */

createContext(req, res) {
  const context = Object.create(this.context);
  const request = context.request = Object.create(this.request);
  const response = context.response = Object.create(this.response);
  context.app = request.app = response.app = this;
  context.req = request.req = response.req = req;
  context.res = request.res = response.res = res;
  // 注意context、request、response都有成员变量双向绑定在一起,从一个对象上能获取另外两个对象,后面可以看到很多context上的方法都被代理到了request和response上。
  request.ctx = response.ctx = context;
  request.response = response;
  response.request = request;
  context.originalUrl = request.originalUrl = req.url;
  //公共的存储内容,后端模板也会把这里的属性作为视图的上下文用于渲染
  context.state = {};
  return context;
}

context.js

// COOKIES是一个Symbol类型的私有变量
const COOKIES = Symbol('context#cookies');

const proto = module.exports = {

  /**
   * util.inspect() implementation, which
   * just returns the JSON output.
   *
   * @return {Object}
   * @api public
   */

  inspect() {
    if (this === proto) return this;
    return this.toJSON();
  },

  /**
   * 获取当前ctx的内容
   */

  toJSON() {
    return {
      request: this.request.toJSON(),
      response: this.response.toJSON(),
      app: this.app.toJSON(),
      originalUrl: this.originalUrl,
      req: '<original node req>',
      res: '<original node res>',
      socket: '<original node socket>'
    };
  },

  /**
   * http-assert,对http-errors的封装,一些基本的断言并设置http返回体
   */

  assert: httpAssert,

  throw(...args) {
    throw createError(...args);
  },

  /**
   *
   * @param {Error} err
   * @api private
   */

  onerror(err) {
    // 没有错误时什么也不做
    if (null == err) return;

    // err不是Error实例时,使用err创建一个Error实例
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error;
    if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));

    // headerSent变量表示响应头是否发送,如果响应头已经发送,或者响应处于不可写状态,将无法在响应中添加错误信息,直接退出该函数,否则需要将之前写入的响应头部信息清空。
    let headerSent = false;
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true;
    }

    // 触发koa实例app的error事件
    this.app.emit('error', err, this);

    // nothing we can do here other
    // than delegate to the app-level
    // handler and log.
    if (headerSent) {
      return;
    }

    const { res } = this;

    // 移除所有设置过的响应头
    if (typeof res.getHeaderNames === 'function') {
      res.getHeaderNames().forEach(name => res.removeHeader(name));
    } else {
      res._headers = {}; // Node < 7.7
    }

    // 设置错误头部
    this.set(err.headers);

    // 设置错误时的Content-Type
    this.type = 'text';

    let statusCode = err.status || err.statusCode;

    // 找不到文件错误码设为404
    if ('ENOENT' === err.code) statusCode = 404;

    // 不能被识别的错误将错误码设为500
    if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500;

    // respond
    const code = statuses[statusCode];
    const msg = err.expose ? err.message : code;
    this.status = err.status = statusCode;
    this.length = Buffer.byteLength(msg);
    res.end(msg);
  },

  // 处理Cookie
  get cookies() {
    if (!this[COOKIES]) {
      this[COOKIES] = new Cookies(this.req, this.res, {
        keys: this.app.keys,
        secure: this.request.secure
      });
    }
    return this[COOKIES];
  },

  set cookies(_cookies) {
    this[COOKIES] = _cookies;
  }
};

/**
 * 本质上是一个构造函数,接受两个参数,第一个参数是代理对象,第二个参数是被代理的对象
 *
 * context使用了delegates这个库。主要是将context中的一些值和函数代理到request和response中,这样实际上我们调用ctx.hostname获取值的时候,实际上是调用了req.hostname。从而方便调用。
 */

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('has')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .access('accept')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('URL')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip');

request.js 和 response.js 是对原生 req 和 res 的封装,同时也提供一些额外的值和函数。

整体流程🔎🔎:

初始化阶段: new初始化一个实例,use搜集中间件到middleware数组,listen 合成中间件fnMiddleware,返回一个callback函数给http.createServer,开启服务器,等待http请求。

请求阶段: 每次请求,createContext生成一个新的ctx,传给fnMiddleware,触发中间件的整个流程。

响应阶段: 整个中间件完成后,调用respond方法,对请求做最后的处理,返回响应给客户端。


手写 koa

koa 👉👉👉👉👉 kao

模仿 koa,搭建最基本的骨架:

kao/application.js

const http = require('http');

class Application {
  constructor() {
    // 回调函数
    this.callbackFn = null;
  }

  use(fn) {
    this.callbackFn = fn;
  }

  callback() {
    return (req, res) => this.callbackFn(req, res)
  }

  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
}

module.exports = Application;

测试文件kao/index.js

const Kao = require('./application');
const app = new Kao();

app.use(async (req, res) => {
  res.writeHead(200);
  res.end('hello world');
});

app.listen(3001, () => {
  console.log('server start at 3001');
});

通过new实例一个对象,use注册回调函数,listen启动server并传入回调。

使用 get 和 set 来处理原生请求,进行代理

kao/request.js

module.exports = {
  get header() {
    return this.req.headers;
  },

  set header(val) {
    this.req.headers = val;
  },

  get url() {
    return this.req.url;
  },

  set url(val) {
    this.req.url = val;
  },
}

当访问request.url时,其实就是在访问原生的req.url

kao/response.js

module.exports = {
  get status() {
    return this.res.statusCode;
  },

  set status(code) {
    this.res.statusCode = code;
  },

  get body() {
    return this._body;
  },

  set body(val) {
    // 源码里有对val类型的各种判断,这里省略
    /* 可能的类型
    1. string
    2. Buffer
    3. Stream
    4. Object
    */
    this._body = val;
  }
}

处理上下文对象 context

const delegate = require('delegates');

const proto = module.exports = {
  // context自身的方法
  toJSON() {
    return {
      request: this.request.toJSON(),
      response: this.response.toJSON(),
      app: this.app.toJSON(),
      originalUrl: this.originalUrl,
      req: '<original node req>',
      res: '<original node res>',
      socket: '<original node socket>'
    };
  },
}

// delegates 原理就是__defineGetter__和__defineSetter__

// method是委托方法,getter委托getter,access委托getter和setter。

// proto.status => proto.response.status
delegate(proto, 'response')
  .access('status')
  .access('body')


// proto.url = proto.request.url
delegate(proto, 'request')
  .access('url')
  .getter('header')

真正注入原生对象,是在application.js里的createContext方法中注入

const http = require('http');
const context = require('./context');
const request = require('./request');
const response = require('./response');

class Application {
  constructor() {
    this.callbackFn = null;
    // 每个Kao实例的context request respones
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }

  use(fn) {
    this.callbackFn = fn;
  }

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

    return handleRequest;
  }

  handleRequest(ctx) {
    const handleResponse = () => respond(ctx);
    // callbackFn是个async函数,最后返回promise对象
    return this.callbackFn(ctx).then(handleResponse);
  }

  createContext(req, res) {
    // 针对每个请求,都要创建ctx对象
    // 每个请求的ctx request response
    // ctx代理原生的req res就是在这里代理的
    let ctx = Object.create(this.context);
    ctx.request = Object.create(this.request);
    ctx.response = Object.create(this.response);
    ctx.req = ctx.request.req = req;
    ctx.res = ctx.response.res = res;
    ctx.app = ctx.request.app = ctx.response.app = this;
    return ctx;
  }

  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
}

module.exports = Application;

function respond(ctx) {
  // 根据ctx.body的类型,返回最后的数据
  /* 可能的类型,代码删减了部分判断
  1. string
  2. Buffer
  3. Stream
  4. Object
  */
  let content = ctx.body;
  if (typeof content === 'string') {
    ctx.res.end(content);
  }
  else if (typeof content === 'object') {
    ctx.res.end(JSON.stringify(content));
  }
}

代码中使用了Object.create的方法创建一个全新的对象,通过原型链继承原来的属性。这样可以有效的防止污染原来的对象。

createContext在每次http请求时都会调用,每次调用都新生成一个ctx对象,并且代理了这次http请求的原生的对象。

respond才是最后返回http响应的方法。根据执行完所有中间件后ctx.body的类型,调用res.end结束此次http请求。

洋葱圈模型的真正实现是 koa-compose,原理前面的源码分析已经讲过了。这里我们直接使用。

kao/compose.js

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      // 一个中间件里多次调用next
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      // fn就是当前的中间件
      let fn = middleware[i]
      if (i === middleware.length) fn = next // 最后一个中间件如果也next时进入(一般最后一个中间件是直接操作ctx.body,并不需要next了)
      if (!fn) return Promise.resolve() // 没有中间件,直接返回成功
      try {
        
        /* 
          * 使用了bind函数返回新的函数,类似下面的代码
          return Promise.resolve(fn(context, function next () {
            return dispatch(i + 1)
          }))
        */
        // dispatch.bind(null, i + 1)就是中间件里的next参数,调用它就可以进入下一个中间件

        // fn如果返回的是Promise对象,Promise.resolve直接把这个对象返回
        // fn如果返回的是普通对象,Promise.resovle把它Promise化
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        // 中间件是async的函数,报错不会走这里,直接在fnMiddleware的catch中捕获
        // 捕获中间件是普通函数时的报错,Promise化,这样才能走到fnMiddleware的catch方法
        return Promise.reject(err)
      }
    }
  }
}

module.exports = compose;

修改 kao/application.js

class Application {
  constructor() {
    this.middleware = []; // 存储中间件
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }

  use(fn) {
    this.middleware.push(fn); // 存储中间件
  }

  compose (middleware) {
    if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
    for (const fn of middleware) {
      if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
    }
  
    /**
     * @param {Object} context
     * @return {Promise}
     * @api public
     */
  
    return function (context, next) {
      // last called middleware #
      let index = -1
      return dispatch(0)
      function dispatch (i) {
        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, dispatch.bind(null, i + 1)));
        } catch (err) {
          return Promise.reject(err)
        }
      }
    }
  }
  

  callback() {
    // 合成所有中间件
    const fn = this.compose(this.middleware);

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

    return handleRequest;
  }

  handleRequest(ctx, fnMiddleware) {
    const handleResponse = () => respond(ctx);
    // 执行中间件并把最后的结果交给respond
    return fnMiddleware(ctx).then(handleResponse);
  }

  createContext(req, res) {
    // 针对每个请求,都要创建ctx对象
    let ctx = Object.create(this.context);
    ctx.request = Object.create(this.request);
    ctx.response = Object.create(this.response);
    ctx.req = ctx.request.req = req;
    ctx.res = ctx.response.res = res;
    ctx.app = ctx.request.app = ctx.response.app = this;
    return ctx;
  }

  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
}

module.exports = Application;

function respond(ctx) {
  let content = ctx.body;
  if (typeof content === 'string') {
    ctx.res.end(content);
  }
  else if (typeof content === 'object') {
    ctx.res.end(JSON.stringify(content));
  }
}

修改 index.js 进行测试

const Kao = require('./application');
const app = new Kao();

app.use(async (ctx, next) => {
  console.log('1-start');
  await next();
  console.log('1-end');
})

app.use(async (ctx) => {
  console.log('2-start');
  ctx.body = 'hello tc';
  console.log('2-end');
});

app.listen(3001, () => {
  console.log('server start at 3001');
});

// 1-start 2-start 2-end 1-end

错误处理机制

因为compose组合之后的函数返回的仍然是Promise对象,所以我们可以在catch捕获异常

kao/application.js

handleRequest(ctx, fnMiddleware) {
  const handleResponse = () => respond(ctx);
  const onerror = err => ctx.onerror(err);
  // catch捕获,触发ctx的onerror方法
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

kao/context.js

const proto = module.exports = {
  // context自身的方法
  onerror(err) {
    // 中间件报错捕获
    const { res } = this;

    if ('ENOENT' == err.code) {
      err.status = 404;
    } else {
      err.status = 500;
    }
    this.status = err.status;
    res.end(err.message || 'Internal error');
  }
}

kao/index.js

const Kao = require('./application');
const app = new Kao();

app.use(async (ctx) => {
  // 报错可以捕获
  a.b.c = 1;
  ctx.body = 'hello tc';
});

app.listen(3001, () => {
  console.log('server start at 3001');
});

Application继承原生的Emitter,从而实现error监听,实现框架层发生错误的捕获机制

kao/application.js

const Emitter = require('events');

// 继承Emitter
class Application extends Emitter {
  constructor() {
    // 调用super
    super();
    this.middleware = [];
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }
}

kao/context.js

const proto = module.exports = {
  onerror(err) {
    const { res } = this;

    if ('ENOENT' == err.code) {
      err.status = 404;
    } else {
      err.status = 500;
    }

    this.status = err.status;

    // 触发error事件
    this.app.emit('error', err, this);

    res.end(err.message || 'Internal error');
  }
}

kao/index.js

const Kao = require('./application');
const app = new Kao();

app.use(async (ctx) => {
  // 报错可以捕获
  a.b.c = 1;
  ctx.body = 'hello tc';
});

app.listen(3001, () => {
  console.log('server start at 3001');
});

// 监听error事件
app.on('error', (err) => {
  console.log(err.stack);
});

ok,以上就是本文的全部内容。建议阅读本文的同时,下载 koa 源码。多翻翻书,总是有好处的。

写着写着有点饿了,不说了,干饭去了。
在这里插入图片描述

参考文章:

https://juejin.cn/post/6844904088220467213

https://segmentfault.com/a/1190000019603834

https://zhuanlan.zhihu.com/p/90677000

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值