我常常在网上看到相似于KOA VS express
的文章,你们都在讨论哪个好,哪个更好。做为小白,我真心看不出他两who更胜一筹。我只知道,我只会跟着官方文档的start作一个DEMO,而后我就会宣称我会用KOA或者express框架了。可是几个礼拜后,我就全忘了。web框架就至关于一个工具,要使用起来,那是分分钟的事。毕竟人家写这个框架就是为了方便你们上手使用。可是这种生硬的照搬模式,不适合我这种理解能力极差的使用者。所以我决定扒一扒源码,经过官方API,本身写一个web框架,其实就至关于“抄”一遍源码,加上本身的理解,从而加深影响。不只须要知其然,还要须要知其因此然。html
我这里选择KOA做为参考范本,只有一个缘由!他很是的精简!核心只有4个js文件!基本上就是对createServer的一个封装。node
在开始解刨KOA以前,createServer的用法仍是须要回顾下的:git
const http = require('http');
let app=http.createServer((req, res) => {
//此处省略其余操做
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.body="我是createServer";
res.end('okay');
});
app.listen(3000)
复制代码
回顾了createServer,接下来就是解刨KOA的那4个文件了:github
app.use(callback);
来调用,其中callback
大概就是令你们闻风丧胆的中间件(middleWare)了。好了~开始写框架咯~web
仅分析大概思路,分析KOA的原理,因此并非100%重现KOA。express
本文github地址:点我api
http.createServer
先写一个初始版的application
,让程序先跑起来。这里咱们仅仅实现:数组
http.createServer
到myhttp的类listen
方法能够直接用step1/application.jspromise
let http=require("http")
class myhttp{
handleRequest(req,res){
console.log(req,res)
}
listen(...args){
// 起一个服务
let server = http.createServer(this.handleRequest.bind(this));
server.listen(...args)
}
}
复制代码
这边的listen
彻底和server.listen
的用法一摸同样,就是传递了下参数bash
友情连接
step1/testhttp.js
let myhttp=require("./application")
let app= new myhttp()
app.listen(3000)
复制代码
运行testhttp.js
,结果打印出了req
和res
就成功了~
这里咱们须要作的封装,所需只有两步:
step2/request.js
let request={
get url(){
return this.req.url
}
}
module.exports=request
复制代码
step2/response.js
let response={
get body(){
return this.res.body
},
set body(value){
this.res.body=value
}
}
module.exports=response
复制代码
若是po上代码,就是这么简单,须要的属性能够本身加上去。那么问题来这个this
指向哪里??代码是很简单,可是这个指向,并不简单。
回到咱们的application.js
,让这个this
指向咱们的myhttp的实例。
step2/application.js
class myhttp{
constructor(){
this.request=Object.create(request)
this.response=Object.create(response)
}
handleRequest(req,res){
let request=Object.create(this.request)
let response=Object.create(this.response)
request.req=req
request.request=request
response.req=req
response.response=response
console.log(request.headers.host,request.req.headers.host,req.headers.host)
}
...
}
复制代码
此处,咱们用Object.create
拷贝了一个副本,而后把request和response分别挂上,咱们能够经过最后的一个测试看到,咱们能够直接经过request.headers.host
访问咱们须要的信息,而能够不用经过request.req.headers.host
这么长的一个指令。这为咱们下一步,将request
和response
挂到context
打了基础。
context
闪亮登场context
的功能,我对他没有其余要求,就能够直接context.headers.host
,而不用context.request.headers.host
,可是我不可能每次新增须要的属性,都去写一个get/set吧?因而Object.defineProperty
这个神操做来了。
step3/content.js
let context = {
}
//可读可写
function access(target,property){
Object.defineProperty(context,property,{
get(){
return this[target][property]
},
set(value){
this[target][property]=value
}
})
}
//只可读
function getter(target,property){
Object.defineProperty(context,property,{
get(){
return this[target][property]
}
})
}
getter('request','headers')
access('response','body')
...
复制代码
这样咱们就能够方便地进行定义数据了,不过须要注意地是,Object.defineProperty
地对象只能定义一次,不能屡次定义,会报错滴。
step3/application.js 接下来就是链接context
和request
和response
了,新建一个createContext
,将response
和request
颠来倒去地挂到context
就可了。
class myhttp{
constructor(){
this.context=Object.create(context)
...
}
createContext(req,res){
let ctx=Object.create(this.context)
let request=Object.create(this.request)
let response=Object.create(this.response)
ctx.request=request
ctx.response=response
ctx.request.req=ctx.req=req
ctx.response.res=ctx.res=res
return ctx
}
handleRequest(req,res){
let ctx=this.createContext(req,res)
console.log(ctx.headers)
ctx.body="text"
console.log(ctx.body,res.body)
res.end(ctx.body);
}
...
}
复制代码
以上3步终于把准备工做作好了,接下来进入正题。😭 友情连接:
use
这里我须要完成两个功能点:
use
能够屡次调用,中间件middleWare按顺序执行。use
中传入ctx
上下文,供中间件middleWare调用想要多个中间件执行,那么就建一个数组,将全部地方法都保存在里头,而后等到执行的地时候forEach一下,逐个执行。传入的ctx
就在执行的时候传入便可。
step4/application.js
class myhttp{
constructor(){
this.middleWares=[]
...
}
use(callback){
this.middleWares.push(callback)
return this;
}
...
handleRequest(req,res){
...
this.middleWares.forEach(m=>{
m(ctx)
})
...
}
...
}
复制代码
此处在use
中加了一个小功能,就是让use能够实现链式调用,直接返回this
便可,由于this
就指代了myhttp
的实例app
。
step4/testhttp.js
...
app.use(ctx=>{
console.log(1)
}).use(ctx=>{
console.log(2)
})
app.use(ctx=>{
console.log(3)
})
...
复制代码
任何程序只要加上了异步以后,感受难度就蹭蹭蹭往上涨。
这里要分两点来处理:
use
中中间件的异步执行compose
的异步执行。首先是use
中的异步 若是我须要中间件是异步的,那么咱们能够利用async/await这么写,返回一个promise
app.use(async (ctx,next)=>{
await next()//等待下方完成后再继续执行
ctx.body="aaa"
})
复制代码
若是是promise,那么我就不能按照普通的程序foreach执行了,咱们须要一个完成以后在执行另外一个,那么这边咱们就须要将这些函数组合放入另外一个方法compose
中进行处理,而后返回一个promise,最后来一个then
,告诉程序我执行完了。
handleRequest(req,res){
....
this.compose(ctx,this.middleWares).then(()=>{
res.end(ctx.body)
}).catch(err=>{
console.log(err)
})
}
复制代码
那么compose怎么写呢?
首先这个middlewares须要一个执行完以后再进行下一个的执行,也就是回调。其次compose须要返回一个promise,为了告诉最后我执行完毕了。
初版本compose,简易的回调,像这样。不过这个和foreach
并没有差异。这里的fn
就是咱们的中间件,()=>dispatch(index+1)
就是next
。
compose(ctx,middlewares){
function dispatch(index){
console.log(index)
if(index===middlewares.length) return;
let fn=middlewares[index]
fn(ctx,()=>dispatch(index+1));
}
dispatch(0)
}
复制代码
第二版本compose,咱们加上async/await,并返回promise,像这样。不过这个和foreach
并没有差异。dispatch
必定要返回一个promise。
compose(ctx,middlewares){
async function dispatch(index){
console.log(index)
if(index===middlewares.length) return;
let fn=middlewares[index]
return await fn(ctx,()=>dispatch(index+1));
}
return dispatch(0)
}
复制代码
return await fn(ctx,()=>dispatch(index+1));
注意此处,这就是为何咱们须要在next
前面加上await才能生效?做为promise的fn
已经执行完毕了,若是不等待后方的promise,那么就直接then
了,后方的next
就自生自灭了。因此若是是异步的,咱们就须要在中间件上加上async/await
以保证next
执行完以后再返回上一个promise
。没法理解?😷了?咱们看几个例子。
具体操做以下:
function makeAPromise(ctx){
return new Promise((rs,rj)=>{
setTimeout(()=>{
ctx.body="bbb"
rs()
},1000)
})
}
//若是下方有须要执行的异步操做
app.use(async (ctx,next)=>{
await next()//等待下方完成后再继续执行
ctx.body="aaa"
})
app.use(async (ctx,next)=>{
await makeAPromise(ctx).then(()=>{next()})
})
复制代码
上述代码先执行ctx.body="bbb"
再执行ctx.body="aaa"
,所以打印出来是aaa
。若是咱们反一反:
app.use(async (ctx,next)=>{
ctx.body="aaa"
await next()//等待下方代码完成
})
复制代码
那么上述代码就先执行ctx.body="aaa"
再执行ctx.body="bb"
,所以打印出来是bbb
。 这个时候咱们会想,既然我这个中间件不是异步的,那么是否是就能够不用加上async/await了呢?实践出真理:
app.use((ctx,next)=>{
ctx.body="aaa"
next()//不等了
})
复制代码
那么程序就不会等后面的异步结束就先结束了。所以若是有异步的需求,尤为是须要靠异步执行再进行下一步的的操做,就算本中间件没有异步需求,也要加上async/await。
有关于router的操做,请移步这份Koa的简易Router手敲指南请收下