阅读 771

写一个最简陋的node框架

我们知道最原生的处理http请求的服务端应该这样写

const http = require("http")
const server = http.createServer(function (req,res) {
    console.log(req)
    res.setHeader("Content-Type","text/plain")
    res.write("hello world")
    console.log(res)
    res.end
})
server.listen(3000)
复制代码

然后保存为test.js,用 node --inspect test.js 运行,在chrome://inspect/#devices打开调试界面调试界面,然后访问 http://localhost:3000/aa/bb?qq=ww ,在调试界面查看结果.

这应该就是最简单的node http server端的代码了。 我们首先创建了一个server,并给他传入了一个回调函数,然后让他监听3000 端口。

这个回调函数接受两个参数,req:包含http请求的相关信息;res:即将返回的http相应的相关信息。

当我们接收到以个http请求后,我们最关注哪些信息? 一般比较关注的有:

  • 请求的方法
  • 请求的路径
  • header
  • cookie

等信息。但是这些信息在req中太原始了,

  • 路径和查询字符串交叉在一起 req.url:"/aa/bb?qq=ww"
  • cookie藏得更深在 req.headers.cookie

所以我们在接收一个请求可先做一些处理,比如说先将查询字符串和cookie从字符串parse为键值对,然后再进入业务逻辑。 我们可以这样写:

const http = require("http")

const server = http.createServer(function (req,res) {
    getQueryObj(req)
    getCookieObj(req)
    res.setHeader("Content-Type","text/plain")
    res.write(JSON.stringify(req.query))
    res.write(JSON.stringify(req.cookie))
    res.end()
})
server.listen(3000)

function getQueryObj(req){
    let query = {}
    let queryString = req.url.split("?")[1] || ""
    let items = queryString.length ? queryString.split("&") : []
    for (let i=0; i < items.length; i++){
        let item = items[i].split("=");
        let name = decodeURIComponent(item[0]);
        let value = decodeURIComponent(item[1]);
        query[name] = value;
    }
    req.query = query
}
function getCookieObj(req) {
    let cookieString = req.headers.cookie || ""
    let cookieObj = {}
    let items = cookieString.length ? cookieString.split(";") : []
    for (let i=0; i < items.length; i++){
        let item = items[i].split("=");
        let name = decodeURIComponent(item[0]);
        let value = decodeURIComponent(item[1]);
        cookieObj[name] = value;
    }
    req.cookie = cookieObj
}
复制代码

(我的localhost:3000之前设置过cookie,你的浏览器未必有,不过没有也没有关系,还是可以看到查询字符串) 后面两个将字符串转化为键值对的函数很简单,就不多介绍了。

我们看到,我们确实将查询字符串和cookie提取出来了,已备后续使用。

上述代码确实完成了任务,但是有非常明显的缺陷---代码耦合度太高,不便于维护。这次写个函数处理查询字符串,下次写个函数处理cookie,那再下次呢。

每添加一个函数就要修改callback,非常不便于维护。

那么我们可以怎样修改呢?

我们可以声明一个函数数组afterReqArrayFuns,然后在callback函数中写道

afterReqArrayFuns.forEach(fun => {         
    fun(req)
})
复制代码

这样可以代码自动适应变化,我们每写一个函数,就将他push到这个数组,就可以了。

这是代码:

const http = require("http")

const myHttp = {
    listen:function(port){               
        const server = http.createServer(this.getCallbackFun())
        return server.listen(port)
    },
    getCallbackFun:function(){
        let that = this
        return function (req,res) {
            that.afterReqArrayFuns.forEach(fun => {        
                fun(req)
            })
            res.write(JSON.stringify(req.query))
            res.end()
        }
    },

    afterReqArrayFuns:[],
    afterReq:function(fun){
        this.afterReqArrayFuns.push(fun)
    }
}

function getQueryObj(req){
    //同上
}
function getCookieObj(req) {
    //同上
}

myHttp.afterReq(getQueryObj)
myHttp.afterReq(getCookieObj)

myHttp.listen(3003)
复制代码

router

除了预处理http请求,我们另一个要求就是对不同的请求url做出正确的回应,在express中,这种写法很舒服:


const express = require('express');
const app = express();

app.get('/', function (req, res) {
  res.send('Hello World!');
});

app.post('/aa', function (req, res) {
  res.send('Hello World!');
});

app.listen(3000, function () {
  console.log('Example app listening on port 3000!');
});
复制代码

每个url对应一个路由函数

但是在原生的http中,我们可能要写无数个if-else 或则 switch-case. 那么我们怎么实现类似express的写法呢?

我们可以建立一个url-callback的map对象,每次匹配到相应的url,变调用相应的回调函数。

看代码

const http = require("http")

const myHttp = {
    listen:function(port){                
        const server = http.createServer(this.getCallbackFun())
        return server.listen(port)
    },
    getCallbackFun:function(){
        let that = this
        return function (req,res) {
            that.afterReqArrayFuns.forEach(fun => {     
                fun(req)
            })
            let path = req.url.split("?")[0]    
            let callback = that.router[req.method][path] || 1   // !!!! look here !!!!!!
            callback(req,res)
            res.end()
        }
    },

    afterReqArrayFuns:[],
    afterReq:function(fun){
        this.afterReqArrayFuns.push(fun)
    },

    router:{
        "GET":{},
        "POST":{},
    },
    get:function (path,callback) {
        this.router["GET"][path] = callback
    },
    post:function(path,callback){
        this.router["POST"][path] = callback
    }
}


myHttp.get("/",(req,res) => {
    res.write("it is /")
})
myHttp.get("/aa/bb",(req,res) => {
    res.setHeader("Content-Type","text/plain")
    res.write("it is /aa/bb")
})

myHttp.listen(3003)
复制代码

业务逻辑中,callback函数并没有写死,而是动态确定的

每次写下myHttp.get(path,callback)后,都会在myHttp.router的建立键值对, 而在接受http请求后,模块会查找对应的路由函数来处理请求。

用ES6 改写

上面的代码看起来不规范,我们用ES6语法来改写

module.js


const http = require("http");

class fishHttp {
    constructor(){
        this.afterReqArrayFuns = [];
        this.router = {
            "GET":{},
            "POST":{},
        }
    }

    listen(port){       
        const server = http.createServer(this.getCallbackFun());
        return server.listen(port)
    }
    getCallbackFun(req,res){
        let that =this;
        return function (req,res) {
            that.afterReqArrayFuns.forEach(fun => {       
                fun(req)
            });
            res.write(JSON.stringify(req.query));
            let path = req.url.split("?")[0];
            let callback = that.router[req.method][path] || that.NotExistUrl;
            callback(req,res);
        }
    }

    afterReq(fun){
        for(let i = 0;i<arguments.length;i++){
            this.afterReqArrayFuns.push(arguments[i])
        }
    }

    get(path,callback) {
        this.router["GET"][path] = callback
    }
    post(path,callback){
        this.router["POST"][path] = callback
    }

    NotExistUrl(req,res){
        res.end('Not found')
    }
}

module.exports = fishHttp;
复制代码

在同级目录下test.js

const fishHttp = require("./module")    //node 自动尝试.js .node .json扩展名

function getQueryObj(req){
    let query = {}
    let queryString = req.url.split("?")[1] || ""   
    let items = queryString.length ? queryString.split("&") : []
    for (let i=0; i < items.length; i++){
        let item = items[i].split("=");
        let name = decodeURIComponent(item[0]);
        let value = decodeURIComponent(item[1]);
        query[name] = value;
    }
    req.query = query
}
function getCookieObj(req) {
    let cookieString = req.headers.cookie || ""
    let cookieObj = {}
    let items = cookieString.length ? cookieString.split(";") : []
    for (let i=0; i < items.length; i++){
        let item = items[i].split("=");
        let name = decodeURIComponent(item[0]);
        let value = decodeURIComponent(item[1]);
        cookieObj[name] = value;
    }
    req.cookie = cookieObj
}

myHttp = new fishHttp()

myHttp.afterReq(getQueryObj,getCookieObj)

myHttp.get("/",(req,res) => {
    res.write("it is /")
    res.end()
})
myHttp.get("/aa/bb",(req,res) => {
    res.write("it is /aa/bb")
    res.end()
})

myHttp.listen(3003)
复制代码

是不是有几分自定义模块的味道了?

关注下面的标签,发现更多相似文章
评论