时间:2021-07-01 10:21:17 帮助过:1人阅读
01、介绍
本文 Koa 版本为 2.7.0, 版本不一样源码可能会有变动
02、源码目录介绍
Koa 源码目录截图
通过源码目录可以知道,Koa主要分为4个部分,分别是:
这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
方法
- // 负责执行中间件函数的函数
- 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 上下文对象的方法
- 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的地方改用ctx
- function 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 = '<h1>hello myKoa</h1>';
- })
- // 监听端口号
- app.listen(3000, err => {
- if (!err) console.log('服务器启动成功了');
- else console.log(err);
- })
到这里已经写完了 Koa 主要代码,有一句古话 - 看万遍代码不如写上一遍。 还等什么,赶紧写上一遍吧~
当你能够写出来,再去阅读源码,你会发现源码如此简单~
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。