经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » Node.js » 查看文章
如何从零开始手写Koa2框架
来源:jb51  时间:2019/3/22 12:08:55  对本文有异议

01、介绍

  • Koa-- 基于 Node.js 平台的下一代 web 开发框架

  • Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。

  • 与其对应的 Express 来比,Koa 更加小巧、精壮,本文将带大家从零开始实现 Koa 的源码,从根源上解决大家对 Koa 的困惑

本文 Koa 版本为 2.7.0, 版本不一样源码可能会有变动

02、源码目录介绍

Koa 源码目录截图

1.png

通过源码目录可以知道,Koa主要分为4个部分,分别是:

  • application: Koa 最主要的模块, 对应 app 应用对象

  • context: 对应 ctx 对象

  • request: 对应 Koa 中请求对象

  • response: 对应 Koa 中响应对象

这4个文件就是 Koa 的全部内容了,其中 application 又是其中最核心的文件。我们将会从此文件入手,一步步实现 Koa 框架

03、实现一个基本服务器代码目录

my-application

  1. const {createServer} = require('http');
  2.  
  3. module.exports = class Application {
  4.  constructor() {
  5.  // 初始化中间件数组, 所有中间件函数都会添加到当前数组中
  6.  this.middleware = [];
  7.  }
  8.  // 使用中间件方法
  9.  use(fn) {
  10.  // 将所有中间件函数添加到中间件数组中
  11.  this.middleware.push(fn);
  12.  }
  13.  // 监听端口号方法
  14.  listen(...args) {
  15.  // 使用nodejs的http模块监听端口号
  16.  const server = createServer((req, res) => {
  17.   /*
  18.   处理请求的回调函数,在这里执行了所有中间件函数
  19.   req 是 node 原生的 request 对象
  20.   res 是 node 原生的 response 对象
  21.   */
  22.   this.middleware.forEach((fn) => fn(req, res));
  23.  })
  24.  server.listen(...args);
  25.  }
  26. }

index.js

  1. // 引入自定义模块
  2. const MyKoa = require('./js/my-application');
  3. // 创建实例对象
  4. const app = new MyKoa();
  5. // 使用中间件
  6. app.use((req, res) => {
  7.  console.log('中间件函数执行了~~~111');
  8. })
  9. app.use((req, res) => {
  10.  console.log('中间件函数执行了~~~222');
  11.  res.end('hello myKoa');
  12. })
  13. // 监听端口号
  14. app.listen(3000, err => {
  15.  if (!err) console.log('服务器启动成功了');
  16.  else console.log(err);
  17. })

运行入口文件 index.js 后,通过浏览器输入网址访问 http://localhost:3000/ , 就可以看到结果了~~

神奇吧!一个最简单的服务器模型就搭建完了。当然我们这个极简服务器还存在很多问题,接下来让我们一一解决

04、实现中间件函数的 next 方法

提取createServer的回调函数,封装成一个callback方法(可复用)

  1. // 监听端口号方法
  2. listen(...args) {
  3.  // 使用nodejs的http模块监听端口号
  4.  const server = createServer(this.callback());
  5.  server.listen(...args);
  6. }
  7. callback() {
  8.  const handleRequest = (req, res) => {
  9.  this.middleware.forEach((fn) => fn(req, res));
  10.  }
  11.  return handleRequest;
  12. }

封装compose函数实现next方法

  1. // 负责执行中间件函数的函数
  2. function compose(middleware) {
  3.  // compose方法返回值是一个函数,这个函数返回值是一个promise对象
  4.  // 当前函数就是调度
  5.  return (req, res) => {
  6.  // 默认调用一次,为了执行第一个中间件函数
  7.  return dispatch(0);
  8.  function dispatch(i) {
  9.   // 提取中间件数组的函数fn
  10.   let fn = middleware[i];
  11.   // 如果最后一个中间件也调用了next方法,直接返回一个成功状态的promise对象
  12.   if (!fn) return Promise.resolve();
  13.   /*
  14.   dispatch.bind(null, i + 1)) 作为中间件函数调用的第三个参数,其实就是对应的next
  15.    举个栗子:如果 i = 0 那么 dispatch.bind(null, 1)) 
  16.    --> 也就是如果调用了next方法 实际上就是执行 dispatch(1) 
  17.     --> 它利用递归重新进来取出下一个中间件函数接着执行
  18.   fn(req, res, dispatch.bind(null, i + 1))
  19.    --> 这也是为什么中间件函数能有三个参数,在调用时我们传进来了
  20.   */
  21.   return Promise.resolve(fn(req, res, dispatch.bind(null, i + 1)));
  22.  }
  23.  }
  24. }

使用compose函数

  1. callback () {
  2.  // 执行compose方法返回一个函数
  3.  const fn = compose(this.middleware);
  4.  
  5.  const handleRequest = (req, res) => {
  6.  // 调用该函数,返回值为promise对象
  7.  // then方法触发了, 说明所有中间件函数都被调用完成
  8.  fn(req, res).then(() => {
  9.   // 在这里就是所有处理的函数的最后阶段,可以允许返回响应了~
  10.  });
  11.  }
  12.  
  13.  return handleRequest;
  14. }

修改入口文件 index.js 代码

  1. // 引入自定义模块
  2. const MyKoa = require('./js/my-application');
  3. // 创建实例对象
  4. const app = new MyKoa();
  5. // 使用中间件
  6. app.use((req, res, next) => {
  7.  console.log('中间件函数执行了~~~111');
  8.  // 调用next方法,就是调用堆栈中下一个中间件函数
  9.  next();
  10. })
  11. app.use((req, res, next) => {
  12.  console.log('中间件函数执行了~~~222');
  13.  res.end('hello myKoa');
  14.  // 最后的next方法没发调用下一个中间件函数,直接返回Promise.resolve()
  15.  next();
  16. })
  17. // 监听端口号
  18. app.listen(3000, err => {
  19.  if (!err) console.log('服务器启动成功了');
  20.  else console.log(err);
  21. })

此时我们实现了next方法,最核心的就是compose函数,极简的代码实现了功能,不可思议!

05、处理返回响应

定义返回响应函数respond

  1. function respond(req, res) {
  2.  // 获取设置的body数据
  3.  let body = res.body;
  4.  
  5.  if (typeof body === 'object') {
  6.  // 如果是对象,转化成json数据返回
  7.  body = JSON.stringify(body);
  8.  res.end(body);
  9.  } else {
  10.  // 默认其他数据直接返回
  11.  res.end(body);
  12.  }
  13. }

callback中调用

  1. callback() {
  2.  const fn = compose(this.middleware);
  3.  
  4.  const handleRequest = (req, res) => {
  5.  // 当中间件函数全部执行完毕时,会触发then方法,从而执行respond方法返回响应
  6.  const handleResponse = () => respond(req, res);
  7.  fn(req, res).then(handleResponse);
  8.  }
  9.  
  10.  return handleRequest;
  11. }

修改入口文件 index.js 代码

  1. // 引入自定义模块
  2. const MyKoa = require('./js/my-application');
  3. // 创建实例对象
  4. const app = new MyKoa();
  5. // 使用中间件
  6. app.use((req, res, next) => {
  7.  console.log('中间件函数执行了~~~111');
  8.  next();
  9. })
  10. app.use((req, res, next) => {
  11.  console.log('中间件函数执行了~~~222');
  12.  // 设置响应内容,由框架负责返回响应~
  13.  res.body = 'hello myKoa';
  14. })
  15. // 监听端口号
  16. app.listen(3000, err => {
  17.  if (!err) console.log('服务器启动成功了');
  18.  else console.log(err);
  19. })

此时我们就能根据不同响应内容做出处理了~当然还是比较简单的,可以接着去扩展~

06、定义 Request 模块

  1. // 此模块需要npm下载
  2. const parse = require('parseurl');
  3. const qs = require('querystring');
  4.  
  5. module.exports = {
  6.  /**
  7.  * 获取请求头信息
  8.  */
  9.  get headers() {
  10.  return this.req.headers;
  11.  },
  12.  /**
  13.  * 设置请求头信息
  14.  */
  15.  set headers(val) {
  16.  this.req.headers = val;
  17.  },
  18.  /**
  19.  * 获取查询字符串
  20.  */
  21.  get query() {
  22.  // 解析查询字符串参数 --> key1=value1&key2=value2
  23.  const querystring = parse(this.req).query;
  24.  // 将其解析为对象返回 --> {key1: value1, key2: value2}
  25.  return qs.parse(querystring);
  26.  }
  27. }

07、定义 Response 模块

  1. module.exports = {
  2.  /**
  3.  * 设置响应头的信息
  4.  */
  5.  set(key, value) {
  6.  this.res.setHeader(key, value);
  7.  },
  8.  /**
  9.  * 获取响应状态码
  10.  */
  11.  get status() {
  12.  return this.res.statusCode;
  13.  },
  14.  /**
  15.  * 设置响应状态码
  16.  */
  17.  set status(code) {
  18.  this.res.statusCode = code;
  19.  },
  20.  /**
  21.  * 获取响应体信息
  22.  */
  23.  get body() {
  24.  return this._body;
  25.  },
  26.  /**
  27.  * 设置响应体信息
  28.  */
  29.  set body(val) {
  30.  // 设置响应体内容
  31.  this._body = val;
  32.  // 设置响应状态码
  33.  this.status = 200;
  34.  // json
  35.  if (typeof val === 'object') {
  36.   this.set('Content-Type', 'application/json');
  37.  }
  38.  },
  39. }

08、定义 Context 模块

  1. // 此模块需要npm下载
  2. const delegate = require('delegates');
  3.  
  4. const proto = module.exports = {};
  5.  
  6. // 将response对象上的属性/方法克隆到proto上
  7. delegate(proto, 'response')
  8.  .method('set') // 克隆普通方法
  9.  .access('status') // 克隆带有get和set描述符的方法
  10.  .access('body') 
  11.  
  12. // 将request对象上的属性/方法克隆到proto上
  13. delegate(proto, 'request')
  14.  .access('query')
  15.  .getter('headers') // 克隆带有get描述符的方法

09、揭秘 delegates 模块

  1. module.exports = Delegator;
  2.  
  3. /**
  4.  * 初始化一个 delegator.
  5.  */
  6. function Delegator(proto, target) {
  7.  // this必须指向Delegator的实例对象
  8.  if (!(this instanceof Delegator)) return new Delegator(proto, target);
  9.  // 需要克隆的对象
  10.  this.proto = proto;
  11.  // 被克隆的目标对象
  12.  this.target = target;
  13.  // 所有普通方法的数组
  14.  this.methods = [];
  15.  // 所有带有get描述符的方法数组
  16.  this.getters = [];
  17.  // 所有带有set描述符的方法数组
  18.  this.setters = [];
  19. }
  20.  
  21. /**
  22.  * 克隆普通方法
  23.  */
  24. Delegator.prototype.method = function(name){
  25.  // 需要克隆的对象
  26.  var proto = this.proto;
  27.  // 被克隆的目标对象
  28.  var target = this.target;
  29.  // 方法添加到method数组中
  30.  this.methods.push(name);
  31.  // 给proto添加克隆的属性
  32.  proto[name] = function(){
  33.  /*
  34.   this指向proto, 也就是ctx
  35.   举个栗子:ctx.response.set.apply(ctx.response, arguments)
  36.   arguments对应实参列表,刚好与apply方法传参一致
  37.   执行ctx.set('key', 'value') 实际上相当于执行 response.set('key', 'value')
  38.  */
  39.  return this[target][name].apply(this[target], arguments);
  40.  };
  41.  // 方便链式调用
  42.  return this;
  43. };
  44.  
  45. /**
  46.  * 克隆带有get和set描述符的方法.
  47.  */
  48. Delegator.prototype.access = function(name){
  49.  return this.getter(name).setter(name);
  50. };
  51.  
  52. /**
  53.  * 克隆带有get描述符的方法.
  54.  */
  55. Delegator.prototype.getter = function(name){
  56.  var proto = this.proto;
  57.  var target = this.target;
  58.  this.getters.push(name);
  59.  // 方法可以为一个已经存在的对象设置get描述符属性
  60.  proto.__defineGetter__(name, function(){
  61.  return this[target][name];
  62.  });
  63.  
  64.  return this;
  65. };
  66.  
  67. /**
  68.  * 克隆带有set描述符的方法.
  69.  */
  70. Delegator.prototype.setter = function(name){
  71.  var proto = this.proto;
  72.  var target = this.target;
  73.  this.setters.push(name);
  74.  // 方法可以为一个已经存在的对象设置set描述符属性
  75.  proto.__defineSetter__(name, function(val){
  76.  return this[target][name] = val;
  77.  });
  78.  
  79.  return this;
  80. };

10、使用 ctx 取代 req 和 res

修改 my-application

  1. const {createServer} = require('http');
  2. const context = require('./my-context');
  3. const request = require('./my-request');
  4. const response = require('./my-response');
  5.  
  6. module.exports = class Application {
  7.  constructor() {
  8.  this.middleware = [];
  9.  // Object.create(target) 以target对象为原型, 创建新对象, 新对象原型有target对象的属性和方法
  10.  this.context = Object.create(context);
  11.  this.request = Object.create(request);
  12.  this.response = Object.create(response);
  13.  }
  14.  
  15.  use(fn) {
  16.  this.middleware.push(fn);
  17.  }
  18.  
  19.  listen(...args) {
  20.  // 使用nodejs的http模块监听端口号
  21.  const server = createServer(this.callback());
  22.  server.listen(...args);
  23.  }
  24.  
  25.  callback() {
  26.  const fn = compose(this.middleware);
  27.  
  28.  const handleRequest = (req, res) => {
  29.   // 创建context
  30.   const ctx = this.createContext(req, res);
  31.   const handleResponse = () => respond(ctx);
  32.   fn(ctx).then(handleResponse);
  33.  }
  34.  
  35.  return handleRequest;
  36.  }
  37.  
  38.  // 创建context 上下文对象的方法
  39.  createContext(req, res) {
  40.  /*
  41.   凡是req/res,就是node原生对象
  42.   凡是request/response,就是自定义对象
  43.   这是实现互相挂载引用,从而在任意对象上都能获取其他对象的方法
  44.   */
  45.  const context = Object.create(this.context);
  46.  const request = context.request = Object.create(this.request);
  47.  const response = context.response = Object.create(this.response);
  48.  context.app = request.app = response.app = this;
  49.  context.req = request.req = response.req = req;
  50.  context.res = request.res = response.res = res;
  51.  request.ctx = response.ctx = context;
  52.  request.response = response;
  53.  response.request = request;
  54.  
  55.  return context;
  56.  }
  57. }
  58. // 将原来使用req,res的地方改用ctx
  59. function compose(middleware) {
  60.  return (ctx) => {
  61.  return dispatch(0);
  62.  function dispatch(i) {
  63.   let fn = middleware[i];
  64.   if (!fn) return Promise.resolve();
  65.   return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)));
  66.  }
  67.  }
  68. }
  69.  
  70. function respond(ctx) {
  71.  let body = ctx.body;
  72.  const res = ctx.res;
  73.  if (typeof body === 'object') {
  74.  body = JSON.stringify(body);
  75.  res.end(body);
  76.  } else {
  77.  res.end(body);
  78.  }
  79. }

修改入口文件 index.js 代码

  1. // 引入自定义模块
  2. const MyKoa = require('./js/my-application');
  3. // 创建实例对象
  4. const app = new MyKoa();
  5. // 使用中间件
  6. app.use((ctx, next) => {
  7.  console.log('中间件函数执行了~~~111');
  8.  next();
  9. })
  10. app.use((ctx, next) => {
  11.  console.log('中间件函数执行了~~~222');
  12.  // 获取请求头参数
  13.  console.log(ctx.headers);
  14.  // 获取查询字符串参数
  15.  console.log(ctx.query);
  16.  // 设置响应头信息
  17.  ctx.set('content-type', 'text/html;charset=utf-8');
  18.  // 设置响应内容,由框架负责返回响应~
  19.  ctx.body = '<h1>hello myKoa</h1>';
  20. })
  21. // 监听端口号
  22. app.listen(3000, err => {
  23.  if (!err) console.log('服务器启动成功了');
  24.  else console.log(err);
  25. })
到这里已经写完了 Koa 主要代码,有一句古话 - 看万遍代码不如写上一遍。 还等什么,赶紧写上一遍吧~
当你能够写出来,再去阅读源码,你会发现源码如此简单~

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持w3xue。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号