参考KOA,5步手写一款粗糙的web框架
2018-08-23 23:16
399 查看
我经常在网上看到类似于
我这里选择KOA作为参考范本,只有一个原因!他非常的精简!核心只有4个js文件!基本上就是对createServer的一个封装。
在开始解刨KOA之前,createServer的用法还是需要回顾下的:
回顾了createServer,接下来就是解刨KOA的那4个文件了:
application.js
这个js主要就是对createServer的封装,其中一个最主要的目的就是将他的callback分离出来,让我们可以通过
request.js
封装createServer中返回的req,主要用于读写属性。
response.js
封装createServer中返回的res,主要用于读写属性。
context.js
这个文件就很重要了,它主要是封装了request和response,用于框架和中间件的沟通。所以他叫上下文,也是有道理的。
好了~开始写框架咯~
仅分析大概思路,分析KOA的原理,所以并不是100%重现KOA。
本文github地址:点我
step1 封装
先写一个初始版的
封装
将回调独立出来
step1/application.js
这边的
友情链接
ES6解构赋值
step1/testhttp.js
运行
读取(get)req和res的内容
修改(set)res的内容
step2/request.js
step2/response.js
如果po上代码,就是这么简单,需要的属性可以自己加上去。那么问题来这个
回到我们的
step2/application.js
此处,我们用
step3
step3/content.js
这样我们就可以方便地进行定义数据了,不过需要注意地是,
step3/application.js
接下来就是连接
以上3步终于把准备工作做好了,接下来进入正题。😭
友情链接:
Object.defineProperty
step4 实现
这里我需要完成两个功能点:
想要多个中间件执行,那么就建一个数组,将所有地方法都保存在里头,然后等到执行的地时候forEach一下,逐个执行。传入的
step4/application.js
此处在
step4/testhttp.js
这里要分两点来处理:
中间件的异步完成后
首先是
如果我需要中间件是异步的,那么我们可以利用async/await这么写,返回一个promise
如果是promise,那么我就不能按照普通的程序foreach执行了,我们需要一个完成之后在执行另一个,那么这边我们就需要将这些函数组合放入另一个方法
那么compose怎么写呢?
首先这个middlewares需要一个执行完之后再进行下一个的执行,也就是回调。其次compose需要返回一个promise,为了告诉最后我执行完毕了。
第一版本compose,简易的回调,像这样。不过这个和
第二版本compose,我们加上async/await,并返回promise,像这样。不过这个和
具体操作如下:
上述代码先执行
那么上述代码就先执行
这个时候我们会想,既然我这个中间件不是异步的,那么是不是就可以不用加上async/await了呢?实践出真理:
那么程序就不会等后面的异步结束就先结束了。因此如果有异步的需求,尤其是需要靠异步执行再进行下一步的的操作,就算本中间件没有异步需求,也要加上async/await。
终于写完了,感觉脑细胞死了不少,接下来我去研究router和ejs,等这一块加入我的web框架之后,就很完美了~
KOA VS express的文章,大家都在讨论哪一个好,哪一个更好。作为小白,我真心看不出他两who更胜一筹。我只知道,我只会跟着官方文档的start做一个DEMO,然后我就会宣称我会用KOA或者express框架了。但是几个礼拜后,我就全忘了。web框架就相当于一个工具,要使用起来,那是分分钟的事。毕竟人家写这个框架就是为了方便大家上手使用。但是这种生硬的照搬模式,不适合我这种理解能力极差的使用者。因此我决定扒一扒源码,通过官方API,自己写一个web框架,其实就相当于“抄”一遍源码,加上自己的理解,从而加深影响。不仅需要知其然,还要需要知其所以然。
我这里选择KOA作为参考范本,只有一个原因!他非常的精简!核心只有4个js文件!基本上就是对createServer的一个封装。
在开始解刨KOA之前,createServer的用法还是需要回顾下的:
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个文件了:
application.js
这个js主要就是对createServer的封装,其中一个最主要的目的就是将他的callback分离出来,让我们可以通过
app.use(callback);来调用,其中
callback大概就是令大家闻风丧胆的中间件(middleWare)了。
request.js
封装createServer中返回的req,主要用于读写属性。
response.js
封装createServer中返回的res,主要用于读写属性。
context.js
这个文件就很重要了,它主要是封装了request和response,用于框架和中间件的沟通。所以他叫上下文,也是有道理的。
好了~开始写框架咯~
仅分析大概思路,分析KOA的原理,所以并不是100%重现KOA。
本文github地址:点我
step1 封装http.createServer
先写一个初始版的application,让程序先跑起来。这里我们仅仅实现:
封装
http.createServer到myhttp的类
将回调独立出来
listen方法可以直接用
step1/application.js
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的用法一摸一样,就是传递了下参数
友情链接
server.listen的API
ES6解构赋值
...
step1/testhttp.js
let myhttp=require("./application") let app= new myhttp() app.listen(3000)
运行
testhttp.js,结果打印出了
req和
res就成功了~
step2 封装原生req和res
这里我们需要做的封装,所需只有两步:读取(get)req和res的内容
修改(set)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打了基础。
step3 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步终于把准备工作做好了,接下来进入正题。😭
友情链接:
Object.defineProperty
step4 实现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) }) ...
step5 实现中间件的异步执行
任何程序只要加上了异步之后,感觉难度就蹭蹭蹭往上涨。这里要分两点来处理:
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和ejs,等这一块加入我的web框架之后,就很完美了~
相关文章推荐
- 动手写一个Python Web 框架学习笔记 - 搭建开发环境 (1)
- Spring 框架参考文档(五)-The Web之 WebSocket 支持
- 推荐一款程序员常用的web前端框架
- Spring 框架参考文档(六)-Integration之Remoting and web services using Spring
- Spring Framework 开发参考手册 之十二 Web框架
- 对Golang有兴趣的朋友,推荐一款go语言Web框架-dotweb
- golang下很舒服的web框架:neo——推荐了解nodejs的koa的用户使用
- python - web框架 - Flask 参考内容
- 国际化编程实现和Struts验证框架 参考工程validateWeb
- KoaHub.js -- 基于 Koa.js 平台的 Node.js web 快速开发框架
- 一款轻量级移动web开发框架
- karloop介绍--hello world大家好,今天为大家介绍一款非常轻量级的的web开发框架,karloop框架。使用python开发 首先我们下载karloop源码进行安装。 源码地址 下载成
- Ionic:一款用web技术开发类似原生App的神乎其技的html5框架
- Python实现手写一个类似django的web框架示例
- Faygo一款最适合开发API的 Go Web 框架
- Resty 一款极简的restful轻量级的web框架
- 动手写一个Python Web 框架学习笔记 - 相关依赖学习(2)
- 为 Koa 框架封装 webpack-dev-middleware 中间件
- RDIFramework.NET V2.5(.NET快速信息化系统开发框架) Web版界面样例(可参考)
- web前端技术框架选型参考