我們知道最原生的處理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)
複製程式碼
是不是有幾分自定義模組的味道了?