当前位置:Gxlcms > JavaScript > 如何从零开始手写Koa2框架

如何从零开始手写Koa2框架

时间:2021-07-01 10:21:17 帮助过:1人阅读

01、介绍

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

02、源码目录介绍

Koa 源码目录截图

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

  • application: Koa 最主要的模块, 对应 app 应用对象
  • context: 对应 ctx 对象
  • request: 对应 Koa 中请求对象
  • response: 对应 Koa 中响应对象

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

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

my-application

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

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. const handleRequest = (req, res) => {
  5. // 调用该函数,返回值为promise对象
  6. // then方法触发了, 说明所有中间件函数都被调用完成
  7. fn(req, res).then(() => {
  8. // 在这里就是所有处理的函数的最后阶段,可以允许返回响应了~
  9. });
  10. }
  11. return handleRequest;
  12. }

修改入口文件 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. if (typeof body === 'object') {
  5. // 如果是对象,转化成json数据返回
  6. body = JSON.stringify(body);
  7. res.end(body);
  8. } else {
  9. // 默认其他数据直接返回
  10. res.end(body);
  11. }
  12. }

callback中调用

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

修改入口文件 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. module.exports = {
  5. /**
  6. * 获取请求头信息
  7. */
  8. get headers() {
  9. return this.req.headers;
  10. },
  11. /**
  12. * 设置请求头信息
  13. */
  14. set headers(val) {
  15. this.req.headers = val;
  16. },
  17. /**
  18. * 获取查询字符串
  19. */
  20. get query() {
  21. // 解析查询字符串参数 --> key1=value1&key2=value2
  22. const querystring = parse(this.req).query;
  23. // 将其解析为对象返回 --> {key1: value1, key2: value2}
  24. return qs.parse(querystring);
  25. }
  26. }

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. const proto = module.exports = {};
  4. // 将response对象上的属性/方法克隆到proto上
  5. delegate(proto, 'response')
  6. .method('set') // 克隆普通方法
  7. .access('status') // 克隆带有get和set描述符的方法
  8. .access('body')
  9. // 将request对象上的属性/方法克隆到proto上
  10. delegate(proto, 'request')
  11. .access('query')
  12. .getter('headers') // 克隆带有get描述符的方法

09、揭秘 delegates 模块

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

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

修改入口文件 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 主要代码,有一句古话 - 看万遍代码不如写上一遍。 还等什么,赶紧写上一遍吧~
当你能够写出来,再去阅读源码,你会发现源码如此简单~

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

人气教程排行