深刻理解express框架的匹配路由機制

如今node的web框架有不少,除express 還有koa egg等等。 但它們本質上仍是基於原生node框架的http。其實框架都大差不差,主要是觀摩和學習。本篇文章主要記錄下本身在node爬坑之路上的經歷和收穫~node

本文主要實現express的功能之一, 匹配路由git

  • 匹配簡單靜態路由
  • 匹配動態路由

首先咱們看一下express:github

const express = require('express');

let app = new express();

app.get('/',(req,res)=>{
    res.end('home Page.');
});

app.get('/center',(req,res)=>{
    res.end('center Page.');
});

/** 匹配到動態路由 獲取路由參數並返回 */
 app.get('/product/:id/:name',(req,res)=>{
     res.end(JSON.stringify(req.params));
 });

/** 當以上路徑都沒有匹配成功時 返回404 */
app.all('*',(req,res)=>{
    res.end('404');
});

let port = 3000;

app.listen(port,()=>{
    console.log(`Server is start on port ${port}`);
});

ok.代碼很簡單。引入express,new了個express實例,寫了幾個路由,最後啓了本地服務。web

代碼第二行 咱們把引入的express 給new出來,說明express內部返回的是一個function。正則表達式

好.麻雀雖小 五臟俱全,咱們今天就來實現express的這些功能。express

let http = require('http'); /** express基於http */
let url = require('url'); /** 用來解析請求的路徑 */
/** express引入了methods 它的做用是返回各類的請求方法 */
let methods = require('methods');

function application(){
    /** 1 express返回了一個函數 
     * 這個函數就是http.createServer的監聽函數
    */
    let app = (req,res) => {
        /** 1.1 url模塊解析 拿到請求路徑 好比 /user */
        let { pathname } = url.parse(req.url);
        /** 1.2 拿到請求方法 方法是大寫 記得轉換爲小寫 */
        let requestMethod = req.method.toLowerCase();
        /** 1.3 經過拿到的路徑和方法 在以前定義好的路由數組routes中 循環去匹配 */
        for(let i = 0; i < app.routes.length; i++){
            /** 1.4 解構 拿到每個路由的 路徑 方法 回調 */
            let { path, method, cb } = app.routes[i];
            if((pathname===path||path==='*') && (requestMethod===method)||method==='all'){
                /** 1.5 若是匹配到 返回回調並執行 */
                return cb(req,res);
            }
        }
        /** 1.6 沒有匹配到任何路由 */
        res.end(`Cannot found ${pathname}/${requestMethod}`);
    }

    /** 2 定義一個存放全部路由的數組 */
    app.routes = [];
    /** 2.1 往methods數組中添加一個方法 all  並循環數組 */
    [...methods,'all'].forEach((method)=>{
        app[method] = function(path,cb){
            /** 2.2 先將每一個請求的路由地址 方法和回調保存起來 
             * path:路徑  method:方法   cb:回調
            */
            let layer = { path, method, cb };
            app.routes.push(layer);
        }
    });

    /** 3 監聽端口 */
    app.listen = function(...arguments){
        /** 3.1 利用http的createServer方法 將app傳進去 */
        let server = http.createServer(app);
        server.listen(...arguments);
    }
    return app;
}

/** 4 將方法導出出去 */
module.exports = application;

代碼上面都仔細的標註了觀看序號,1.2.3... 按照順序觀看便可。數組

咱們手寫的整個express就是一個函數 函數裏面return了一個函數。經過node原生框架http的方法 包裝了該函數,最後再將整個函數module.exports導出出去。
最後咱們啓動項目,經過瀏覽器或者postman調用接口,發現確實能實現部分的express功能,可是有一點,此時咱們能實現的僅僅是靜態的路由,若是有路由參數的狀況下,好比/product/:id/:name。結果就不符合預期。 改造:瀏覽器

代碼上面都仔細的標註了觀看序號,1.2.3... 按照順序觀看便可。app

let http = require('http');
let url = require('url');
let methods = require('methods');

function application(){
    let app = (req,res) => {
        let { pathname } = url.parse(req.url);
        let requestMethod = req.method.toLowerCase();
        for(let i = 0; i < app.routes.length; i++){
            let { path, method, cb } = app.routes[i];
            /** 7 若是請求路徑path中 就說明該路由是動態的 */
            if(path.params){
                /** 8 匹配該動態路由後面的動態參數 匹配成功返回true */
                if(path.test(pathname)){
                    /** 9 解構賦值 拿到動態路由的參數 */
                    let [, ...otherParams] = pathname.match(path);
                    /** 10 經過reduce()方法 將路由參數轉換爲對象形式
                     * 並放到req.params中
                     */
                    req.params = path.params.reduce(
                        (memo,key,index)=>(
                            memo[key]=otherParams[index],memo
                        ),{}
                    );
                    /** 11 返回匹配到的動態路由 */
                    return cb(req,res);
                }
            }

            if((pathname===path||path==='*') && (requestMethod===method)||method==='all'){
                return cb(req,res);
            }
        }
        res.end(`Cannot found ${pathname}/${requestMethod}`);
    }

    app.routes = [];
    [...methods,'all'].forEach((method)=>{
        app[method] = function(path,cb){
            let layer = { path, method, cb };

            /** 1 定義一個空數組 來存放動態路由的參數 */
            let params = [];
            /** 2 若是路徑中包含: 說明該路由是動態路由 */
            if(path.includes(':')){
                /** 3 更改該動態路由的路徑path爲一個正則表達式
                 * 目的是爲了等真正請求到來時 匹配到該動態路由 並拿到路由參數
                 */
                layer.path = new RegExp(path.replace(/:([^\/]*)/g,function(){
                    /** 4 將動態路由參數的key 放入params數組中 */
                    params.push(arguments[1]);
                    /** 5 返回了一個正則來匹配真正的動態路由參數 注意此處沒有: */
                    return '([^\/]*)';
                }));
                /** 6 把解析到的動態路由放到該路由路徑path的params上 */
                layer.path.params = params;
            }

            app.routes.push(layer);
        }
    });

    app.listen = function(...arguments){
        let server = http.createServer(app);
        server.listen(...arguments);
    }

    return app;
}

module.exports = application;

先經過正則匹配到該動態路由,並把該動態路由的path替換爲一個正則,放到數組中,等待真正的動態路由到來時,從路由數組中拿到該動態路由的路徑,也就是剛纔替換的正則,來匹配該動態路由後的參數便可。框架

經過以上就能實現獲取動態路由的參數 上圖:

clipboard.png

代碼在git mock-express

相關文章
相關標籤/搜索