前言
Koa和Express 都是基于 Node.js 平台的下一代 web 开发框架,由 Express 幕后的原班人马打造,
express和Koa的两者区别简述:
- Express 源码是 es5 写的,koa 源码基于 es6 写的
- Express 比较全 内置了很多功能;koa 内部核心非常小巧(我们可以通过拓展的插件进行扩展)
- Express 和 Koa 都是可以自己去使用实现 mvc 功能的,没有约束
- Express 处理异步的方式是回调函数,函数处理异步的方式是 async+await
Koa的启动
初始化项目
npm init -y
Koa 的安装
npm install koa
Koa 的use 方法
- use 是一个中间件,每次执行(请求)会产生一个上下文 ctx
- 可以通过ctx.body 来实现node 中res.en()的效果
- 上下文 ctx包含了主要部分
- app 当前应用实例,
- req,res对应 原生 node 中的 req,res
- koa 自己封装的 request 和 response 是对原生的 req 和 res 进行的一层抽象,和扩展,功能更多
const Koa = require("koa");
const app = new Koa();
app.use((ctx) =>
ctx.body = "hello1111"; //ctx.body 相当于node 里sever中 res.en()的功能 给浏览器写入数据
console.log(ctx);//打印ctx,查看属性
});
app.listen(3000, function () {
console.log("server start 3000");
}); //监听端口号 ,同我们的的node中http的listen方法
ctx上下的打印结果:
{
request: {
method: 'GET',
url: '/',
header: {
host: 'localhost:3000',
connection: 'keep-alive',
'cache-control': 'max-age=0',
'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"',
'sec-ch-ua-mobile': '?0',
'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36',
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,_/_;q=0.8,application/signed-exchange;v=b3;q=0.9',
'sec-fetch-site': 'none',
'sec-fetch-mode': 'navigate',
'sec-fetch-user': '?1',
'sec-fetch-dest': 'document',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'zh-CN,zh;q=0.9',
cookie: '\_uab_collina=161916550781026855009784; zoe=27'
}
},
response: {
status: 404,
message: 'Not Found',
header: [Object: null prototype] {}
},
app: { subdomainOffset: 2, proxy: false, env: 'development' },
originalUrl: '/',
req: '<original node req>',
res: '<original node res>',
socket: '<original node socket>'
}
话不多说 从上下文入手开始Koa的实现
Koa上下文的实现
目录创建
参考下 koa 的源码 lib 下的文件结构 依次新建对应文
package.json
设置入口文件
{
"main":"lib/application.js"
}
context.js,request.js,response.js 依次创建
const response = {};
module.exports = response;
application.js
const http = require("http");
const context = require("./context");
const request = require("./request");
const response = require("./response");
class Application {
constructor() {
}
}
module.exports = Application;
koa启动server时listen实现
思路:
- 接收两个参数 端口号和callback
- 内部封装node的 listen 并将实例上listen参数传入
实现:
//handleRequest待实现
listen(...args) {
const server = http.createServer(this.handleRequest);
server.listen(...args);
}
- 调用:同Koa框架相同
app.listen(3000, function () {
console.log("server start 3000");
});
use实现
use的作用
每次执行(请求)会产生一个上下文 ctx
思路:
- use用于保存用户写入的函数
- 服务启动时候 触发用户保存的use,创建上下文ctx
- 每一个实例每一个请求对应的use里的ctx都要相互独立
- ctx 里要包含 node 原生的req和res 还有ctx自己扩展的request和response
实现:
- application.js
constructor() {
// 初始化 创建实每一个实例自己的上下文,原理是用了原型继承
this.context = Object.create(context);
//同理 初始化request和response
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn) {
// 保存用户写的函数
this.fn = fn;
}
// 上下文的实现
createContext(req, res) {
let ctx = Object.create(this.context);
// 注意这里 为什么不用 ctx=context 怕上下文相互影响
// Object.create 做中间层包装(隔离) ,可以保证每一个请求产生的上下文是独立空间是自己的上下文,但又可以获取同一个公共上下文
// 同理 创建每次请求自己的request 和response
let request = Object.create(this.request);
let response = Object.create(this.response);
// ctx自己扩展的请求和响应
ctx.request = request;
ctx.response = response;
// 原生请求
ctx.req = ctx.request.req = req; //默认上下文包含原生的req,且自己的requst需要可以取到原生的req
ctx.res = res; //默认上下文包含原生的res
return ctx;
}
handleRequest = (req, res) => {
//每次请求都会执行次方法,然后创建自己的上下文
let ctx = this.createContext(req, res);
// 服务启动时候 触发用户保存的use
this.fn(ctx);
};
- request.js 用于实现上下文ctx的request
const url = require("url");
const request = {
get url() {
//application里 request 是被上下文ctx调用的 所以此处this指向上下文 通过上下文去取到node 原生的re.url就ok
return this.req.url;
},
//同理实现 通过request 获取请求path和请求query
get path() {
return url.parse(this.req.url).pathname;
},
get query() {
return url.parse(this.req.url).query;
},
};
module.exports = request;
测试
调用
- 1.server.js
const Koa = require("./koa");
// 使用koa就是创造一个应用实例
const app = new Koa();
app.use((ctx) => {
console.log(ctx.req.url)
console.log(ctx.request.req.url)
console.log(ctx.request.url)//app里request 被Object.create 包了两次 寻找url 相当于:ctx.request.__proto__.__proto__
console.log(ctx.url)
console.log('--------本次console end')
});
app.listen(3000, function () {
console.log("server start 3000");
}); //监听端口号 ,同我们的的node中http的listen方法
测试结果
console.log(ctx.url) 没有打印成功
ctx.url实现
- context.js
- 同request.js的实现思想相同 我们这里的this指向的是最终调用的那个对象 也就是我们的上下文ctx
- url 就去取我们的this上的request的url 属性
const context = {
get url() {
return this.request["url"];
},
};
module.exports = context;
- console.log(ctx.url)的打印结果
-
但是目前context的实现 如果 要实现其他path或者query的获取 就要复制粘帖,这。。不能忍。于是参考了下 koa的源码里 context 获取的实现
1.
核心: defineGetter 不过 mdn已经提示被废弃,将来可能停止支持。但 主要 研究 koa 的实现 此处不对该接口深入探讨
- 参考源码后做调整
const context = {
}
function defienGetter(target,key) {
context.__defineGetter__(key, function () {
// tips :这里的function 不能是 箭头函数 ,否则 this 就变成指向 context了,就变成了一个空对象
// this 应该指向调用它的对象,也就是实例中每次请求方法中自己对应的那个ctx
return this[target][key];
});
}
defienGetter("request", "url");
defienGetter("request", "path");
module.exports = context;
- 测试url,path的console结果
Koa响应体的实现
原生koa里 ctx.body的使用
- server.js
app.use((ctx) => {
ctx.body = "hello ZOE";
//ctx.response.body ="hello ZOE"
});
发现 ctx.body和ctx.response.body都能在页面中写入
ctx.body和ctx.response.body
两者是什么关系??
- 测试:use里换成以下代码 &查看控制台
ctx.response.body = " ctx.response.body 实现的 hello Zoe";
console.log("ctx.body :", ctx.body, ctx.response.body === ctx.body);
-
测试结果: ctx.body和ctx.response.body指向同一个东西
-
小结:ctx.body值的set应该就是代理了ctx.response.body的set
koa里 ctx.body的实现
response的处理
ctx.response指向原生的res
回到自定义的application.js文件 createContext函数里新增response的指向原生的res
// 响应体的2.
ctx.response = response;
//默认的上下文包含原生的res,那么可以在我们的response对象中,通过this.res拿到原生的res,和
ctx.res = ctx.response.res = res;
ctx.response.body的set和get
response.js: set 和get中的value 还要指向同一个值 ,response对象另外设置一个_body变量 ,set 和get都对_body做操作
const response = {
_body: undefined,
get body() {
return this._body;
},
set body(value) {
this._body = value;
},
};
module.exports = response;
server.js:
app.use((ctx) => {
ctx.response.body = " Zoe 自定义的koa 的响应体测试";
console.log(ctx.response.body);
});
测试结果:
但是 ctx.body 不等同res.use,所以页面上并没有展示我们body 写入的内容
ctx.response.body实现写入页面
application.js
- handleRequest 方法里新增 res.end(ctx.response.body);
server.js:
app.use((ctx) => {
ctx.response.body = " hello Zoe";
});
- 页面输出结果
ctx.body的set 和get实现
- get
-
回顾 request的实现 ctx.req是做了ctx.request.req的代理;同理 ctx.body 也同样可以代理ctx.response.body
-
加一个defienGetter("response", "body");取 ctx.body 的时候 就去取当前this指向的那个ctx 下的response.body
-
- set
- 要实现的是 在ctx.body 赋值的时候 也做一层代理proxy 实际是做了ctx.response.body的set
context.js
原来的基础上新增代码
const context = {
function defienGetter(target, key) {
context.__defineGetter__(key, function () {
return this[target][key];
});
}
function defineSetter(target, key) {//proxy ,defineProperty
context.__defineSetter__(key, function (value) {
return (this[target][key] = value);
});
}
defienGetter("response", "body");
defineSetter("response", "body");
module.exports = context;
- 测试 sever.js
app.use((ctx) => {
ctx.body = " hello Zoe";
console.log(`${ctx.body} 来自ctx.body的测试`);
});
- 测试结果
ctx.body边界处理
- ctx.body为空的时候的处理 404处理;否则 再调用 res.end 写入body
404
- application.js
handleRequest = (req, res) => {
let ctx = this.createContext(req, res);
res.statusCode = 404;//这里做默认处理404 有值则调用set body ,set body 同时改变statusCode 为200
this.fn(ctx);
if (ctx.body) {
res.end(ctx.body);
} else {
res.end("Not Found");
}
};
- response.js
set body(value) {
//如果用户调用了 ctx.body='cxx' 就设置响应状态码200
this.res.statusCode = 200;
this._body = value;
},
- app.use 不做ctx.body的赋值测试结果
ctx.body支持多类型数据
- 新增ctx.body type判断
- 字符串 或者buffer 沿用原有的逻辑
- 数字转成字符串
- 对象的处理 JSON.stringify()
application.js
handleRequest = (req, res) => {
let ctx = this.createContext(req, res);
res.statusCode = 404;
this.fn(ctx);
let _body = ctx.body;
if (_body) {
if (isString(_body) || Buffer.isBuffer(_body)) {
return res.end(_body);
} else if (isNumber(_body)) {
return res.end(_body + "");
} else if (isObject(_body)) {
return res.end(JSON.stringify(_body));
}
} else {
res.end("Not Found");
}
数据类型 type判断 (用的是 自己的写的一个工具函数 实现 可以参考)
type
测试 : ctx.response.body = { name: "ZOE" };
结果: 我想偷个懒 没有图 (有兴趣。可以跟着我的思路写一遍)
多个 use 的支持
目前为止:use 函数 只支持调用一次 现在要变成数组来保存用户写的函数
middleWares缓存数组
初始化
constructor() {
this.middleWares = [];
}
use改写
use(middleWare) {
// this.fn = fn;
this.middleWares.push(middleWare);
}
handleRequest调整
需要对 middleWares 进行依次取值进行 body 写入而不是直接写入 body
- this.compose 处理函数 将 middleWares 数组里的函数一个个执行
- 回调处理每一个函数自己的 ctx.body写入
实现思路
- this.compose 返回一个大的 promise
- 这个 promise 里 将每一个 use 的处理函数 包装成小的内部 promise
- 内部小的 promise的 then 里处理自己的 body 写入
- 内部promise执行顺序上一个 promise 会等待返回的 promise 执行完(递归)
实现代码
handleRequest
this.compose(ctx)
.then(() => {
//包裹原有_body 写入的逻辑代码
}
compose实现
-
边界处理: use 一个都没有,this.middleWares.length===0 直接返回返回一个 Promise.resolve
-
Promise.resolve (处理当前 use 保存的函数,cb 回调 下一个 use 保存的函数)
预期效果
this.middleWares[0(ctx,this.middleWares[1])依次关联下一个use,执行完第一个执行第二个cb -
相同use 里连续调用多个 next 的error处理
- i 缓存每一个执行函数的 index,当下一个 dispacth 执行时 如何 index===i 说明是相同 use 里的两个 next ,reject 抛异常
compose(ctx) {
// 组合是要将数组里的函数一个个执行
let index = 0;
let i = -1;
const dispatch = () => {
if (index === i) return Promise.reject("multiple call next()");
// 如果use 中间件 一个都没有的边界处理
if (this.middleWares.length === index) return Promise.resolve();
i = index;
return Promise.resolve(this.middleWares[index++](ctx, dispatch));
};
return dispatch(0);
}
测试结果:写了个寂寞,并不抛异常,因为上述实现方式里i 放于dispatch外层 ,执行到第二个next的时候 i已经是 之前next 修改了的结果,index === i条件永远满足不了
修改方向: 重新看原生 Koa 的实现 位置 /node_modules/koa-compose/index.js
- i 唯一来源于dispatch入参
调整结果:
compose(ctx) {
// 组合是要将数组里的函数一个个执行
let index = -1
const dispatch = (i) => {
// 如果use 中间件 一个都没有的边界处理 或者一个use里多个next
if (i <= index)
return Promise.reject(new Error("next() called multiple times"));
index = i;
if (this.middleWares.length === i) return Promise.resolve();
// () => dispatch(i + 1))就是use里的next
// 一个promise 会等待返回的promise执行完
return Promise.resolve(this.middleWares[i](ctx, () => dispatch(i + 1)));
};
return dispatch(0);
}
测试:
app.use(async (ctx, next) => {
console.log(1);
await next();
await next();
console.log(2);
});
app.use(async (ctx, next) => {
console.log(3);
await new Promise((resolve, reject) => {
setTimeout(() => {
console.log("zoe");
resolve();
}, 1000);
});
next();
console.log(4);
});
app.use((ctx, next) => {
console.log(5);
next();
console.log(6);
});
app.listen(3000, () => {
console.log("server start 3000");
});
测试结果:
额外的解释:为什么 dispatch 要return Promise.resolve?
因为 use里 如果写入的next没有awiat this.compose().then无法正常进行
err 的优雅处理
Koa的错误处理
支持 自定义错误信息处理
app.on('error',function(err){
console.log({err})
})
实现
- Application 继承EventEmitter (要记得 super)
- this.compose 执行的时候 catch 捕获执行错误, 然后 this.emit("error", err);用户订阅的 error 事件
const EventEmitter = require("events");
class Application extends EventEmitter {
constructor() {
super();//相当于 EventEmitter.call(this)
}
handleRequest(){
this.compose(ctx)
.then(() => {
//ctx.body的设置代码,此处不重复阐述
})
.catch((err) => {
this.emit("error", err);
});
}
}
测试结果:
小结
先写到这吧 待续....
最后如果觉得本文有帮助 记得点赞三连哦 十分感谢