Express源碼級實現(xiàn)の路由全解析(上闋)

  • Pre-Notify
  • 項目目錄
  • express.js 和 application.js
  • app對象之http服務(wù)器
  • app對象之路由功能
    • 注冊路由
    • 接口實現(xiàn)
    • 分發(fā)路由
    • 接口實現(xiàn)
  • router
    • 測試用例1與功能分析
    • 功能實現(xiàn)
      • router和route
      • layer
      • 注冊路由
      • 注冊流程圖
      • 路由分發(fā)
      • 分發(fā)流程圖
    • 測試用例2與功能分析
    • 功能實現(xiàn)
  • Q
    • 為什么選用next遞歸遍歷而不選用for?
    • 我們從Express的路由系統(tǒng)設(shè)計中能學到什么?
  • 源碼

Pre-Notify

閱讀本文前可以先參考一下我之前那篇簡單版的express實現(xiàn)的文章。

Express深入理解與簡明實現(xiàn)

相較于之前那版,此次我們將實現(xiàn)Express所有核心功能。

預計分為:路由篇(上、下)、中間件篇(上、下)、炸雞篇~

(づ ̄ 3 ̄)づ Let's Go!

項目目錄

iExpress/
|
|   
| - application.js  #app對象
|
| - html.js         #模板引擎
|
| - route/
|   | - index.js    #路由系統(tǒng)(router)入口
|   | - route.js    #路由對象
|   | - layer.js    #router/route層
|
| - middle/
|   | - init.js     #內(nèi)置中間件
|
| - test-case/
|    | - 測試用例文件1
|    | - ...
|
·- express.js       #框架入口

express.js 和 application.js

在簡單版Express實現(xiàn)中我們已經(jīng)知道,將express引入到項目后會返回一個函數(shù),當這個函數(shù)運行后會返回一個app對象。(這個app對象是原生http的超集

其中,express.js模塊導出的就是那個運行后會返回app對象的函數(shù)

// test-case0.js
let express = require('./express.js');
let app = express(); //app對象是原生http對象的超集
...
app.listen(8080); //調(diào)用的其實就是原生的server.listen

上個版本中因為實現(xiàn)的功能較簡單,只用了一個express.js文件就搞定了,而在這個版本中我們需要專門用一個模塊application.js來存放app相關(guān)的部分

//express.js
const Application = require('./application.js'); //app

function createApplication(){
    return new Application(); //app對象
}

module.exports = createApplication;

app對象之http服務(wù)器

app對象 最重要的一個作用是用來啟動一個http服務(wù)器,通過app.listen方法我們能間接調(diào)用到原生的.listen方法來啟動一個服務(wù)器。

//application.js
function Application(){}
Application.prototype.listen = function(){
    function done(){}
    let server = http.createServer(function(req,res,done){
        ...
    })
    server.listen.apply(server,arguments);
}

app對象之路由功能

app對象的另外一個重要作用,也就是Express框架的主要作用是實現(xiàn)路由功能。

路由功能是個蝦?

路由功能能讓服務(wù)器針對客戶端不同的請求路徑和請求方法做出不同的回應。

而要實現(xiàn)這個功能我們需要做兩件事情:注冊路由路由分發(fā)

[warning] 為了保證app對象作為接口層的清晰明了,app對象只存放接口,而真正實現(xiàn)部分是委托給路由系統(tǒng)(router.js)來處理的。

注冊路由

當一個請求來臨時,我們可以依據(jù)它的請求方式和請求路徑來決定服務(wù)器是否給予響應以及怎么響應。

而我們怎么讓服務(wù)器知道哪些請求該給予響應以及怎樣響應呢?
這就是注冊路由所要做的事情了。

在服務(wù)器啟動時,我們需要對服務(wù)器想要給予回應的請求做上記錄,先存起來,這樣在請求來臨的時候服務(wù)器就能對照這些記錄分別作出響應。

[warning]注意
每一條記錄都對應一條請求,記錄中一般都包含著這條請求的請求路徑和請求方式。但一條請求不一定只對應一條記錄(中間件、all方法什么的)。

接口實現(xiàn)

我們通過在 app對象 上掛載.get.post這一類的方法來實現(xiàn)路由的注冊。

其中.get方法能匹配請求方式為get的請求,.post方法能匹配請求方式為post的請求。

請求方式一共有33種,每一種都對應一個app下的方法,emmm...我們不可能寫33遍吧?So我們需要利用一個methods包來幫助我們減少代碼的冗余。

const methods = require('methods');
// 這個包是http.METHODS的封裝,區(qū)別在于原生的方法名全文大寫,后者全為小寫。

methods.forEach(method){
    Application.prototype[method] = function(){
        //記錄路由信息到路由系統(tǒng)(router)
        this._router[method].apply(this._router,slice.call(arguments));
        return this; //支持app.get().get().post().listen()連寫
    }
}

//以上代碼是以下的簡寫
Application.prototype.get = fn
Application.prototype.post = fn
...

[info] 可以發(fā)現(xiàn),app.get等只是一個對外接口,實際要做的事情我們都是委托給router這個類來做的。

分發(fā)路由

當請求來臨時我們就需要依據(jù)記錄的路由信息來作出對應的響應了,這個過程我們稱之為分發(fā)路由/dispatch

上面是廣義的分發(fā)路由的含義,但其實分發(fā)路由其實包括兩個過程,匹配路由分發(fā)路由。

  • 匹配路由
    當一個請求來臨時,我們需要知道我們所記錄的路由信息中是否囊括這條請求。(如果沒有囊括,一般來說服務(wù)器會對客戶端作出一個提示性的回應)
  • 分發(fā)路由
    當路由匹配上,則會執(zhí)行被匹配上的路由信息中所存儲的回調(diào)。

接口實現(xiàn)

Application.prototype.listen = function(){
    let self = this;
   
    let server = http.createServer(function(req,res){
        function done(){ //沒有匹配上路由時的回調(diào)
            res.end(`Cannot ${req.method} ${req.url}`);
        }
        //將路由匹配的具體處理交給路由系統(tǒng)的handle方法
        //handle方法中會對匹配上的路由再進行路由分發(fā)
        self._router.handle(req,res,done); 
    })
    server.listen.apply(server,arguments);
}

router

測試用例1與功能分析

const express = require('../lib/express');
const app = express();

app
  .get('/hello',function(req,res,next){
    res.write('hello,');
    next(); 
  },function(req,res,next){
    res.write('world');
    next();
  })
  .get('/other',function(req,res,next){
    console.log('不會走這里');
    next();
  })
  .get('/hello',function(req,res,next){
    res.end('!');
  })
.listen(8080,function(){
  let tip = `server is running at 8080`;
  console.log(tip);
});

<<< 輸出
hello,world!

相較于之前簡單版的express實現(xiàn),完整的express還支持同一條路由同時添加多個cb,以及分開對同一條路由添加cb。

這是怎么辦到的呢?

最主要的是,我們存儲路由信息時,將路由方法組織成了一種類似于二維數(shù)組的二維數(shù)據(jù)形式

即在router(路由容器)里存放一層層route,而又在每一層route(路由)里再存放一層層callbcak。

這樣我們通過遍歷router中的route,匹配上一個route后,就能在這個route下找到所這個route注冊的callbacks。

功能實現(xiàn)

router和route

router(路由容器)里存放一層層route,而又在每一層route(路由)里再存放一層層callbcak。

首先我們需要在有兩個構(gòu)造函數(shù)來生產(chǎn)我們需要的router和route對象。

//router/index.js
function Router(){
    this.stack = [];
}
//router/route.js
function Route(path){
    this.path = path;
    this.stack = [];
    this.methods = {};
}

接著,我們在Router和Route中生產(chǎn)出的對象下都開辟了一個stack,這個stack用來存放一層層的層/layer。這個layer(層),在Router和Route中所存放的東東是不一樣的,在router中存放的是一個層層的route(即Router的實例),而route中存放的是一層層的方法。

它們各自的stack里存放的對象大概是長這樣的

//router.stack
[
    {
        path
        handler
    }
    ,{
        ...
    }
]

//route.stack
[
    {
        handler 
    }
    ,{
        ...
    }
]

可以發(fā)現(xiàn),這兩種stack里存放的對象都包含handler,并且第一種還包含一個path。

第一種包含path,這是因為在router.stack遍歷時是匹配路由,這就需要比對path。

而兩種都需要有一個handler屬性是為什么呢?

我們很容易理解第二個stack,route.stack里存放的就是我們設(shè)計時準備要存放的callbacks那第一個stack里的handler存放的是什么呢?

當我們路由匹配成功時,我們需要接著遍歷這個路由,這個route,這就意味著我們需要個鉤子在我們路由匹配成功時執(zhí)行這個操作,這個遍歷route.stack的鉤子就是第一個stack里對象所存放的handler(即是下文中的route.dispatch方法)。

layer

實際項目中我們將router.stackroute.stack里存放的對象們封裝成了同一種對象形式——layer

一方面是為了語義化,一方面是為了把對layer對象(原本的routes對象和methods對象)進行操作的方法都歸納到layer對象下,以便維護。

// router/layer.js
function Layer(path,handler){
    this.path = path;  //如果這一層代表的存放的callbcak,這為任意路徑即可
    this.handler =handler;
}
//路由匹配時,看路徑是否匹配得上
Layer.prototype.match = function(path){
    return this.path === path?true:false;
}

注冊路由

//在router中注冊route

http.METHODS.forEach(METHOD){
    let method = METHOD.toLowercase();
    Router.prototype[method] = function(path){
        let route = this.route(path); //在router.stack里存儲一層層route
        route[method].apply(route,slice.call(arguments,1)); //在route.stack里存儲一層層callbcak
    }
}

Router.prototype.route = function(path){
    let route = new Route(path);
    let layer = new Layer(path,route.dispatch.bind(route)); //注冊路由分發(fā)函數(shù),用以在路由匹配成功時遍歷route.stack
    layer.route = route; //用以區(qū)分路由和中間件
    this.stack.push(layer);
    
    return route;
}
//在route中注冊callback

http.METHODS.forEach(METHOD){
    let method = METHOD.toLowercase();
    Route.prototype[method] = function(){
        let handlers = slice.call(arguments);
        this.methods[method] = true; //用以快速匹配
        for(let i=0;i<handlers.length;++i){
            let layer = new Layer('/',handler[i]);
            layer.method = method; //在遍歷route中的callbacks依據(jù)請求方法進行篩選
            this.stack.push(layer);
        }
        return this; //為了支持app.route(path).get().post()...
    }
}
注冊流程圖
image

路由分發(fā)

整個路由分發(fā)就是遍歷我們之前用router.stackroute.stack所組成的二維數(shù)據(jù)結(jié)構(gòu)的過程。

我們將遍歷router.stack的過程稱之為匹配路由,將遍歷route.stack的過程稱之為路由分發(fā)。

匹配路由:

// router/index.js

Router.prototype.handle = function(req,res,done){
    let self = this,i = 0,{pathname} = url.parse(req.url,true);
    function next(err){ //err主要用于錯誤中間件 下一章再講
        if(i>=self.stack.length){
            return done;
        }
        let layer = self.stack[i++];
        if(layer.match(pathname)){ //說明路徑匹配成功
            if(layer.route){ //說明是路由
                if(layer.route.handle_method){ //快速匹配成功,說明route.stack里存放有對應請求類型的callbcak
                    layer.handle_request(req,res,next);
                }else{
                    next(err);
                }
            }else{ //說明是中間件
                //下一章講先跳過
                next(err);
            }
        }else{
            next(err);
        }
    }
    next();
}

路由分發(fā)

上面在我們最終匹配路由成功時,會執(zhí)行layer.handle_request方法

// layer.js中

Layer.prototype.handle_request = function(req,res,next){
    this.handler(req,res,next);
}

此時的handler為route.dispatch (忘記的同學可以往上查看注冊路由部分)

//route.js中

Route.prototype.dispatch = function(req,res,out){ //注意這個out接收的是遍歷route.stack時的next()
    let self = this,i =0;
    
    function next(err){
        if(err){ //說明回調(diào)執(zhí)行錯誤,跳過當前route.stack的遍歷交給錯誤中間件來處理
            return out(err);
        }
        if(i>=self.stack.length){
            return out(err); //說明當前route.stack遍歷完成,繼續(xù)遍歷router.stack,進行下一條路由的匹配
        }
        let layer = self.stack[i++];
        if(layer.method === req.method){
            self.handle_request();
        }else{
            next(err);
        }
    }
    next();
}
分發(fā)流程圖
image

測試用例2與功能分析

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

app
  .route('/user')
  .get(function(req,res){
    res.end('get');
  })
  .post(function(req,res){
    res.end('post');
  })
  .put(function(req,res){
    res.end('put');
  })
  .delete(function(req,res){
    res.end('delete');
  })
.listen(3000);

以上是一種resful風格的借口寫法,如果理清了我們上面的東東,其實這個實現(xiàn)起來相當簡單。

無非就是在調(diào)用.route()方法的時候返回我們的route(route.stack里的一層),這樣再調(diào)用.get等其實就是調(diào)用Route.prototype.get等了,就能夠順利往這一層的route里添加不同的callbcak了。

功能實現(xiàn)

//application.js中

Application.prototype.route = function(){
    this.lazyrouter();
    let route = this._router.route(path);
    return route;
}

另外要注意的是,需要讓 route.prototype[method] 返回route以便連續(xù)調(diào)用。

So easy~

Q

為什么選用next遞歸遍歷 而不 選用for?

emmm...我想說express源碼是這么設(shè)計的,嗯,這個答案好不好??(′???`?)

其實可以用for的哦,我有試過的啦,

修改router/index.js 下的 handle方法如下

 let self = this
    ,{pathname} = url.parse(req.url,true);

  for(let i=0;i<self.stack.length;++i){
    if(i>=self.stack.length){
      return done();
    }
    let layer = self.stack[i];
    if(layer.match(pathname)){
      if(!layer.route){

      }else{

        if(layer.route&&layer.route.handle_method(req.method)){
          // let flag = layer.handle_request(req,res);

          for(let j=0;j<layer.route.stack.length;++j){
            let handleLayer = layer.route.stack[j];
            if(handleLayer.method === req.method.toLowerCase()){
              handleLayer.handle_request(req,res);
              if(handleLayer.stop){
                return;
              }
            }
          }//遍歷handleLayer

        }//快速匹配成功

      }//說明是路由

    }//匹配路徑
  }

我們調(diào)用.get等方法時就不再需要傳遞next和傳入next參數(shù)

app
  .get('/hello',function(req,res){
    res.write('hello,');
    // this.stop = true;
    this.error = true; //交給錯誤處理中間件來處理。。 中間件還沒實現(xiàn),但原則上來說是能行的
    // next(); 
  },function(req,res,next){
    res.write('world');
    this.stop = true; //看這里!!!!!!!!!!!!layer遍歷將在這里結(jié)束
    // next();
  })
  .get('/other',function(req,res){
    console.log('不會走這里');
    // next();
  })
  .get('/hello',function(req,res){
    res.end('!');   //不會執(zhí)行,在上面已經(jīng)結(jié)束了
  })
.listen(8080,function(){
  let tip = `server is running at 8080`;
  console.log(tip);
});

在上面這段代碼中this.stop=true的作用就相當于不調(diào)用next(),而不在回調(diào)身上掛載this.stop時就相當于調(diào)用了next()。

原理很簡單,就是在遍歷每一層route.stack時(注意是route的stack不是router的stack),檢查layer.handler是否設(shè)置了stop,如果設(shè)置了就停止遍歷,不論是路由layer(router.stack)的遍歷還是callbacks layer(route.stack)的遍歷。

那么問題來了,有什么理由非要用next來遍歷嗎?

答案是:

for無法支持異步,而next能!這里的支持異步是指,當一個callbcak執(zhí)行后需要拿到它的異步結(jié)果在下一個callbcak執(zhí)行時用到。嗯...for就干不成這事了,for無法感知它執(zhí)行的函數(shù)中是否調(diào)用了異步函數(shù),也不知道這些異步函數(shù)什么能執(zhí)行完畢。

我們從Express的路由系統(tǒng)設(shè)計中能學到什么?

emmm...私認為layer這個抽象還是不錯的,把對每一層(不關(guān)心它具體是route還是callback)的層級相關(guān)操作都封裝掛載到這個對象下,嗯。。?;仡櫫艘幌骂愓Q生的初衷~

當然next這種鉤子式遞歸遍歷也是可以的,我們知道了它的應用場景,支持異步~

emmm...學到什么...我們不僅要模仿寫一個框架,更重要的是,嗯..要思考!要思考!同學們,學到了個什么,要學以致用...嗯...嘿哈!

所以我半夜還在碼這篇文章到底學到了個蝦??emmm...

世界那么大——

源碼

//express.js

const Application= require('./application.js');
const Router = require('./router');

function createApplication(){
  return new Application;
}

createApplication.Router = Router;

module.exports = createApplication;
//application.js
const http = require('http');
const url = require('url');
const Router = require('./router');

function Application(){

}

Application.prototype.lazyrouter = function(){
  if(!this._router){
    this._router= new Router();
  }
};


http.METHODS.forEach(function(METHOD){
  let method = METHOD.toLowerCase();
  Application.prototype[method] = function(){
    this.lazyrouter();
    this._router[method].apply(this._router,arguments);
    return this;
  }
});

Application.prototype.listen = function(){
  let self = this;
  let server = http.createServer(function(req,res){
    function done(){
      let tip = `Cannot ${req.method} ${req.url}`;
      res.end(tip);
    }
    self._router.handle(req,res,done);
  });

  server.listen.apply(server,arguments);
};

module.exports = Application;
//router/index.js
//這一部分兼容了一些后一章要將的內(nèi)容

let http = require('http');
const Route = require('./route.js');
const Layer = require('./layer.js');
const slice = Array.prototype.slice;
const url = require('url');

function Router(){
  function router(){
    router.handle(req,res,next);
  }
  Object.setPrototypeOf(router,proto);
  router.stack = [];
  router.paramCallbacks = [];
  return router;
}

let proto = Object.create(null);

proto.route = function(path){
  let route = new Route(path)
    ,layer = new Layer(path,route.dispatch.bind(route));
  layer.route = route;
  this.stack.push(layer);

  return route;
};

http.METHODS.forEach(function(METHOD){
  let method = METHOD.toLowerCase();
  proto[method] = function(path){
    let route = this.route(path); //注冊路由層
    route[method].apply(route,slice.call(arguments,1)); //注冊路由層的層
  }
});

proto.handle = function(req,res,done){
  let index = 0,self = this
    ,removed
    ,{pathname} = url.parse(req.url,true);
  function next(err){
    if(index>=self.stack.length){
      return done();
    }
    if(removed){
      req.url = removed+req.url;
      removed = '';
    }
    let layer = self.stack[index++];

    if(layer.match(pathname)){
      if(!layer.route){

      } else{
        if(layer.route&&layer.route.handle_method(req.method)){
          layer.handle_request(req,res,next);
        }else{
          next(err);
        }
      }
    }else{
      next(err);
    }
  }

  next();
};

module.exports = Router;
// router/route.js
let http = require('http');
let Layer = require('./layer.js');
let slice = Array.prototype.slice;

function Route(path){
  this.path = path;
  this.methods = {};
  this.stack = [];
}

http.METHODS.forEach(function(METHOD){
  let method = METHOD.toLowerCase();
  Route.prototype[method] = function(){
    let handlers = slice.call(arguments);
    this.methods[method] = true;

    for(let i=0;i<handlers.length;++i){
      let layer = new Layer('/',handlers[i]);
      layer.method = method;
      this.stack.push(layer);

    }
    return this;
  }
});

Route.prototype.handle_method = function(method){
  return this.methods[method.toLowerCase()]?true:false;
};

Route.prototype.dispatch = function(req,res,out){
  let self = this
    ,index = 0;
  // let q = 0
  function next(err){
    if(err){
      return out(err); //出現(xiàn)錯誤,退出當前路由交給錯誤中間件處理
    }

    if(index>=self.stack.length){
      return out(); //當前路由的layer已經(jīng)遍歷完 跳出 繼續(xù)匹配下一條路由
    }

    let layer = self.stack[index++];

    if(layer.method === req.method.toLowerCase()){
      layer.handle_request(req,res,next);
    }else{
      next(err);
    }
  }
  next();
};

module.exports = Route;
// router/layer.js
function Layer(path,handler){
  this.path = path;
  this.handler = handler;
}

Layer.prototype.match = function(path){
  return path === this.path?true:false;
};

Layer.prototype.handle_request = function(req,res,next){
  this.handler(req,res,next);
};

Layer.prototype.handle_error = function(err,req,res,next){
  if(this.handler.length !=4){
    return next(err);
  }
  this.handler(err,req,res,next);
};

module.exports = Layer;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容