介紹

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應用應該是怎麼樣

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

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

//========== 你的應用 app.js ==================
const http = require(http)
const app = express()

app.get(/, (req, res) => res.send(Hello World!))

const server = http.createServer(app)
server.listen(8000);

//========== express.js =============
var connect = require(connect)

function createApplication() {
var app = connect();
utils.merge(app, proto);
app.request = { __proto__: req };
app.response = { __proto__: res };
app.init();
return app;
}

module.exports = createApplication;

//=========== express依賴的connect.js==============
function createServer() {
function app(req, res, next){ app.handle(req, res, next); }
// ... 省略
return app;
}

module.exports = createServer;

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

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

const app = express();

// nodejs啟動時,app函數內部被express增加了能力,如中間件的調用
app.use(middleware) // 中間件
app.use(router) // 路由
app.engine(ejs); // 模板引擎
app.statifc(public) // 靜態文件服務
// ... 還有代理以及其他許多屬性與方法

const server = http.createServer(
function app(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 的簡要內容

function createServer(){

// app是用於http.createServer的回調函數
function app(req, res, next){

// 運行時調用handle函數
app.handle(req, res, next);
}

mixin(app, proto, false);

// 初始化一個stack數組
app.stack = [];
return app;
}

// use調用時往app的stack數組中push一個對象(中間件),標識path與回調函數
proto.use = function(route, fn){
var path = route,
handle = fn;

//... 省略其他

this.stack.push({
route: path,
handle
});
};

// handle方法,串列取出stack數組中的中間件,逐個運行
proto.handle = function(req, res, out){
var index = 0;
var stack = this.stack;
var done = out || finalhandler(req, res, { onerror: logerror });

// 遍歷stack,逐個取出中間件運行
function next(err){
var layer = stack[index++];
// 遍歷完成為止
if(layer === undefined){
return done();
}

var route = pathFormat(layer.route);
var pathname = 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,則流程結束。

接下來提一下非同步編程的串列控制,加強理解;

非同步串列流程式控制制

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

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

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

// 數組
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 = new Router();
this.usedRouter = false;

// app調用router時初始化router中間件
Object.defineProperty(this, router, {
configurable : true,
enumerable : true,
get: function () {
this.usedRouter = true;
return this._router.middlewareInit.bind(this._router);
}
})
};

// methods是一個數組,[get,post,put,delete,...]
methods.forEach(method => {
app[method] = function (path) {
// 如果首次調用則放入路由中間價
if(!this.usedRouter){
this.use(this.router);
}

// 加入stack
this._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){
let layer = {
path,
handles
};
this.map[method] = this.map[method] || [];
this.map[method].push(layer);
};

Router.prototype.middlewareInit = function(req, res, out){

let index = 0;
let method = req.method.toLowerCase() || get;
let stack = this.map[method];

function next(err) {
let layer = stack[index++];
let hasError = Boolean(err);

// 如果沒有了則結束中間件,走下一個中間件
if(!layer){
return hasError ? out(err) : out();
}

let route = utils.pathFormat(layer.path);
let pathname = utils.pathFormat(urlParser(req.url).pathname || /);

// 進行過濾
if(route!== && route !== pathname){
return next(err);
}

executeHandles(layer.handles, err, req, res, next);
}

next();
};

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

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

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

視圖-模板引擎

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

var cons = require(consolidate)
, name = swig;

cons[name](views/page.html, { user: tobi }, function(err, html){
if (err) throw err;
console.log(html);
});

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

// express設置屬性
app.set = function(key, value){
if(this.settings.hasOwnProperty(key)){
return this.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) {

let cacheTemplate = this.cache[name];

let view = cacheTemplate || new View(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 簡化

function View(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(模板沒有找到);
throw new Error(模板沒有找到);
}
};

View.prototype.render = function (options, fn) {
let templatePath= this.templatePath;
// 調用模板引擎完成渲染
return cons[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做一個類似的封裝實現。有興趣的可以繼續關注我的博文


推薦閱讀:
相關文章