参考KOA,5步手写一款粗糙的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,结果打印出了reqres就成功了~

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这么长的一个指令。这为我们下一步,将requestresponse挂到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

接下来就是连接contextrequestresponse了,新建一个createContext,将responserequest颠来倒去地挂到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步终于把准备工作做好了,接下来进入正题。??

友情链接:

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框架之后,就很完美了~

原文地址:https://www.cnblogs.com/cherryvenus/p/9527211.html

时间: 2024-10-15 22:06:19

参考KOA,5步手写一款粗糙的web框架的相关文章

.NET跨平台:在Mac上跟着错误信息一步一步手写ASP.NET 5程序

今天坐高铁时尝试了一种学习ASP.NET 5的笨方法,从空文件夹开始,根据运行dnx . kestrel命令的错误信息,一步一步写代码,直至将一个最简单的ASP.NET程序运行起来. 尝试的具体步骤如下. 新建一个空文件夹HelloCnblogs: mkdir HelloCnblogs && cd $_ 在这个空HelloCnblogs文件夹中运行 dnx . kestrel 命令(基于CoreCLR的dnx),运行结果是如下的出错信息: System.InvalidOperationEx

手写一个自己的简单MVC框架myPHP

myPHP框架 采用的是MVC 思想,应用纯面向对象及项目单一入口,实现的一个自定义的框架.(自己兴趣的练习) 一.项目单一入口 入口文件 myphp\index.php前台 一个网站所有的请求都请求一个文件(入口文件)index.php\admin.php.入口很简单,用于定义一个访问的权限,引入初始化文件. 初始化文件 字符集 路径 配置文件 自动加载 系统错误display_errors='on'      error_reporting =E_ALL 主要用于实现字符集的设置.路径常量的

.NET跨平台:在mac命令行下用vim手写ASP.NET 5 MVC程序

昨天在 Mac 上手写了一个最简单的 ASP.NET 5 程序,直接在 Startup.cs 中通过 Response.WriteAsync() 输出响应内容,详见 .NET跨平台:在Mac上跟着错误信息一步一步手写ASP.NET 5程序. 今天接着昨天的代码,用 vim 一步一步手写一个最简单的 ASP.NET 5 MVC 程序. 先创建 Controllers 文件夹: mkdir Controllers && cd $_ 接着创建 HomeController.cs 文件: vi H

手写Tomcat服务器

预备知识 编写服务器用到的知识点 1) Socket 编程2) HTML3) HTTP 协议4) 反射5) XML 解析6) 服务器编写 Socket编程 https://www.cnblogs.com/bfcs/p/10790130.html HTML知识 HTML:HyperText Markup Language 超文本标记语言用于描述网页文档的一种标记语言 表单(form):与用户之间进行交互 method:请求方式 get/post get 数据量小,安全性低,默认方式 post 数据

设计师福利:免费使用10款英文手写字体

        又送福利,解决设计师为老土字体抓狂的烦恼,特来送英文手写字体免费使用下载包~让你的设计更加高大上吧~         Pacifico(下载地址)         Recorda Script(下载地址)         Wolf in the City(下载地址)         Hey Pretty Girl(下载地址)         Impregnable(下载地址)         Love & Trust(下载地址)         Mathlete(下载地址)   

设计师收藏的20款英文手写字体【免费下载】

在所有的字体类型中,手写字体是最有用的和用途最多的.根据你需要完成的任务,使用手写字体的地方无处不在.找到合适的匹配特定的项目的字体是很耗费时间的. 所以在这里,我收集了20款精美的手写字体,可以在你未来的设计中使用.这些字体是完全免费的,所以赶快行动吧! 您可能感兴趣的相关文章 字体大宝库:10款有趣的精美节日字体 分享20款很漂亮的免费英文LOGO字体 字体大宝库:20款充满艺术感英文字体 推荐20款免费的又好看的英文手写字体 向设计师推荐20款好看的英文装饰字体 Pacifico Reco

全面理解Handler第一步:理解消息队列,手写消息队列

前言 Handler机制这个话题,算是烂大街的内容.但是为什么偏偏重拿出来"炒一波冷饭"呢?因为自己发现这"冷饭"好像吃的不是很明白.最近在思考几个问题,发现以之前对Handler机制的了解是在过于浅显.什么问题? Handler机制存在的意义是什么?能否用其他方式替换? Looper.loop();是一个死循环,为什么没有阻塞主线程?用什么样的方式解决死循环的问题? 如果透彻的了解Handler,以及线程的知识.是肯定不会有这些疑问的,因为以上问题本身就存在问题.

【机器学习】BP神经网络实现手写数字识别

最近用python写了一个实现手写数字识别的BP神经网络,BP的推导到处都是,但是一动手才知道,会理论推导跟实现它是两回事.关于BP神经网络的实现网上有一些代码,可惜或多或少都有各种问题,在下手写了一份,连带着一些关于性能的分析也写在下面,希望对大家有所帮助. 本博文不含理论推导,如对BP的理论推导感兴趣百度即可,或参考<模式识别>. 一.数据库 程序使用的数据库是mnist手写数字数据库,这个数据库我有两个版本,一个是别人做好的.mat格式,训练数据有60000条,每条是一个784维的向量,

C++使用matlab卷积神经网络库MatConvNet来进行手写数字识别

环境:WIN10(64 bit)+VS2010(64 bit)+Matlab2015b(64 bit) 关于MatConvNet的介绍参考:http://www.vlfeat.org/matconvnet/ Github下载地址为:https://github.com/vlfeat/matconvnet/ 我们的目的是将MatConvNet自带的手写数字识别DEMO移植到一个简单的WIN32 DEMO中使用,主要过程有以下几个步骤: (1)配置MatConvNet,然后将手写数字识别DEMO编译