根据koa的原理写一个仿koa的库。
1、koa
定义:
- Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。 ——官网
使用:
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
简介:
- Koa将Node的request 和 response对象都封装到了context中,每次请求都会创建一个ctx,并且在中间件中作为接收器使用。
- 使用Koa过程中要避免使用node原生的方法,绕开koa的response是不被处理的。
- Koa的中间件是一个很强大的功能,接受两个参数
ctx对象、next函数
,通过调用next将执行权交给下一个中间件。多个中间件会形成堆栈结构,按先进后出顺序执行,类似于洋葱模型。 - ctx代理了ctx.requset/ctx.response/req/res,所以他们两个的方法、属性可以直接使用ctx获取到。
2、使用
const Koa = require('koa');
const app = new Koa();
// 对于任何请求,app将调用该异步函数处理请求:
app.use(async (ctx, next) => {
await next();
ctx.response.body = 'Hello, koa';
// 或者
ctx.body = 'Hello, koa';
// 如果想使用node原生的可以, 官网说避免使用node原生的,因为koa封装好了更好用的
ctx.res.end('hello koa');
});
app.listen(3000);
注意:绕过Koa的resposne处理是不被支持的。避免使用以下属性
- res.statusCode: 换成ctx.status,如果 response.status 未被设置, Koa 将会自动设置状态为 200 或 204。
- res.writeHead():
- res.write():
- res.end():换成ctx.body()
2.1 use的坑
use中间件可以使用多次,调用next()会走到下一个中间件里面去,use会把函数封装成promise,当同时有多个use共同处理的时候就会很难搞,下一个use需要“卡住”不然就直接完成了,不会等第二个的promise执行。
app.use((ctx, next)=>{
next();
})
app.use((ctx, next)=>{
ctx.body = 'hello 2';
// 注意这里的异步并不会被等待执行完毕
fs.readFile('./1.txt', function(chunk){
// todo..
})
})
我们肯定是需要在第二个use里异步操作一些什么的,比如读取个文件啥的,然后返回,这个时候就尴尬了。我们只能这样做
app.use((ctx, next)=>{
next();
})
app.use((ctx, next)=>{
ctx.body = 'hello 2';
return Promise((resolve, reject)=>{
fs.readFile('./1.txt', function(chunk){
// 注意这里的异步并不会被等待执行完毕
})
resolve();
})
})
一个请求还好,如果多个请求,都写成这样,GG思密达~
我们需要用到一个包koa-bodyparser
,解析用户的请求体,它用先帮我们处理完请求,然后再走中间件,需要先调用一下方法,不是原生的需要npm install koa-bodyparser
let bodyparser = require('bodyparser');
app.use(bodyparser());
app.use(()=>{
// todo..
})
需要注意的是app.use(bodyparser())
需要放到实际业务逻辑的上面,需要先执行它。
2.2 bodyparser
中间件的特点:
- 可以扩展公共属性和方法
- 可以做权限校验
根据上面的使用,大概可以总结出来bodyparser
的原理,它返回一个async promise
,在里面执行了某些操作之后,把结果挂载到ctx.request.body
上,然后再走next
方法,走下面的use
其他方法。
let bodyparser = function (){
return async (ctx, next)=>{
// 等待当前的promise执行完毕。
await new Promise((resolve, reject)=>{
// 进行一系列的判断和操作
ctx.request.body = 'ok';
resolve();
})
// 执行下一个use
await next();
}
}
2.3 koa-static
处理静态文件需要使用koa-static
这个包进行处理。
2.4 router
处理响应使用router包,快速匹配,安装npm install koa-router
const Router = require('koa-router');
let router = new Router();
let children = new Router();
router.get('/info', async (ctx, next)=>{
ctx.body = 'info';
})
router.post('/save', async (ctx, next)=>{
ctx.body = 'save';
})
children.get('/user', async (ctx, next)=>{
ctx.body = 'user';
})
// 分级匹配
router.post('/save', async (ctx, next)=>{
ctx.body = 'save';
})
router.use('/save', children.routes());
app.use(router.routes());
2.5 koa-generator
安装npm install koa-generator -g
可以快速生成koa项目。
3、创建服务器
来仿一个简易的koa,根据源码会发现Koa有四个文件,所以创建一个文件夹下面有四个文件index.js
,context.js
,request.js
,reponse.js
。
在index.js
里面创建服务器
const http = require('http');
class App{
constructor() {
}
handleRequset(req, res){
res.end('hello')
}
listen(...args) {
let server = http.createServer(this.handleRequset.bind(this));
server.listen(...args);
console.log('server is running')
}
}
module.exports = App;
在根目录创建一个文件server.js
const Server = require('./lowK/index');
const app = new Server();
app.listen(3000); // server is running
这样我们的服务就创建起来了。
koa的中间件use
是一个很强大的功能,接下来实现简易版的use
class App{
constructor() {
this.context = context;
this.response = response;
this.request = request;
}
use(fn){
this.fn = fn;
}
createContext(req, res){
let context = Object.create(this.context);
// 把req res都放到上下文上
context.request = Object.create(this.request);
context.response = Object.create(this.response);
context.req = context.request.req = req;
context.res= context.response.res = res;
return context;
}
handleRequset(req, res){
// 创建上下文
let ctx = this.createContext(req, res);
this.fn(ctx);
ctx.res.end('hello')
}
listen(...args) {
let server = http.createServer(this.handleRequset.bind(this));
server.listen(...args);
console.log('server is running')
}
}
3.1 获取属性
使用:server.js
app.use((ctx, next)=>{
console.log(ctx.req.url); // '/'
})
需要把req,res,request,response都挂载到context上,然后执行use传进来的函数,把context传过去。
koa源码还可以ctx.request.url
获取,这个时候ctx.request指的文件还为空,来完善一下。
// request.js
let request = {
get url() {
// this 为 ctx.request, ctx.request上有req属性,它是原生的req属性,有url,把它的url返回就好了。
return this.req.url;
}
};
module.exports =request;
这个时候这样是可以使用的,但是Koa源码上,可以使用ctx.url
这样获取url,我们来完善这一个功能,给ctx代理上response, request
// context.js
let context = {};
context.__defineGetter__('url',function(){
return this.request.url;
})
module.exports =context;
源码上用的是__defineGetter__
这个方法,MCD上说
- 该特性是非标准的,请尽量不要在生产环境中使用它!
- 该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。
那我们用另一个来实现一下。
Object.defineProperty(context, 'url', {
get: function() {
return this.request.url;
}
})
__defineGetter__
就相当于defineProperty
的get,同时还有一个__defineSetter__
相当于defineProperty
set
封装起来:
function getter(prop, name) {
context.__defineGetter__(name,function(){
return this[prop][name];
})
}
使用:getter('response', 'url');
3.2 设置属性
当我们要实现设置的时候,Koa源码可以直接在中间件里面设置
ctx.body = 'xxx';
我们来实现这个问题,首先需求确认一点,koa源码里面可以采用ctx.body='xxx'
or ctx.response.body='xxx'
;
能设置肯定可以获取,我们先来代理上body
,方便可以获取
getter('response', 'body');
我们在response
里代理好body
let response = {
_body:'',
get body(){
return this._body
},
set body(n){
this._body = n
}
};
module.exports =response;
然后在上下文中代理上,做一个双向代理
// context.js
function setter(prop, name){
context.__defineSetter__(name,function(val){
this[prop][name] = val;
})
}
setter('response', 'body');
这样就可以了
3.3 next方法
koa.use(ctx, next)方法的第二个参数是一个方法,决定是否继续向下执行,支持async+await语法,所以next肯定是一个promise。
来实现一下它,先确定用法
// 使用,期待服务器返回页面一个hello 3
app.use((ctx, next)=>{
ctx.body = 'hello 1';
next();
})
app.use((ctx, next)=>{
ctx.body = 'hello 2';
next();
})
app.use((ctx, next)=>{
ctx.body = 'hello 3';
})
首先这是一个并发问题,然后next是个promise,使用队列的原理来做,我们把所有的next都放到一个数组里,由于next是个promise,我们最后肯定要返回一个promise
// 开搞 index.js
// 这样就把所以的fn都存起来了,我们把他们组合成一个大的promise,源码里就是这样搞的
constructor() {
this.context = context;
this.response = response;
this.request = request;
this.middlewares = [];
}
use(fn){
this.middlewares.push(fn);
}
handleRequset(req, res){
// 创建上下文
let ctx = this.createContext(req, res);
// 需要一个函数把所有的fn都执行完毕之后返回一个promise,然后执行
this.compose(ctx).then( ()=>{
let _body = ctx.body;
res.end(_body);
})
}
compose(ctx){
let dispatch = (index) => {
if(index == this.middlewares.length){
return Promise.resolve();
}
// 拿到每一个fn
let fn = this.middlewares[index];
// 因为是一个promise,所以需要这里返回一个成功的promise
// fn() 接受两个参数,一个ctx,一个next方法。
return Promise.resolve(fn(ctx, dispatch.bind(null, index+1)));
}
return dispatch(0);
}
// 页面展示 hello 3
3.4 错误处理
node中所有事件都是基于events
模块的,所以我们可以通过它开实现错误监听
// index.js
const events = require('events');
class App extends events{
constructor() {
super();
this.context = context;
this.response = response;
this.request = request;
this.middlewares = [];
}
handleRequset(req, res){
// 创建上下文
let ctx = this.createContext(req, res);
this.compose(ctx).then( ()=>{
let _body = ctx.body;
res.end(_body);
}).catch((err)=>{
this.emit('error', err);
})
}
}
// server.js
// 监听错误
app.on('error', (err)=>{
console.log(err);
})
最终
源码我会放到GitHubhttps://github.com/LB-nan/lowK。欢迎star