我们发现 http 基于回调创建服务的方式很不优雅,而在 node 中,有一个第三方模块 koa,它就是基于 http 模块进行了一次封装,和 express 不同的是,它底层全部基于 promise,所以需要我们对 promise 有非常深入的了解。
为什么会有 koa
- 原生 req 和 res 功能非常弱,koa 增强了 req 和 res,自己生成了两个对象 request 和 response,并代理到 ctx 上
- 有自己的中间件机制,可以通过 next 控制中间件的执行顺序
koa 安装 和使用
npm i koa
// server.js
const Koa = require('koa');
// 通过 new 方式创建一个应用
const app = new Koa(); // 相当于 原生的 const server = createServer(cb);
app.use(ctx => {
})
app.listen(3000, function() {
console.log(`serve is running on port 3000`);
});
不过我们没有返回任何东西,在浏览器上访问会返回 Not Found
app.use(ctx => {
ctx.body = 'hello world'
})
这时候,就能真正的输出结果啦,如果发生错误,还能进行错误捕获
app.use(ctx => {
ctx.body = `hello world: ${ a }`;
})
app.on('error', function(err) {
console.log('err', err);
});
此刻访问服务,页面展示 Internal Server Error,并且 node 命令行打印错误
err ReferenceError: a is not defined
到这里,我们已经了解 koa 70% 的功能了,是不是特别简单呢
手动实现 koa
先来仿 node_modules 中的 koa 目录结构
- myKoa
- lib
- application.js
- context.js
- request.js
- response.js
- package.json
- server.js
step1: ctx 代理 & 返回数据
这一步主要处理服务启动,ctx 数据代理,数据返回等逻辑。
修改 server.js
- 引用我们自己实现的 myKoa 包
- 各种测试打印代码,可以看到我们要处理 ctx & ctx.request 上挂载原生 req, ctx & ctx.response 上挂载 res 等一系列挂载方式,而且支持 ctx.xxx 取自 ctx.request.xxx 的功能。
server.js(包含各种测试代码)
// const Koa = require('koa');
const Koa = require('./myKoa');
// 通过 new 方式创建一个应用
const app = new Koa(); // 相当于 原生的 const server = createServer(cb);
app.use(ctx => {
// // 原生 req 上取 url
// console.log(ctx.req.url); // 好长呀
// console.log(ctx.request.req.url); // 更长了
// // koa 实现的
// console.log(ctx.request.url, 'ctx.request.url');
// console.log(ctx.url); // 我们一般用这种方式,通过代理哦
// console.log(ctx.path, 'ctx.path');
// ctx.body = 'hello';
// ctx.response.body += ' world'
ctx.body = { a: 1 };
console.log(ctx.body);
})
app.listen(3000, function() {
console.log(`serve is running on port 3000`);
});
app.on('error', function(err) {
console.log('err', err);
});
增加 myKoa/package.json
- 声明包入口文件路径
myKoa/package.json
{
"main": "./lib/application"
}
增加 myKoa/application.js 主入口文件
- 构建 Koa 类
- 封装回调方法,包含构建 ctx,执行中间件,返回数据给客户端逻辑。
- 根据 listen 参数启动服务,传入 handleRequest 回调方法
myKoa/application.js
const EventEmitter = require('events'); // 有 on('error') 肯定用了 events 模块
const http = require('http');
const request = require('./request');
const response = require('./response');
const context = require('./context');
class Koa extends EventEmitter {
constructor() {
super();
this.context = Object.create(context); // 防止多个应用之间公用一个 context
this.request = Object.create(request); // 防止多个应用之间公用一个 request
this.response = Object.create(response); // 防止多个应用之间公用一个 response
}
use(fn) {
this.fn = fn; // 保存中间件函数
}
// 构建 ctx
createContext(req, res) {
let ctx = Object.create(this.context); // 防止多个请求共享 ctx
let request = Object.create(this.request); // 防止多个请求共享 request
let response = Object.create(this.response); // 防止多个请求共享 response
ctx.request = request;
ctx.request.req = ctx.req = req;
ctx.response = response;
ctx.response.res = ctx.res = res;
return ctx;
}
handleRequest = (req , res) => {
const ctx = this.createContext(req, res);
res.statusCode = 404;
this.fn(ctx); // 中间件执行
// 如果用户设置了 ctx.body 就传递给客户端
if (typeof ctx.body == 'object') {
res.setHeader('Content-type', 'application/json;chartset=utf-8');
// 对象转字符串
res.end(JSON.stringify(ctx.body));
} else if (ctx.body) {
res.end(ctx.body);
} else {
res.end('Not Fount');
}
}
listen(...args) {
// 就是 node 中的原生 http 模块
const server = http.createServer(this.handleRequest);
server.listen(...args);
}
}
module.exports = Koa;
增加 myKoa/context.js 代理 ctx 内变量访问和赋值
- 代理 ctx.属性名 --> ctx.requst.属性名
- 代理 ctx.body = xxx --> ctx.response.body = xxx
myKoa/context.js
// 代理 比如 ctx.query
const context = {
}
// 代理 ctx.xxx -> ctx.request.xxx
function defineGetter(target, key) {
// 给 context 某属性 增加 get 拦截器
context.__defineGetter__(key, function() {
return this[target][key];
});
}
function defineSetter(target, key) {
context.__defineSetter__(key, function(val) {
this[target][key] = val;
});
}
// 代理取值和赋值
// ctx.query -> ctx.request.query
defineGetter('request', 'query');
defineGetter('request', 'url');
defineGetter('request', 'path');
defineGetter('response', 'body');
defineSetter('response', 'body');
module.exports = context;
增加 myKoa/request.js 代理 req 内需要 url 模块的变量
- 代理 ctx.path --> url.parse(this.url).pathname
- 代理 ctx.query --> url.parse(this.url).query
myKoa/request.js
const url = require('url');
// ctx.path -> url.parse(this.url).pathname
const request = {
get url() {
// 使用 ctx.request.url 时候,执行 ctx.request.url()
// ctx.request 就是 this
return this.req.url;
},
get path() {
return url.parse(this.url).pathname;
},
get query() {
return url.parse(this.url).query;
}
}
module.exports = request;
增加 myKoa/response.js 代理 res.body
- ctx.response.body --> ctx.body = xxx
myKoa/response.js
// ctx.response.body = 'world'
const response = {
_body: undefined,
get body() {
return this._body;
},
set body(val) {
this.res.statusCode = 200;
this._body = val;
}
}
module.exports = response;
至此,我们其实已经完成了 70% 的源码,那还剩下 30% 在哪里呢,那就是中间件机制啦~
step2: 实现中间件机制
这一步我们主要来实现 koa 中间件的特性.
koa 中间件执行顺序
在实现之前,我们先来看下 koa 的中间件是怎么执行的。
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log(1);
next();
console.log(2);
})
app.use(async (ctx, next) => {
console.log(3);
next();
console.log(4);
})
app.use(async (ctx, next) => {
console.log(5);
next();
console.log(6);
})
app.listen(3000, function() {
console.log(`serve is running on port 3000`);
});
app.on('error', function(err) {
console.log('err', err);
});
中间件中遇到 next 就会跳到下个中间件,等下个中间件执行完,才会继续执行剩余部分。
所以输出结果为:1 3 5 6 4 2
koa 会将多个中间件进行组合处理,内部会将这三个函数全部包装成 promise(哪怕没用 async),并且将这三个 promise 串联起来,第一层等待其他层执行完毕再继续执行。
加了 sleep 的中间件执行顺序
const Koa = require('koa');
const app = new Koa();
const sleep = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('sleep')
resolve();
}, time);
})
}
app.use(async (ctx, next) => {
console.log(1);
next();
console.log(2);
})
app.use(async (ctx, next) => {
console.log(3);
sleep(1000); // 睡 1 秒
next();
console.log(4);
})
app.use(async (ctx, next) => {
console.log(5);
next();
console.log(6);
})
app.listen(3000, function() {
console.log(`serve is running on port 3000`);
});
app.on('error', function(err) {
console.log('err', err);
});
- 第一个中间件执行,输出 1,遇到 next,把 next 后面的代码包成一个 promise,这里我们称之为 promsie1,promise1 等待第二个中间件执行结果。
- 第二个中间件执行,输出 3,遇到 sleep,内部 setTimeout 是个宏任务,在 webapi 线程计时(1s 后入宏任务队列),回到第二个中间件,执行 next,将后面的代码包成一个 promise,我们称之为 promise2,promise2 等待第三个中间件的返回结果,跳到第三个中间件
- 第三个中间件执行,输出 5,遇到 next,把后面代码包成一个 promise,我们称之为 promise3,promise3 等待下一个中间件返回结果,发现没有其他中间件了,此时 promise3 入微任务队列,微任务队列为 [promise3]
- 同步代码执行完,去清空微任务队列,promise3 执行,输出 6,同时 promise2 入队列,微任务队列为 [promise2]
- promise2 执行,输出4, promise1 入微任务队列,微任务队列为 [promise1]
- promsie1 执行,输出 2,微任务队列情况
- 开始执行宏任务,等待定时器到时间,回调函数作为一个宏任务入队列执行,输出 sleep
所以得到输出结果为:1 3 5 6 4 2 sleep,sleep 函数并没有影响中间件的执行顺序。
加了 await sleep 的中间件执行顺序
const Koa = require('koa');
const app = new Koa();
const sleep = (time) => {
// 注意这里虽然有 promsie,但并未执行 .then,这里没有微任务
// 注意,return new Promsie,只有等 new Promise 执行完毕后,sleep 才有返回值
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('sleep')
resolve();
}, time);
})
}
app.use(async (ctx, next) => {
ctx.body = 'one';
console.log(1);
next();
ctx.body = 'two';
console.log(2);
})
app.use(async (ctx, next) => {
ctx.body = 'three';
console.log(3);
await sleep(1000);
next();
ctx.body = 'four';
console.log(4);
})
app.use(async (ctx, next) => {
ctx.body = 'five';
console.log(5);
next();
ctx.body = 'six';
console.log(6);
})
app.listen(3000, function() {
console.log(`serve is running on port 3000`);
});
app.on('error', function(err) {
console.log('err', err);
});
- 第一个中间件执行,输出 1,遇到 next,把 next 后面的代码包成一个 promise,这里我们称之为 promsie1,promise1 等待第二个中间件执行结果。
- 第二个中间件执行,输出 3,遇到 await sleep,会把之后的代码包装成一个 promise,我们这里称为 afterSleepPromise,然后第二个中间件同步代码执行完毕,返回 undefined,promsie1 入微任务队列,至此 同步代码执行完毕,微任务队列为 [promise1]
- 扫描微任务队列,promise1 执行,输出 2,页面渲染为 two(首个中间件执行完毕,页面立马渲染),微任务队列清空完毕。
- 等定时器到时间,setTimeout 回调进入宏任务队列,宏任务执行,输出 sleep,sleep 函数接收到成功态的返回值,afterSleepPromise 入微任务队列,宏任务队列清空完毕,微任务队列为 [afterSleepPromise]
- 扫描微任务队列,afterSleepPromise 执行,把第二个中间件 next 后面代码包成 promise2,promise2 等待第三个中间件的返回结果,跳到第三个中间件
- 第三个中间件执行,输出 5,遇到 next,把后面代码包成一个 promise,我们称之为 promise3,promise3 等待下一个中间件返回结果,发现没有其他中间件了,此时 promise3 入微任务队列,微任务队列为 [promise3]
- 同步代码执行完,去清空微任务队列,promise3 执行,输出 6,同时 promise2 入队列,微任务队列为 [promise2]
- promise2 执行,输出4,代码执行完毕。
所以得到输出结果为:1 3 2 (sleep 1s) 5 6 4,sleep 函数影响了中间件的执行顺序,而且在输出 2 时,已经给浏览器返回了 ctx.body,此后再改 ctx.body 已经没有用了。
koa 中间件的使用原则
但是这肯定不是我们希望得到的结果,你总不能让我的中间件内不使用 await 吧,我们肯定希望每个中间件都要被等待执行完成后,才返回数据,所以每个中间件的 next 前需要加个 await 或者 return (一个 promise 返回另一个 promise 也会有等待效果)。
app.use(async (ctx, next) => {
ctx.body = 'one';
console.log(1);
await next();
ctx.body = 'two';
console.log(2);
})
app.use(async (ctx, next) => {
ctx.body = 'three';
console.log(3);
await sleep(1000);
await next();
ctx.body = 'four';
console.log(4);
})
app.use(async (ctx, next) => {
ctx.body = 'five';
console.log(5);
await next();
ctx.body = 'six';
console.log(6);
})
app.on('error', function(err) {
console.log('err', err);
});
这样的话,中间件 1 就要等待下一个中间件执行完成,而中间件 2 需要等待 sleep 和 下一个 next,这样就形成了一种 koa 中 next 方法的使用原则,所有的 next 方法调用,前面必须加 await。
输出 1 3 sleep 5 6 4 2
完善 koa,增加中间件
我们需要收集所有的 middleware,然后把他们包成 promise,并用 next 方法控制执行即可。
修改 myKoa/application.js
- 收集中间件,维护中间件列表
- 创建 compose 方法,该方法会把中间件函数包装成一个 promise,并用 next 方法来控制依次执行,整体串成一个 promise 链,中间件 1 等待其他中间件 promise 返回结果。
- 同一个中间件多次调用 next,直接返回一个失败的 promise
- 中间件 1 执行完毕后,直接 return 一个成功态的 promise 对象,修改 handleRequest,在成功回调中执行 ctx.body 取值,并返回给页面的操作,失败回调把错误 emit,留给全局的 app.on('err', cn) 监听处理。
myKoa/application.js
const EventEmitter = require('events'); // 有 on('error') 肯定用了 events 模块
const http = require('http');
const request = require('./request');
const response = require('./response');
const context = require('./context');
class Koa extends EventEmitter {
constructor() {
super();
this.context = Object.create(context); // 防止多个应用之间公用一个 context
this.request = Object.create(request); // 防止多个应用之间公用一个 request
this.response = Object.create(response); // 防止多个应用之间公用一个 response
this.middlewares = []; // 中间件队列
}
use(middleware) {
// this.fn = fn; // 保存中间件函数
this.middlewares.push(middleware);
}
// 构建 ctx
createContext(req, res) {
let ctx = Object.create(this.context); // 防止多个请求共享 ctx
let request = Object.create(this.request); // 防止多个请求共享 request
let response = Object.create(this.response); // 防止多个请求共享 response
ctx.request = request;
ctx.request.req = ctx.req = req;
ctx.response = response;
ctx.response.res = ctx.res = res;
return ctx;
}
compose(ctx) {
let flag = -1; // 用来标识当前中间件第几次调用 next 的一个标识。
// 我需要将 middlewares 中的所有方法拿出了来,先调用第一个,第一个调用完毕后,会调用 next,再去执行第二个
// 类似 co 的异步串行,声明一个执行任务的方法,并初始化执行,完毕后调用下一个任务
const dispatch = i => {
if (flag > i) {
// 同一个中间件 多次调用 next,报错
return Promise.reject('next() called multiple times');
}
flag = i;
// 如果没有中间件,或者执行到最后一个中间件了,返回一个成功的 promise
if (this.middlewares.length == i) return Promise.resolve();
// 包成 promise,传入 ctx,并执行下一个
// () => dispatch(i + 1) 就是我们调用的 next 方法
// 也就是说,每个中间件本身被包成 promise,而且每个 next 返回一个新的 promise
// 这样一层层串起来的
return Promise.resolve(this.middlewares[i](ctx, () => dispatch(i + 1)));
}
return dispatch(0)
}
handleRequest = (req , res) => {
const ctx = this.createContext(req, res);
res.statusCode = 404;
// this.fn(ctx); // 中间件执行
// 把中间件组合成一个 promsie
// promise 执行完之后,再异步返回数据给客户端
this.compose(ctx).then(() => {
// 如果用户设置了 ctx.body 就传递给客户端
if (typeof ctx.body == 'object') {
res.setHeader('Content-type', 'application/json;chartset=utf-8');
// 对象转字符串
res.end(JSON.stringify(ctx.body));
} else if (ctx.body) {
res.end(ctx.body);
} else {
res.end('Not Fount');
}
}).catch(err=>{
this.emit('error',err)
})
}
listen(...args) {
// 就是 node 中的原生 http 模块
const server = http.createServer(this.handleRequest);
server.listen(...args);
}
}
module.exports = Koa;
step3: 兼容 ctx.body = 文件流
ctx.body 支持返回一个文件流。
修改 myKoa/application.js
- 判断 ctx.body 是流类型,直接 ctx.body.pipe(res)
myKoa/application.js
// ...
const Stream = require('stream');
class Koa extends EventEmitter {
// ...
handleRequest = (req , res) => {
const ctx = this.createContext(req, res);
res.statusCode = 404;
this.compose(ctx).then(() => {
// 这里兼容流
if (ctx.body instanceof Stream){
ctx.body.pipe(res);
} else if (typeof ctx.body == 'object') {
res.setHeader('Content-type', 'application/json;chartset=utf-8');
// 对象转字符串
res.end(JSON.stringify(ctx.body));
} else if (ctx.body) {
res.end(ctx.body);
} else {
res.end('Not Fount');
}
}).catch(err=>{
this.emit('error',err)
})
}
}
源码汇总
到这里,koa 的核心原理我们已经实现啦。
myKoa/package.json
{
"main": "./lib/application"
}
myKoa/application.js
const EventEmitter = require('events'); // 有 on('error') 肯定用了 events 模块
const http = require('http');
const request = require('./request');
const response = require('./response');
const context = require('./context');
const Stream = require('stream');
class Koa extends EventEmitter {
constructor() {
super();
this.context = Object.create(context); // 防止多个应用之间公用一个 context
this.request = Object.create(request); // 防止多个应用之间公用一个 request
this.response = Object.create(response); // 防止多个应用之间公用一个 response
this.middlewares = []; // 中间件队列
}
use(middleware) {
// this.fn = fn; // 保存中间件函数
this.middlewares.push(middleware);
}
// 构建 ctx
createContext(req, res) {
let ctx = Object.create(this.context); // 防止多个请求共享 ctx
let request = Object.create(this.request); // 防止多个请求共享 request
let response = Object.create(this.response); // 防止多个请求共享 response
ctx.request = request;
ctx.request.req = ctx.req = req;
ctx.response = response;
ctx.response.res = ctx.res = res;
return ctx;
}
compose(ctx) {
let flag = -1; // 用来标识当前中间件第几次调用 next 的一个标识。
// 我需要将 middlewares 中的所有方法拿出了来,先调用第一个,第一个调用完毕后,会调用 next,再去执行第二个
// 类似 co 的异步串行,声明一个执行任务的方法,并初始化执行,完毕后调用下一个任务
const dispatch = i => {
if (flag > i) {
// 同一个中间件 多次调用 next,报错
return Promise.reject('next() called multiple times');
}
flag = i;
// 如果没有中间件,或者执行到最后一个中间件了,返回一个成功的 promise
if (this.middlewares.length == i) return Promise.resolve();
// 包成 promise,传入 ctx,并执行下一个
// () => dispatch(i + 1) 就是我们调用的 next 方法
// 也就是说,每个中间件本身被包成 promise,而且每个 next 返回一个新的 promise
// 这样一层层串起来的
return Promise.resolve(this.middlewares[i](ctx, () => dispatch(i + 1)));
}
return dispatch(0)
}
handleRequest = (req , res) => {
const ctx = this.createContext(req, res);
res.statusCode = 404;
// this.fn(ctx); // 中间件执行
// 把中间件组合成一个 promsie
// promise 执行完之后,再异步返回数据给客户端
this.compose(ctx).then(() => {
// 如果用户设置了 ctx.body 就传递给客户端
if (ctx.body instanceof Stream){
ctx.body.pipe(res);
} else if (typeof ctx.body == 'object') {
res.setHeader('Content-type', 'application/json;chartset=utf-8');
// 对象转字符串
res.end(JSON.stringify(ctx.body));
} else if (ctx.body) {
res.end(ctx.body);
} else {
res.end('Not Fount');
}
}).catch(err=>{
this.emit('error',err)
})
}
listen(...args) {
// 就是 node 中的原生 http 模块
const server = http.createServer(this.handleRequest);
server.listen(...args);
}
}
module.exports = Koa;
myKoa/context.js
// 代理 比如 ctx.query
const context = {
}
// 代理 ctx.xxx -> ctx.request.xxx
function defineGetter(target, key) {
// 给 context 某属性 增加 get 拦截器
context.__defineGetter__(key, function() {
return this[target][key];
});
}
function defineSetter(target, key) {
context.__defineSetter__(key, function(val) {
this[target][key] = val;
});
}
// 代理取值和赋值
// ctx.query -> ctx.request.query
defineGetter('request', 'query');
defineGetter('request', 'url');
defineGetter('request', 'path');
defineGetter('response', 'body');
defineSetter('response', 'body');
module.exports = context;
myKoa/request.json
const url = require('url');
// ctx.path -> url.parse(this.url).pathname
const request = {
get url() {
// 使用 ctx.request.url 时候,执行 ctx.request.url()
// ctx.request 就是 this
return this.req.url;
},
get path() {
return url.parse(this.url).pathname;
},
get query() {
return url.parse(this.url).query;
}
}
module.exports = request;
myKoa/response.js
// ctx.response.body = 'world'
const response = {
_body: undefined,
get body() {
return this._body;
},
set body(val) {
this.res.statusCode = 200;
this._body = val;
}
}
module.exports = response;