編寫可維護代碼之「中間件模式」

引言

此次我們談論的中間件,針對前端開發而言。對於嚴格意義上的中間件(平台與應用之間的通用服務),例如用於緩解後台高訪問量的消息中間件,本篇不會去敘述,因為不是本篇的論述意圖。

言歸正傳,當我們在編寫業務代碼時候,我們無法避免有些業務邏輯複雜而導致業務代碼寫得又長又亂,如果再加上時間緊湊情況下寫出來的代碼估計會更讓人抓狂。以至於我們一直在尋求更好的架構設計和更好的代碼設計,這是一個沒有終點的求知之路,但是在這條路上會越走越好。

1. AOP

AOP意為面向切面編程,是在Java的Spring框架的重點內容,其作用如下圖所示:

根據上圖,整個響應http過程可以看做是一條串聯的管道,對於每個http請求我們都想插入相同的邏輯例如數據過濾、日誌統計的目的,為了不和業務邏輯混淆一塊,提高代碼復用率,AOP提倡從橫向切面思路向管道某個位置插入一段代碼邏輯這樣就實現在任何業務邏輯前後都有相同代碼邏輯段,開發者只需專註寫業務邏輯,既不影響整個響應http過程,而且隔離了業務邏輯,實現高內聚低耦合原則。

可以說AOP對OOP進行了一個補充,OOP是對做同一件事情的業務邏輯封裝成一個對象,但是做一件事情過程中又想做別的事情對OOP來說難以解決。就像上圖所示,當系統在響應用戶修改信息的請求時,系統在業務處理之前對用戶提交的數據做了安全過濾,業務處理之後還要做日誌統計。相反如果把所有邏輯都柔合在一起,每次寫業務都需重複編寫數據過濾和日誌統計的代碼,違反了單一職責,高內聚低耦合的原則,並且降低代碼復用率。

在前端,我們可以借用這種思想通過before和after函數來實現,我們看下代碼實現:

Function.prototype.before = function(fn){//函數處理前執行fn var self = this; return function(){ fn.call(this); self.apply(this, arguments); }}

Function.prototype.after = function(fn){//函數處理後執行fn var self = this; return function(){ self.apply(this, arguments); fn.call(this); }}

實現思路是對被處理的函數通過閉包封裝在新的函數里,在新的函數內部按照順序執行傳入的參數fn和被處理的函數。

舉個栗子:

用戶提交表單數據之前需要用戶行為統計,代碼應該是這樣寫:

function report(){ console.log("上報數據");}function submit(){ console.log("提交數據");}submit.before(report)(); //提交之前執行report//結果: 上報數據// 提交數據

從代碼可以看出已經把統計和數據提交業務隔離起來,互不影響。

但是如果提交數據之前,需要數據驗證並且依據驗證結果判斷是否能提交,怎麼做?這裡要改動before函數,看下代碼:

Function.prototype.before = function(fn){//函數處理後執行fn var self = this; return function(){ var res = fn.call(this); if(res)//返回成功則執行函數 self.apply(this, arguments); }}

function report(){ console.log("上報數據"); return true;}function validate(){ console.log("驗證不通過"); return false;}function submit(){ console.log("提交數據");}submit.before(report).before(validate)();//結果: // 驗證不通過

function report(){ console.log("上報數據"); return true;}function validate(){ console.log("驗證通過"); return true;}function submit(){ console.log("提交數據");}submit.before(report).before(validate)();//結果: // 驗證通過// 上報數據// 提交數據

AOP思想在前端分解隔離業務已經做到位了,但是卻有了一串長長的鏈式出來,如果處理不當很容易讓維護者看暈,例如下面這樣:

//提交數據前,驗證數據,然後上報,在提交之後做返回首頁的跳轉function report(){ console.log("上報數據"); return true;}function validate(){ console.log("驗證通過"); return true;}function submit(){ console.log("提交數據");}function goBack(){ console.log("返回首頁")}submit.before(report).before(validate).after(goBack)();//結果: // 驗證通過// 上報數據// 提交數據

栗子可能並沒有那麼暈,但是也得仔細看才能看懂整個流程,實際開發中估計會有更麻煩情況出現,另外,如果before或after的參數fn是一個非同步操作的話,又需要做些patch,顯然還是有些不足的,那麼還有沒有其他解決辦法呢,既能隔離業務,又能方便清爽地使用~我們可以先看看其他框架的中間件解決方案。

2. express 與 koa的中間件

express和koa本身都是非常輕量的框架,express是集合路由和其他幾個中間件合成的web開發框架,koa是express原班人馬重新打造一個更輕量的框架,所以koa已經被剝離所有中間件,甚至連router中間件也被抽離出來,任由用戶自行添加第三方中間件。express和koa中間件原理一樣,我們就抽express來講。

我們先看下express中間件寫法:

var express = require("express");var app = express(); app.use(function(req, res, next) { console.log("數據統計"); next();//執行權利傳遞給});app.use(function(req, res, next) { console.log("日誌統計"); next();});app.get("/", function(req, res, next) { res.send("Hello World!");});app.listen(3000);//整個請求處理過程就是先數據統計、日誌統計,最後返回一個Hello World!

上圖運作流程圖如下:

從上圖來看,每一個「管道」都是一個中間件,每個中間件通過next方法傳遞執行權給下一個中間件,express就是一個收集並調用各種中間件的容器

中間件就是一個函數,通過express的use方法接收中間件,每個中間件有express傳入的req,res和next參數。如果要把請求傳遞給下一個中間件必須使用 next() 方法。當調用res.send方法則此次請求結束,node直接返回請求給客戶,但是若在res.send方法之後調用next方法,整個中間件鏈式調用還會往下執行,因為當前hello world所處的函數也是一塊中間件,而res.send只是一個方法用於返回請求

3. 借用中間件

我們可以借用中間件思想來分解我們的前端業務邏輯,通過next方法層層傳遞給下一個業務。做到這幾點首先必須有個管理中間件的對象,我們先創建一個名為Middleware的對象:

function Middleware(){ this.cache = [];}

Middleware通過數組緩存中間件。下面是next和use方法:

Middleware.prototype.use = function(fn){ if(typeof fn !== "function"){ throw "middleware must be a function"; } this.cache.push(fn); return this;}Middleware.prototype.next = function(fn){ if(this.middlewares && this.middlewares.length > 0 ){ var ware = this.middlewares.shift(); ware.call(this, this.next.bind(this)); }}Middleware.prototype.handleRequest = function(){//執行請求 this.middlewares = this.cache.map(function(fn){//複製 return fn; }); this.next();}

我們用Middleware簡單使用一下:

var middleware = new Middleware();middleware.use(function(next){console.log(1);next();})middleware.use(function(next){console.log(2);next();})middleware.use(function(next){console.log(3);})middleware.use(function(next){console.log(4);next();})middleware.handleRequest();//輸出結果: //1//2//3//

4沒有出來是因為上一層中間件沒有調用next方法,我們升級一下Middleware高級使用

var middleware = new Middleware();middleware.use(function(next){ console.log(1);next();console.log("1結束");});middleware.use(function(next){ console.log(2);next();console.log("2結束");});middleware.use(function(next){ console.log(3);console.log("3結束");});middleware.use(function(next){ console.log(4);next();console.log("4結束");});middleware.handleRequest();//輸出結果: //1//2//3//3結束//2結束//1結束

上面代碼的流程圖:

可以看出:每一個中間件執行權利傳遞給下一個中間件並等待其結束以後又回到當前並做別的事情,方法非常巧妙,有這特性讀者可以玩轉中間件

4. 實際應用

/*** @param data 驗證的數據* @param next */function validate(data, next){ console.log("validate", data);//驗證 next();//通過驗證}/*** @param data 發送的數據* @param next */function send(data, next){ setTimeout(function(){//模擬非同步 console.log("send", data);//已發送數據 next(); }, 100);}function goTo(url, next){ console.log("goTo", url);//跳轉}

validate和send函數都需要數據參數,目前Middleware只傳next,需要傳遞data數據才能順利執行下去,然而每個中間件需要的數據不一定都一致(就像goTo與validate、send)。

我們需要引入一個options對象來包裹這一串邏輯需要的數據,每個中間件在options內提取自己所需的數據,這樣就能滿足所有中間件,Middleware函數做相應調整:

function Middleware(){ this.cache = []; this.options = null;//緩存options}Middleware.prototype.use = function(fn){ if(typeof fn !== "function"){ throw "middleware must be a function"; } this.cache.push(fn); return this;}Middleware.prototype.next = function(fn){ if(this.middlewares && this.middlewares.length > 0 ){ var ware = this.middlewares.shift(); ware.call(this, this.options, this.next.bind(this));//傳入options與next }}/*** @param options 數據的入口* @param next */Middleware.prototype.handleRequest = function(options){ this.middlewares = this.cache.map(function(fn){//複製 return fn; }); this.options = options;//緩存數據 this.next();}

業務邏輯做相應修改:

function validate(options, next){ console.log("validate", options.data); next();//通過驗證}function send(options, next){ setTimeout(function(){//模擬非同步 console.log("send", options.data); options.url = "www.baidu.com";//設置跳轉的url next(); }, 100);}function goTo(options){ console.log("goTo", options.url);}var submitForm = new Middleware();submitForm.use(validate).use(send).use(goBack);submitForm.handleRequest({data:{name:"xiaoxiong", age: 20}});//結果:// validate Object {name: "xiaoxiong", age: 20}//// send Object {name: "xiaoxiong", age: 20}// goTo www.baidu.comsubmitForm.handleRequest({data:{name:"xiaohong", age: 21}});//觸發第二次,改變數據內容//結果:// validate Object {name: "xiaohong", age: 21}//// send Object {name: "xiaohong", age: 21}// goTo www.baidu.com

以上代碼大功告成。

5. 總結

通過以上代碼,實現了業務隔離,滿足每個業務所需的數據,又能很好控制業務下發執行的權利,所以「中間件」模式算是一種不錯的設計。從代碼閱讀和代碼編寫的角度來說難度並不大,只要維護人員擁有該方面的知識,問題就不大了。

這裡是完整代碼:humyfred/js_demo_and_blog


推薦閱讀:

一種Python全局配置規範以及其魔改
【遊戲設計模式】之三 狀態模式、有限狀態機 & Unity版本實現
「小白DAY4」這樣你就懂了,談CSS設計模式
自己實現的觀察者模式、BroadcastReceiver和EventBus三者的優缺點是什麼?
MVC 架構與 Observer 模式有什麼異同點?

TAG:JavaScript | 前端开发 | 设计模式 |