我经常在网上看到类似于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的用法一摸一样,就是传递了下参数
友情链接
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=requeststep2/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地对象只能定义一次,不能多次定义,会报错滴。