es6時代來了,相信會讓一批有java,C++等面嚮對象語言開發基礎的夥子們,感覺到來自js世界滿滿的善意。es6可讓開發者幾乎擺脫prototype的編程模式,讓開發更加如絲般順滑,雖然目前大部分瀏覽器並無支持es6,可是打雞血般日新月異的node和與時俱進的babel,仍是已經讓大部分前端和node開發者享受到es6時代的酸爽。面向對象有不少精妙的設計思想,雖然說思想 js框架相信你們都用過很多了,前端如redux,後臺框架如express,koa,等等等等,固然還有不少其餘優秀的框架,不過與咱們今天的主題無關就很少說了。若是你們使用過redux或者koa,應該對其中的中間件不會陌生。中間件雖然在不一樣的框架中用法各有不一樣,可是實現原理倒是大致一致的。咱們發現做爲一箇中間件,無論其具體實現的是什麼能力,其實它一個最主要的職能就是加強目標對象的能力。在研究各大中間件的過程當中,隱隱約約看看一個及其熟悉的背影,那就是裝飾模式。在衆多設計模式中,裝飾模式應用最普遍的就是加強目標對象能力,大部分中間件的實現,應該都是借鑑了裝飾模式這種靈活的設計思想。所以,這裏咱們首先來介紹一下 裝飾模式,說到裝飾模式,就不得不先提一下es7提案中新增的註解功能(本人習慣叫註解,由於寫法相似於java中的註解),好比以下一個類,定義了加和減兩個方法:前端
class MyClass {
add(a, b){
return a + b;
}
sub(a, b){
return a - b;
}
}
複製代碼
假如如今有個需求,須要實現每次調用add或者sub函數的時候,都分別打印出方法調用先後的log,好比調用前'before operate',調用後打印'after operate',咱們是否須要在調用先後分別調用console.log(),es7裏面固然沒必要了,咱們只須要定義好咱們須要的打印函數,而後使用@註解,好比以下使用方式:java
//註解的函數定義
let log = (type) => {
const logger = console;
return (target, name, descriptor) => {
const method = descriptor.value;
descriptor.value = (...args) => {
logger.info(`(${type}) before function execute: ${name}(${args}) = ?`);
let ret = method.apply(target, args);
logger.info(`(${type})after function execute: ${name}(${args}) => ${ret}`);
return ret;
}
}
}
//註解調用
class MyClass {
@log("add")
add(a, b){
return a + b;
}
@log("sub")
sub(a, b){
return a - b;
}
}
複製代碼
如上在咱們調用MyClass實例化方法add和sub的時候,分別會打印調用前和調用後的日誌了,這就是在不改動MyClass源碼的狀況下,使用裝飾模式對於原方法add和sub的能力加強,這是es7的語法,定義註解的方式很簡單,一個函數返回另外一個函數,返回函數的參數分別是target:類的上下文,name:目標方法名,descriptor就不用解釋了吧,不理解能夠看看defineProperty的定義,簡單易用,須要加強其餘能力,那就多定義幾個,多@幾下。這是es7的,編譯器支持的仍是看着有點抽象,接下來咱們來看看普通es5對象如何使用裝飾模式進行能力的加強。以下一個add函數node
function add(a, b){
return a + b;
}
複製代碼
如今須要加強log和notify的能力,在調用前打印日誌併發送消息。代碼以下:es6
function logDecorator(target){
var old = target;
return function(){
console.log("log before operate");
var ret = old.apply(null,arguments);
console.log(target.name,"results:",ret,",log after operate");
return ret;
}
}
function notifyDecorator(target){
var old = target;
return function(){
console.log("notify before operate");
var ret = old.apply(null,arguments);
console.log("finished, notify u");
return ret;
}
}
var add = logDecorator(notifyDecorator(add));
複製代碼
稍微解釋一下,var old = target;先將原目標保存,並返回一個函數,在該函數中var ret = old.apply(null,arguments);
執行原目標函數的調用,這時候,或前或後,在須要的節點進行具體的能力加強便可,是否是很失望呢,咋就這麼簡單?很差意思,真就這麼簡單,這就是各大框架中高大上的中間件的基本原理了。以koa舉例,若是咱們須要簡單實現一個log中間件,應該怎麼作呢?express
module.exports = (opts = {}) => {
var log = console.log;
return async (ctx, next) => {
log("before ",ctx.request.url, "...");
await next();
log("after ",ctx.request.url, "...");
}
}
複製代碼
如上代碼就是了,固然,咱們能夠在中間件中作一些過濾條件,好比咱們只但願對非靜態資源的請求進行自定義的log等等。koa以及express做爲一個後臺框架,中間件比較不一樣的地方就在於路由的實現,聽起來彷佛有點複雜哦。其實,以koa爲例,想要實現路由,咱們對ctx.request.url進行字符串分析處理進入不一樣的處理函數,是否就能夠有一個基本的路由功能了呢?因此中間件很強大,其實也很簡單,它並不矛盾。中間件定義完了,接下來看看怎麼用了。
咱們的中間件可能須要十個八個,那這麼多箇中間件們是如何進行compose呢,不一樣框架實現方式可能不太一致,可是原理仍是同一個原理。一批中間件加入以後,存於一個函數列表中,而後對列表中的函數進行順序執行,且每個函數的返回值做爲下一個函數的入參。咱們以koa和redux的中間件爲例來分析一下。首先來看koa的:編程
app.use(中間件);
複製代碼
koa-compose源碼:redux
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
複製代碼
let fn = compose(middlewares);
fn(ctx)...;
複製代碼
首先,使用app.use()加入中間件,使用如上compose函數對中間件middlewares列表進行遞歸調用。具體代碼就不一一解釋了吧,對於熟悉koa以及express的同窗,應該很熟悉next的用法,這其實就是咱們前面的var old = target;
這種方式的升級版本,而且經過next的方式能夠更加優雅地解了中間件新增的問題,而不須要使用嵌套調用的方式。
遞歸遍歷是個思路,其實咱們js原生提供了一種方式進行compose,能夠更加優雅解決這個問題,redux就是採用了這種調用方式,就是使用reduce函數,咱們來看看redux處理方式:設計模式
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製代碼
reduce再加上es6簡直賞心悅目有沒有,若是看的不太舒服能夠轉成es5看看,給你們一個簡單的測試用例跑跑,可能會更加好理解:api
function fun1(obj){
console.log(1);
obj.a=1;
return obj;
}
function fun2(obj){
console.log(2);
obj.b=2;
return obj;
}
let fn = compose(fun1,fun2);
fun({});
複製代碼
看看調用的結果是啥,這只是一個幫助理解的小栗子,栗子雖小,可是已經小秀了一把肌肉了,重點就在於咱們在各個中間件中透傳傳入的這個參數obj了,能夠是個對象,也能夠是個函數,總之是咱們能夠隨心所欲地加強它的能力。
根據不一樣的目的,中間件的實現機制會有一些差別,koa跟redux其實就有比較明顯的一些區別,有興趣能夠深刻去看看,可是萬變不離其宗。
到此,中間件的定義和調用中的一些核心邏輯就講完了,都是我的一些淺見,水平有限,若有謬誤,敬請指出!!!瀏覽器