日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

對比原生Node封裝的Express路由 和 express框架路由

作者:白瑕 更新時間: 2022-01-12 編程語言


前言

我將擬express方法和正規的express方法做了很多對比(類似"這個方法是為了實現express的哪個功能")

全篇主要在兩個文件里完成, 一個是"父目錄/app.js", 另一個是"父目錄/module/route.js";
app.js部分因為有個很長的方法會有些繁瑣, 那是為了完成路由中間件的注冊和阻復注冊, 不過我加了很多注釋, 我覺得你應該能看懂…


一、routes.js

以下是源碼, 我會拆開說的:

const fs = require('fs');
const path = require('path');

let changesRes = function (res) {
    res.send = (data) => {
        res.writeHead(200, { 'Content-Type': 'text/html;charset="utf-8"' });
        res.end(data);
    }
}

let getFileMime = function (extname) {
    let data = fs.readFileSync('./data/mime.json');
    let mimeObj = JSON.parse(data.toString());
    return mimeObj[extname];
}

let initStatic = function (req, res, staticPath) {
    const { url } = req;
    const { host } = req.headers;
    const myURL = new URL(url, `http://${host}`);
    let pathname = myURL.pathname;
    let extname = path.extname(pathname);

    if (extname) {
        try {
            let data = fs.readFileSync('./' + staticPath + pathname);
            if (data) {
                let mime = getFileMime(extname);
                res.writeHead(200, { 'Content-Type': '' + mime + ';charset="utf-8"' });
                res.end(data);
            }
        } catch (error) {
            console.log(error)
        }
    }

}
let server = () => {
    let G = {
        _get: {},
        _post: {}, 
        staticPath: "static",
    };
    let app = function (req, res) {
        changesRes(res);
        initStatic(req, res, G.staticPath);

        const { url } = req;
        const { host } = req.headers;
        const myURL = new URL(url, `http://${host}`);
        let pathname = myURL.pathname;
        let method = req.method.toLowerCase();
        let extname = path.extname(pathname);
        if (!extname) {
            if (G['_' + method][pathname]) {
                if (method == "get") {
                    G['_' + method][pathname](req, res);

                } else {
                    let postData = '';
                    req.on('data', (chunk) => {
                        postData += chunk;
                    })
                    req.on('end', () => {
                        req.body = postData;
                        G['_' + method][pathname](req, res);
                    })
                }
            } else {
                res.writeHead(404, { 'Content-Type': 'text/html;charset="utf-8"' });
                res.end("頁面早已離你而去了...");
            }
        }

    }
    app.get = function (str, cb) {
        G._get[str] = cb;
    }
    app.post = function (str, cb) {
        G._post[str] = cb;
    }

    app.static = function (staticPath) {
        G.staticPath = staticPath;
    }
    return app;
}

module.exports = server();

1.引入模塊

因為原本的static()需要fs文件模塊來讀取文件.

const fs = require('fs');
const path = require('path');

2.changesRes() - send()

為了實現express的res.send()方法.
把send()放到changesRes()里, 通過在app()中調用changesRes()的手段將send()釋放進app.js;

這層changesRes()外殼存在的意義是將內部的send()完好的釋放到app()中,并從app()中攝取res.

因為app.js直接調用了send(), 所以send()可以接收到app.js傳入的data(也就是renderFile()的執行結果, 即渲染后的頁面數據), 而app.js也調用了app()和其內部的changesRes()為send()提供了res;
最后由send()中的end()將data放到頁面上.

let changesRes = function (res) {
//在app()內調用changesRes()相當于直接將send()寫到app()中;
    res.send = (data) => {
        res.writeHead(200, { 'Content-Type': 'text/html;charset="utf-8"' });
        res.end(data);
    }
}

changesRes()方法也簡化了app.js中的路由隊列, 現在路由表里只要寫一句"res.send(data)"就好(然而偷懶直接傳了一個字符串);


3.getFileMime() - type()

算是半個res.type()吧, 這個方法調用后返回這個文件對應的文件類型即Content-Type屬性到app()中, 由initStatic()方法進行接收并設置到writeHead里.
或許該說是getFileMime()和initStatic()共同組成了擬res.type();

它接收extname作為參數(別擔心, extname會由app()調用傳入), 這個參數是從URL里拿到的文件路徑所指向文件的擴展名, 因為我們在封裝靜態web, 針對那些帶有文件擴展名的URL進行處理, 至于沒擴展名的…
扔給路由吧! (啪)

//根據后綴名獲取文件類型;
let getFileMime = function (extname) {
    let data = fs.readFileSync('./data/mime.json');
    let mimeObj = JSON.parse(data.toString());  //拿到data轉換為字符串再轉換為對象;
    return mimeObj[extname];
    //extname是".xxx"的形式, 如果用mimeObj.extname會翻車;
}

拿到后綴名之后readFileSync()利用同步阻塞從外部文件mime.json中讀取相應的Content-Type, 讀取結果賦值給data.

但是讀取到的是16進制的數字碼, 先用toString()將其轉化為字符串, 再JSON.parse()把它們轉換為原本在文件里時候的對象形態, 賦值給mimeObj,這樣用起來就舒服多了.

額, 記得return出去…


4.initStatic

一個為了讀取文件而設立的方法.
該方法讀取URL中的文件路徑, 提取文件內容并傳給end();
end()內傳入執行完畢后要呈現的內容,如果指定了 data 的值,就意味著在執行完 res.end() 之后,會接著執行如下語句:

response.write(data , [對應的字符編碼encoding]);

來看看完整方法:

//靜態web服務的方法;
let initStatic = function (req, res, staticPath) {

    const { url } = req;
    const { host } = req.headers;
    const myURL = new URL(url, `http://${host}`);//拼湊絕對路徑, 并解析;
    let pathname = myURL.pathname;               //拿到解析結果中的pathname屬性即文件路徑;
    let extname = path.extname(pathname);        //利用path模塊提供的方法拿到文件路徑指向的文件的后綴名;
    if (extname) {                               //如果文件擴展名存在, 判定交由route.js的靜態web服務處理;                        //否則交付至app.js中的路由進行處理;
        try {
            let data = fs.readFileSync('./' + staticPath + pathname);  //將同步讀取的文件內容存入變量data;
            if (data) {
                let mime = getFileMime(extname); //傳入文件擴展名,mime被賦值文件類型;
                res.writeHead(200, { 'Content-Type': '' + mime + ';charset="utf-8"' });
                res.end(data);
            }
        } catch (error) {
            console.log(error);                  //嘗試執行出錯, 抓取錯誤信息輸出;
        }
    }

}

5.server()

外殼負責提供G對象內部的數據和方法;
app()負責依據請求方法調用G中的方法(既路由激活后的處理辦法);
app.get()負責將各路由受get請求的處理方法注冊入G;
app.post()負責將各路由受post請求的處理方法注冊入G;
app.static()負責將靜態路徑存入G供initStatic(ststicPath)讀取路由需要的文件;

let server = () => {
    let G = {
        _get: {},  //在G里聲明空對象_get;
        _post: {},  //在G里聲明空對象_post;\
        staticPath: "static",
    };
    //app();
    //app.get();
    //app.post();
    //app.stsatic();
    return app;
}

app() - 注冊中間件

依據本次請求的方法來決定是調用app.get()注冊到G里的回調函數還是調用app.post()注冊到G里的回調函數.

這步其實可以看作是express中對路由中間件進行的抽離注冊, 然后在合適的時候掛載到Router對象上;

    //把app放在server里防止全局污染;
let app = function (req, res) {
    changesRes(res);
    //相當于書寫了如下:
    /* res.send = (data) => {
    res.writeHead(200, {'Content-Type':'text/html;charset="utf-8"'});
    res.end(data);
    app.js中可以直接調用send()方法了;
    */
} 

    initStatic(req, res, G.staticPath);    //根據請求的URL讀取響應文件.
    const { url } = req;
    const { host } = req.headers;
    const myURL = new URL(url, `http://${host}`);
    let pathname = myURL.pathname;         //拿取pathname文件路徑(相對路徑);
    let method = req.method.toLowerCase(); //獲取本次請求的方法(GET/POST),并轉換為小寫(get/post)賦值給method;
    let extname = path.extname(pathname);  //用path模塊的自帶方法extname()來獲取到相對路徑所指向文件的擴展名;
    if (!extname) {                        //判定擴展名是否不存在, 存在就留給路由執行;
      if (G['_' + method][pathname]) {     //G中有無歸屬于"_post"或"_get"對象還[符合路徑]的方法;
        if (method == "get") {             //判定本次請求方式是否為get(req.method獲取請求方式)
          G['_' + method][pathname](req, res); //如果是,調用G._get里與路徑(比如"/login")同名的方法(路徑是從app傳入的參數)
            } else {
              //因為post的請求以流的形式進行, 服務器也是分段接收;
              //所以要監聽兩個階段點判斷是不是傳輸完成;
              let postData = '';         
              //聲明并賦值postData為空字符串預備存儲data;
              //POST提交數據以流的形式提交,服務器在接收POST數據的時候是分塊接收的
              //所以用監聽階段點的方式判斷是否傳輸完成;
              req.on('data', (chunk) => {
                    postData += chunk;     //用變量chunk接收拿到的數據, 然后填入postData中;
              })
              req.on('end', () => {       //監聽end事件, 將postData賦值給res.body中
                req.body = postData;
                  G['_' + method][pathname](req, res);//調用app傳入并注冊的該頁的post型回調函數;
              })
            }
        } else {
          //如果沒有找到為這個頁面的路由注冊的任何回調函數, 直接向客戶端返回404;
          res.writeHead(404, { 'Content-Type': 'text/html;charset="utf-8"' });
          res.end("頁面早已離你而去了..."); //或者直接G['/404'](req, res);
        }
  }
}

app.get() - get()

express里可以直接在注冊路由的時候規定這條路由只有甚麼方法才能觸發:

router.get('/api/list', list);

這里就是為了擬這個功能:

將app.js傳入的回調函數注冊到G.get, 作為該條路由觸發時的處理辦法.
GET與POST的提交途徑必須分別交由app.get()和app.post(), 防止G中出現覆蓋注冊;

app.get = function (str, cb) {    //接收app傳來的參數, 在G._get中為傳來的回調函數cb(既"callback的簡寫")進行注冊;   
    G._get[str] = cb;
}

app.post() - post()

express里可以直接在注冊路由的時候規定這條路由只有甚麼方法才能觸發:

router.post('/api/list', list);

這里就是為了擬這個功能:

將app.js傳入的回調函數注冊到G.post, 作為該條路由觸發時的處理辦法.
GET與POST的提交途徑必須分別交由app.get()和app.post(), 防止G中出現覆蓋注冊;

app.post = function (str, cb) {
  //接收app傳來的參數, 在G._post中為傳來的回調函數cb(既"callback的簡寫")進行注冊;
    G._post[str] = cb;
}

app.static()

將app中傳入的靜態路徑存放到G中, 供initStatic()使用.

app.static = function (staticPath) {
//app.static()會在app.js中首先調用, 獲取staticPath存入G中,生成G.staticPath, 供initStatic()使用;
    G.staticPath = staticPath;//將參數staticPath賦值到G.staticPath中.
}

二、app.js

利用route.js中封裝好的擬express方法完成擬express路由的主體結構.
以下是源碼, 我會拆開說的:

const http = require('http');
const app = require('./module/route');
const ejs = require('ejs');

let app = function(req,res){

}

http.createServer(app).listen(3000);

app.static("static"); 
app.get("/login", function (req, res) {
    ejs.renderFile('./views/form.ejs', {}, (err, data) => {
        res.send(data);
    });
})

app.get("/news", function (req, res) {
    res.send("展示");
})

app.get("/register", function (req, res) {
    res.send("注冊頁");
})

app.get("/", function (req, res) {
    res.send("首頁");
})

app.post("/doLogin", function (req, res) {
    res.send(req.body);
})

1.引入模塊

就是引入依賴, 沒什么好說的…

const http = require('http');
//引入route模塊,里面有個server方法要用到;
const app = require('./module/route');
//引入ejs模塊, 待會要使用renderFile();
const ejs = require('ejs');

2.簡化 createServer()

用app()作為createServer的回調函數, 觸發請求直接執行app().

//let app = (req,res) => {}
//http.createServer((req, res) => {});
/* 觀察可知createServer方法中的function結構與app方法的外框完全相同;
因此app()的回調函數可以放入createServer()內,這樣用戶一旦在客戶端發動請求, 馬上就會觸發app(); */

http.createServer(app).listen(3000);

3.擬express路由的掛載

在express框架中, 當有多個子路由需要操作的時候, 將子路由掛載到父級路由router然后直接掛載router會是一個更好的選擇, 只要幾句就好了:

const router = express.Router();
router.get('路徑', 路由中間件);
app.use('/', router);

但如果用app.get()一個個來, 在暴露和引入使用時都不會太方便.很遺憾我只能使用這種"一個個來"的方式在app.js里做路由, 因為router對象是express提供的, 而這是擬express:

app.static("static");    
//向route模塊末的static方法傳參靜態目錄以修改G中的默認靜態web目錄;
//從而完成在靜態web模式下對網頁文件的讀取;

app.get("/login", function (req, res) {
    //app.get()接收到參數后將傳入的回調函數cb注冊到route.js對象G._get中;
    ejs.renderFile('./views/form.ejs', {}, (err, data) => {
        //login頁面的路由被觸發, 渲染路徑form.ejs;
        res.send(data);
        //res.send()將從目標文件讀取到的內容data傳到客戶端;
    });
})

app.get("/", function (req, res) {
    //在此處向引入的route模塊中的app.get()方法傳參,routes模塊的app.get()方法接收到參數后將傳入的回調函數作為參數cb注冊到對象G._get中;
    res.send("首頁信息");
})

依據用戶跳轉到的URL, 調用不同的方法向app.get()或app.post()傳回調函數, 并且在G._post 或 G._get 里注冊這些回調函數, 它們就相當于是express的路由中間件了.

JS的解析是從上至下, 而一次請求只會觸發一次createServer(), 而res.send()或res.join()這種方法如果執行完畢就自動返回, 這一輪響應到這就結束了, 后面的路由根本沒有機會接受匹配, so, 不要把"/"的路由寫在最上面.

與Vue的路由不太像, Vue-router(我是說路由表那邊)不會出現這種結果, Vue路由的結構更加像是標準版的express路由:
用express模塊的Router()注冊路由對象(router是個函數, 也可以作為中間件), 在Vue中這個"中間件"應該可以看作Vue的路由表routes.

const router = express.Router();

然后在這個中間件上安插各條路由, 也就像在routes數組中配置各條路由.

//一般會把路由中間件抽離出來, 這里為了節省篇幅沒有進行;
router.get('/', (req, res, next) => {
    res.send('hello');
});

//use會出現正則匹配錯誤的情況出現, router并不會.
router.get('/index', (req, res, next) => {
    res.send('index pages');
});

rouer對象就像是脊椎, 而這些中間件就像是肋骨.
小路由卡上去以后, 將router暴露.

module.exports = router;

就像vue中暴露路由表至app.js以注冊到全局:

app.use('/', router);

三、總結

這篇拖了好久, 它幾乎占用了這幾天拿來寫博客的所有時間, 我的最初方案是直接記錄下擬express路由的制作方案就結束的, 但因為覺得沒有意義馬上否定了這個方案, 如果不進行任何對比, 不明白它是如何模擬express路由, 與express有何相似, 又有何區別的話, 這種記錄毫無意義, 我想自己以后也不會回來看的.

另外就是因為方法封的比較多, 整篇文章看起來有點亂七八糟的, 那些很長的方法拆開又不行(點名批評app), 但太長大家又不愿意看, 里面再加一堆注釋這看起來就更眼花繚亂了, 我在想如何去解決這個問題(雖然最后也沒解決好).

然后就打算拖一拖等到express學的差不多再回來補上封裝的各個方法與express的對比(事實是每天都要回來改一改這篇文章), 這期間我也完成了幾篇小博文, 但是覺得都有點瑕疵就沒敢發出去, 我覺得哪怕寫一些淺顯的知識, 但至少不能有錯誤.

然后…然后就咕咕咕了…en…

最后, 如果這篇文章幫到了你, 我很高興.

原文鏈接:https://blog.csdn.net/qq_52697994/article/details/121074719

欄目分類
最近更新