寫一個最簡陋的node框架

saltfish666發表於2019-03-04

我們知道最原生的處理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框架
寫一個最簡陋的node框架

這應該就是最簡單的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
}
複製程式碼
寫一個最簡陋的node框架

(我的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)
複製程式碼

是不是有幾分自定義模組的味道了?

相關文章