NodeJS express框架核心原理全揭秘

blank

NodeJS express框架核心原理全揭秘

2019-07-26 號外:近期我開發了一個簡化redux開發的框架booto,大家有興趣可以開參觀下

介紹

express框架大家都已經都很熟悉,是NodeJS最流行的輕量web開發框架。他簡單易用,卻功能強大。最近一個月來一直反复研究調試express框架,深究其源碼不覺為之驚嘆,不論是原理與代碼都非常簡單,很容易理解也很受用,覺得有必要寫個文章分享一下。本系列分2部分全面介紹express。上篇講express框架主要原理和重要的組成部分,下篇是利用這些原理從零開發一個express框架(覆蓋主要功能);一篇理論一篇實戰演練配合完全深入掌握express原理。

由於一些原因本文基於express 3.x版本,但與express4.x差別不大;(express4.x自己實現了connect組件,增加了proxy等)。

適合讀者

  • 有較紮實的JavaScript的基礎
  • 了解NodeJS的http、fs、path等模塊
  • 了解express

express框架提供的能力

可以在express的官網中看到express具備中間件的使用、路由、模板引擎、靜態文件服務、設置代理等主要能力。後面將逐一講解其實現。

本文將主要涵蓋以下內容

  • NodeJS的http模塊創建的服務
  • express中間件思想的本質- 異步串行化流程控制
  • express的router實現原理
  • 模板引擎
  • 靜態文件服務

讀者閱讀本文可以配合這份稍微做了簡化版的express進行運行與調試,方便理解;

express構造的是Http.createServer的回調函數

express是一個基於NodeJS的框架,先來看下如果不使用框架要創建一個最簡單的web應用應該是怎麼樣

consthttp=require('http');constserver=http.createServer(function(req,res){res.end('hello word!')});server.listen(8000);

實際上express是一個函數,運行後可以構造出上面代碼中http.createServer的回調函數,express做的一切文章都是在這個回調函數上。來看下express3.x的源碼express.js

//==========你的應用app.js ==================consthttp=require('http')constapp=express()app.get('/',(req,res)=>res.send('Hello World!'))constserver=http.createServer(app)server.listen(8000);//========== express.js =============varconnect=require('connect')functioncreateApplication(){varapp=connect();utils.merge(app,proto);app.request={__proto__:req};app.response={__proto__:res};app.init();returnapp;}module.exports=createApplication;//=========== express依賴的connect.js==============functioncreateServer(){functionapp(req,res,next){app.handle(req,res,next);}// ...省略returnapp;}module.exports=createServer;

connect.js的具體內容先不關心,後面會重點介紹。可以看出connect是一個函數,運行返回一個app,app是一個形如function(req, res , next){ ... } 的函數。 express的createApplication返回即是此app,用於http.createServer的回調。並在這個函數上混入的許多能力,如req、res的處理、模板引擎、靜態文件服務、router的能力。

blank

用比較簡單的偽代碼表示如下

constapp=express();// nodejs啟動時,app函數內部被express增加了能力,如中間件的調用app.use(middleware)//中間件app.use(router)//路由app.engine('ejs');//模板引擎app.statifc('public')//靜態文件服務// ...還有代理以及其他許多屬性與方法constserver=http.createServer(functionapp(req,res){//此app函數即為express所構造// http請求時,req, res被混入許多屬性與方法,做了很多處理//串行匹配運行按順序註冊的各註冊的中間件如:// 1、日誌、cookie、bodyparser等開發者自己註冊的中間件// 2、router中間件// 3、靜態文件服務// 4、模板引擎處理//經過匹配的中間件處理後輸出返回});server.listen(8000);

上面的1、2、3、4順序即為開發者註冊時的順序(故我們平時在開發時express註冊中間件時是有先後順序的)。 express最主管理與運行中間件的能力,接下來深入內部看看connect這個中間件機制是怎麼實現的。

最為核心的中間件框架

//connect.js的簡要內容functioncreateServer(){// app是用於http.createServer的回調函數functionapp(req,res,next){//運行時調用handle函數app.handle(req,res,next);}mixin(app,proto,false);//初始化一個stack數組app.stack=[];returnapp;}// use調用時往app的stack數組中push一個對象(中間件),標識path與回調函數proto.use=function(route,fn){varpath=route,handle=fn;//...省略其他this.stack.push({route:path,handle});};// handle方法,串行取出stack數組中的中間件,逐個運行proto.handle=function(req,res,out){varindex=0;varstack=this.stack;vardone=out||finalhandler(req,res,{onerror:logerror});//遍歷stack,逐個取出中間件運行functionnext(err){varlayer=stack[index++];//遍歷完成為止if(layer===undefined){returndone();}varroute=pathFormat(layer.route);varpathname=pathFormat(urlParser(req.url).pathname||'/');//匹配中間件,不匹配的不運行if(route!==''&&pathname!==route){next(err);return;}//調用中間件call(layer.handle,err,req,res,next);}next();};

不難看出,app.use中間件時,只是把它放入一個數組中。當http請求時,app會從數組中逐個取出,進行匹配過濾,逐個運行。遍歷完成後,運行finalhandler,結束一個http請求。可以從http請求的角度思考,一次請求它經歷經歷了多少東西。 express的這個中間件架構就是負責管理與調用這些註冊的中間件。中間件順序執行,通過next來繼續下一個,一旦沒有繼續next,則流程結束。

接下來提一下異步編程的串行控制,加強理解;

異步串行流程控制

為了用串行化流程控制讓幾個異步任務按順序執行,需要先把這些任務按預期的執行順序放到一個數組中。如圖,所示,這個數組將起到隊列的作用:完成一個任務後按順序從數組中取出下一個

blank

數組中的每個任務都是一個函數。任務完成後應該調用一個處理器函數,告訴它錯誤狀態和結果。如果有錯誤,處理器函數會終止執行;如果沒有錯誤,處理器就從隊列中取出下一個任務執行它

下面是一個簡單實現方案:

// 数组var tasks = [ function A (){ //... next (); }, function B (){ //... next () }, function C (){ //... next () } //... ]; function next ( err , result ){ if ( err ) throw err ; var currentTask = tasks . shift (); if ( currentTask ) currentTask ( result ) next (); } // 首次主动调用next ();

異步串行控制方案除了上面的這種以外,還可以用es6的promise的then鏈、async/await、yeild、社區工具等;

可以看到代碼確實談不上高級,串行導致的性能談不上優秀,但是得益於此它足夠簡單易用。到此可以發現express的中間件架構就是一個中間件的的管理與數組遍歷運行,這個方案就讓社區形形色色各種各樣的中間件很好的添加express能力,這點很簡單也很重要,因為後續的路由、靜態文件服務、代理等都是中間件,都在這個框架內運行。

Router是一個內置在app函數上的中間件

來看下簡化後的router.js

//express創建時運行app.init=function(){// ...省略其它代碼this._router=newRouter();this.usedRouter=false;// app調用router時初始化router中間件Object.defineProperty(this,'router',{configurable:true,enumerable:true,get:function(){this.usedRouter=true;returnthis._router.middlewareInit.bind(this._router);}})};// methods是一個數組,['get','post','put','delete',...]methods.forEach(method=>{app[method]=function(path){//如果首次調用則放入路由中間價if(!this.usedRouter){this.use(this.router);}//加入stackthis._router.addRoute(method,path,Array.prototype.slice.call(arguments,1))}});

上面的usedRouter是個開關,未開啟則不加入router中間件,因為應用理論上也是可能不用到router的。當app[method] 如app.get('/user', fn)調用後,則觸發this.use(this.router) 使用router中間件,同時把usedRouter設置為true。之後往router對像中加入fn回調函數。

router實際上也是一個異步串行流程控制,簡化版的代碼如下

Router.prototype.addRoute=function(method,path,handles){letlayer={path,handles};this.map[method]=this.map[method]||[];this.map[method].push(layer);};Router.prototype.middlewareInit=function(req,res,out){letindex=0;letmethod=req.method.toLowerCase()||'get';letstack=this.map[method];functionnext(err){letlayer=stack[index++];lethasError=Boolean(err);//如果沒有了則結束中間件,走下一個中間件if(!layer){returnhasError?out(err):out();}letroute=utils.pathFormat(layer.path);letpathname=utils.pathFormat(urlParser(req.url).pathname||'/');//進行過濾if(route!==''&&route!==pathname){returnnext(err);}executeHandles(layer.handles,err,req,res,next);}next();};

router跟connect非常類似,上述理解了connect,router就很清晰了。一圖以蔽之:

blank

實際上router還有細分,某個router還是可以繼續做類似的串行流程控制;與中間件相同,每個router一旦停止了next,流程就結束了。

request經過router可以請求一個數據,或者一個網頁;網頁的話是怎麼返回的呢,接下來看下view的render;

視圖-模板引擎

模板引擎是根據對模板結合data進行運行處理,生產real html;這跟React、Vue、模板引擎是類似的。模板引擎不是express實現的,實際上express僅僅只是做了調用;這裡有個通用的支持各種模板引擎的模塊consolidate.js

varcons=require('consolidate'),name='swig';cons[name]('views/page.html',{user:'tobi'},function(err,html){if(err)throwerr;console.log(html);});

express要做的只是配置與調用;

// express设置属性
app.set=function(key,value){if(this.settings.hasOwnProperty(key)){returnthis.settings[key];}this.settings[key]=value;};app.engine=function(engine){this.settings['engine']=engine;};

通過這兩個函數設置views視圖所在的路徑、模板引擎類型,之後express就可以結合router提供的render page,data,render callback的數據進行視圖渲染

app.render=function(name,options,fn){letcacheTemplate=this.cache[name];letview=cacheTemplate||newView(name,{root:process.cwd(),viewPath:this.settings['views'],engine:this.settings['engine']});if(!cacheTemplate&&this.settings['view cache']){this.cache[name]=view;}view.render(options,fn);};// View.js簡化functionView(page,config){console.log('view初始化');this.engine=config.engine||'ejs';this.templatePath=path.join(config.root,config.viewPath,page);this.lookup();}//檢測模板是否存在View.prototype.lookup=function(){if(!fs.existsSync(this.templatePath)){console.log('模板沒有找到');thrownewError('模板沒有找到');}};View.prototype.render=function(options,fn){lettemplatePath=this.templatePath;//調用模板引擎完成渲染returncons[this.engine](templatePath,options,fn);};

為了性能考慮還做了cache;關於模板引擎,實際上很簡單,讀者可以自定一個模板引擎規則。

靜態文件服務

靜態文件服務也是一個中間件,express做的事情也僅僅是引用。 require一個serve-static,內置在app函數上。

app.static=function(dir){this.use(serveStatic(process.cwd()+'/'+dir),{});};

當調用app.static時就會把靜態文件服務中間件放入stack中,這裡與express調用方式稍有不同,因為筆者覺得這麼寫更好更簡單。

更多的內容

express除了上述的內容外,還做了req,res的擴展。還有許多細節未展開描述。但最核心的內容已經在上面呈現。讀者可以在express的基礎上擴展更多內容加強框架。只需明白一點,express核心主要是一個中間件串行控制方案,內置來router、靜態文件服務中間件、擴展了req,res,其他功能都是集成了其他模塊來加強的;確實是一個簡單易用的web框架。

總結

express我自己實現了一遍,讀者可以自行閱讀express源碼,也可以查看我的express-mini;後續我會對koa、egg等其他框架做一次深入的研究,也會對新的deno做一個類似的封裝實現。有興趣的可以繼續關注我的博文

What do you think?

Written by marketer

blank

前端同構渲染的思考與實踐

blank

RIS,創建React 應用的新選擇