博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从零开始手写Koa2框架
阅读量:6239 次
发布时间:2019-06-22

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

01、介绍

  • Koa -- 基于 Node.js 平台的下一代 web 开发框架
  • Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。
  • 与其对应的 Express 来比,Koa 更加小巧、精壮,本文将带大家从零开始实现 Koa 的源码,从根源上解决大家对 Koa 的困惑

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

已经开放,如果本文对你有帮助,欢迎 star ~~

02、源码目录介绍

  • Koa 源码目录截图
  • 通过源码目录可以知道,Koa主要分为4个部分,分别是:
    • application: Koa 最主要的模块, 对应 app 应用对象
    • context: 对应 ctx 对象
    • request: 对应 Koa 中请求对象
    • response: 对应 Koa 中响应对象
  • 这4个文件就是 Koa 的全部内容了,其中 application 又是其中最核心的文件。我们将会从此文件入手,一步步实现 Koa 框架

03、实现一个基本服务器

  • 代码目录

  • my-application

    const {createServer} = require('http');module.exports = class Application {  constructor() {    // 初始化中间件数组, 所有中间件函数都会添加到当前数组中    this.middleware = [];  }  // 使用中间件方法  use(fn) {    // 将所有中间件函数添加到中间件数组中    this.middleware.push(fn);  }  // 监听端口号方法  listen(...args) {    // 使用nodejs的http模块监听端口号    const server = createServer((req, res) => {      /*        处理请求的回调函数,在这里执行了所有中间件函数        req 是 node 原生的 request 对象        res 是 node 原生的 response 对象      */      this.middleware.forEach((fn) => fn(req, res));    })    server.listen(...args);  }}复制代码
  • index.js

    // 引入自定义模块const MyKoa = require('./js/my-application');// 创建实例对象const app = new MyKoa();// 使用中间件app.use((req, res) => {  console.log('中间件函数执行了~~~111');})app.use((req, res) => {  console.log('中间件函数执行了~~~222');  res.end('hello myKoa');})// 监听端口号app.listen(3000, err => {  if (!err) console.log('服务器启动成功了');  else console.log(err);})复制代码
  • 运行入口文件 index.js 后,通过浏览器输入网址访问 http://localhost:3000/ , 就可以看到结果了~~

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

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

  • 提取createServer的回调函数,封装成一个callback方法(可复用)
    // 监听端口号方法listen(...args) {  // 使用nodejs的http模块监听端口号  const server = createServer(this.callback());  server.listen(...args);}callback() {  const handleRequest = (req, res) => {    this.middleware.forEach((fn) => fn(req, res));  }  return handleRequest;}复制代码
  • 封装compose函数实现next方法
    /** * 负责执行中间件函数的函数 * @param middleware 中间件数组 * @return {
    function} */function compose(middleware) { // compose方法返回值是一个函数,这个函数返回值是一个promise对象 // 当前函数就是调度 return (req, res) => { // 默认调用一次,为了执行第一个中间件函数 return dispatch(0); function dispatch(i) { // 提取中间件数组的函数fn let fn = middleware[i]; // 如果最后一个中间件也调用了next方法,直接返回一个成功状态的promise对象 if (!fn) return Promise.resolve(); /* dispatch.bind(null, i + 1)) 作为中间件函数调用的第三个参数,其实就是对应的next 举个栗子:如果 i = 0 那么 dispatch.bind(null, 1)) --> 也就是如果调用了next方法 实际上就是执行 dispatch(1) --> 它利用递归重新进来取出下一个中间件函数接着执行 fn(req, res, dispatch.bind(null, i + 1)) --> 这也是为什么中间件函数能有三个参数,在调用时我们传进来了 */ return Promise.resolve(fn(req, res, dispatch.bind(null, i + 1))); } }}复制代码
  • 使用compose函数
    callback () {  // 执行compose方法返回一个函数  const fn = compose(this.middleware);    const handleRequest = (req, res) => {    // 调用该函数,返回值为promise对象    // then方法触发了, 说明所有中间件函数都被调用完成    fn(req, res).then(() => {      // 在这里就是所有处理的函数的最后阶段,可以允许返回响应了~    });  }    return handleRequest;}复制代码
  • 修改入口文件 index.js 代码
    // 引入自定义模块const MyKoa = require('./js/my-application');// 创建实例对象const app = new MyKoa();// 使用中间件app.use((req, res, next) => {  console.log('中间件函数执行了~~~111');  // 调用next方法,就是调用堆栈中下一个中间件函数  next();})app.use((req, res, next) => {  console.log('中间件函数执行了~~~222');  res.end('hello myKoa');  // 最后的next方法没发调用下一个中间件函数,直接返回Promise.resolve()  next();})// 监听端口号app.listen(3000, err => {  if (!err) console.log('服务器启动成功了');  else console.log(err);})复制代码
  • 此时我们实现了next方法,最核心的就是compose函数,极简的代码实现了功能,不可思议!

05、处理返回响应

  • 定义返回响应函数respond
    function respond(req, res) {  // 获取设置的body数据  let body = res.body;    if (typeof body === 'object') {    // 如果是对象,转化成json数据返回    body = JSON.stringify(body);    res.end(body);  } else {    // 默认其他数据直接返回    res.end(body);  }}复制代码
  • callback中调用
    callback() {  const fn = compose(this.middleware);    const handleRequest = (req, res) => {    // 当中间件函数全部执行完毕时,会触发then方法,从而执行respond方法返回响应    const handleResponse = () => respond(req, res);    fn(req, res).then(handleResponse);  }    return handleRequest;}复制代码
  • 修改入口文件 index.js 代码
    // 引入自定义模块const MyKoa = require('./js/my-application');// 创建实例对象const app = new MyKoa();// 使用中间件app.use((req, res, next) => {  console.log('中间件函数执行了~~~111');  next();})app.use((req, res, next) => {  console.log('中间件函数执行了~~~222');  // 设置响应内容,由框架负责返回响应~  res.body = 'hello myKoa';})// 监听端口号app.listen(3000, err => {  if (!err) console.log('服务器启动成功了');  else console.log(err);})复制代码
  • 此时我们就能根据不同响应内容做出处理了~当然还是比较简单的,可以接着去扩展~

06、定义 Request 模块

// 此模块需要npm下载const parse = require('parseurl');const qs = require('querystring');module.exports = {  /**   * 获取请求头信息   */  get headers() {    return this.req.headers;  },  /**   * 设置请求头信息   */  set headers(val) {    this.req.headers = val;  },  /**   * 获取查询字符串   */  get query() {    // 解析查询字符串参数 --> key1=value1&key2=value2    const querystring = parse(this.req).query;    // 将其解析为对象返回 --> {key1: value1, key2: value2}    return qs.parse(querystring);  }}复制代码

07、定义 Response 模块

module.exports = {  /**   * 设置响应头的信息   */  set(key, value) {    this.res.setHeader(key, value);  },  /**   * 获取响应状态码   */  get status() {    return this.res.statusCode;  },  /**   * 设置响应状态码   */  set status(code) {    this.res.statusCode = code;  },  /**   * 获取响应体信息   */  get body() {    return this._body;  },  /**   * 设置响应体信息   */  set body(val) {    // 设置响应体内容    this._body = val;    // 设置响应状态码    this.status = 200;    // json    if (typeof val === 'object') {      this.set('Content-Type', 'application/json');    }  },}复制代码

08、定义 Context 模块

// 此模块需要npm下载const delegate = require('delegates');const proto = module.exports = {};// 将response对象上的属性/方法克隆到proto上delegate(proto, 'response')  .method('set')    // 克隆普通方法  .access('status') // 克隆带有get和set描述符的方法  .access('body')  // 将request对象上的属性/方法克隆到proto上delegate(proto, 'request')  .access('query')  .getter('headers')  // 克隆带有get描述符的方法复制代码

09、揭秘 delegates 模块

module.exports = Delegator;/** * 初始化一个 delegator. */function Delegator(proto, target) {  // this必须指向Delegator的实例对象  if (!(this instanceof Delegator)) return new Delegator(proto, target);  // 需要克隆的对象  this.proto = proto;  // 被克隆的目标对象  this.target = target;  // 所有普通方法的数组  this.methods = [];  // 所有带有get描述符的方法数组  this.getters = [];  // 所有带有set描述符的方法数组  this.setters = [];}/** * 克隆普通方法 */Delegator.prototype.method = function(name){  // 需要克隆的对象  var proto = this.proto;  // 被克隆的目标对象  var target = this.target;  // 方法添加到method数组中  this.methods.push(name);  // 给proto添加克隆的属性  proto[name] = function(){    /*      this指向proto, 也就是ctx        举个栗子:ctx.response.set.apply(ctx.response, arguments)        arguments对应实参列表,刚好与apply方法传参一致        执行ctx.set('key', 'value') 实际上相当于执行 response.set('key', 'value')    */    return this[target][name].apply(this[target], arguments);  };  // 方便链式调用  return this;};/** * 克隆带有get和set描述符的方法. */Delegator.prototype.access = function(name){  return this.getter(name).setter(name);};/** * 克隆带有get描述符的方法. */Delegator.prototype.getter = function(name){  var proto = this.proto;  var target = this.target;  this.getters.push(name);  // 方法可以为一个已经存在的对象设置get描述符属性  proto.__defineGetter__(name, function(){    return this[target][name];  });  return this;};/** * 克隆带有set描述符的方法. */Delegator.prototype.setter = function(name){  var proto = this.proto;  var target = this.target;  this.setters.push(name);  // 方法可以为一个已经存在的对象设置set描述符属性  proto.__defineSetter__(name, function(val){    return this[target][name] = val;  });  return this;};复制代码

10、使用 ctx 取代 req 和 res

  • 修改 my-application
    const {createServer} = require('http');const context = require('./my-context');const request = require('./my-request');const response = require('./my-response');module.exports = class Application {  constructor() {    this.middleware = [];    // Object.create(target) 以target对象为原型, 创建新对象, 新对象原型有target对象的属性和方法    this.context = Object.create(context);    this.request = Object.create(request);    this.response = Object.create(response);  }    use(fn) {    this.middleware.push(fn);  }      listen(...args) {    // 使用nodejs的http模块监听端口号    const server = createServer(this.callback());    server.listen(...args);  }    callback() {    const fn = compose(this.middleware);        const handleRequest = (req, res) => {      // 创建context      const ctx = this.createContext(req, res);      const handleResponse = () => respond(ctx);      fn(ctx).then(handleResponse);    }        return handleRequest;  }    /**   * 创建context 上下文对象的方法   * @param req node原生req对象   * @param res node原生res对象   */  createContext(req, res) {    /*      凡是req/res,就是node原生对象      凡是request/response,就是自定义对象      这是实现互相挂载引用,从而在任意对象上都能获取其他对象的方法     */    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;    request.ctx = response.ctx = context;    request.response = response;    response.request = request;        return context;  }}// 将原来使用req,res的地方改用ctxfunction compose(middleware) {  return (ctx) => {    return dispatch(0);    function dispatch(i) {      let fn = middleware[i];      if (!fn) return Promise.resolve();      return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)));    }  }}function respond(ctx) {  let body = ctx.body;  const res = ctx.res;  if (typeof body === 'object') {    body = JSON.stringify(body);    res.end(body);  } else {    res.end(body);  }}复制代码
  • 修改入口文件 index.js 代码
    // 引入自定义模块const MyKoa = require('./js/my-application');// 创建实例对象const app = new MyKoa();// 使用中间件app.use((ctx, next) => {  console.log('中间件函数执行了~~~111');  next();})app.use((ctx, next) => {  console.log('中间件函数执行了~~~222');  // 获取请求头参数  console.log(ctx.headers);  // 获取查询字符串参数  console.log(ctx.query);  // 设置响应头信息  ctx.set('content-type', 'text/html;charset=utf-8');  // 设置响应内容,由框架负责返回响应~  ctx.body = '

    hello myKoa

    ';})// 监听端口号app.listen(3000, err => { if (!err) console.log('服务器启动成功了'); else console.log(err);})复制代码

到这里已经写完了 Koa 主要代码,有一句古话 - 看万遍代码不如写上一遍。 还等什么,赶紧写上一遍吧~ 当你能够写出来,再去阅读源码,你会发现源码如此简单~

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

你可能感兴趣的文章
[Android]让RemoteControlClient显示Music Album
查看>>
01-UI基础-03UIImageView
查看>>
JSP页面ajax提交登录数据demo
查看>>
软件自动化测试框架STAF
查看>>
Hadoop 多表连接
查看>>
使用maven下载jar包,使用ant打包。yqxt项目的安装。
查看>>
Ubuntu 一键安装openresty
查看>>
dlmalloc
查看>>
学习与准备的一些资源
查看>>
MySQL索引背后的数据结构及算法原理
查看>>
Eclipse SVN的更新地址是
查看>>
Intel XDK 跨平台 App 开发初体验
查看>>
Windows 下msvc2010编译 NSIS 2.46
查看>>
第三方授权登录(微博篇)
查看>>
苹果App Store审核指南中文翻译(2014.9.1更新)
查看>>
如何复制一个LIST
查看>>
说说我为什么看好Spring Cloud Alibaba
查看>>
RecyclerView 差异更新(diff)
查看>>
Android之ActionBar学习
查看>>
对于法线贴图的深入研究
查看>>