面向切面編程—koa、redux框架中間件原理解析

做者:曦舒 方凳雅集出品前端

用過express、koa或者redux的同窗應該都知道它們都有「中間件」這樣一個概念(前端意義上的中間件,不是指平臺與應用之間的通用服務),在redux中咱們能夠經過中間件的方式使用redux-thunk和loger的功能,在koa中咱們能夠經過中間件對請求上下文context進行處理。node

經過中間件,咱們能夠在一些方法執行前添加統一的處理(如登陸校驗,打印操做日誌等),中間件的設計思想都是面向切面編程的思想,把一些跟業務無關的邏輯進行抽離,在須要使用的場景中再切入,下降耦合度,提升可重用性,並且使代碼更簡潔。express

下面咱們經過源碼分析redux和koa是如何實現中間件的,最後詳細介紹面向切面編程解決的一些問題。編程

1、redux中間件
咱們先來看下redux中間件的用法。redux暴露了applyMiddleware方法,接受一個函數數組做爲參數,applyMiddleware方法的返回值做爲第二參數傳入createStore方法。redux

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import { createLogger } from 'redux-logger'
import reducer from './reducers'數組

const middleware = [ thunk ]
if (process.env.NODE_ENV !== 'production') {
middleware.push(createLogger())
}瀏覽器

const store = createStore(
reducer,
applyMiddleware(...middleware)
)
接下來看redux中間件是怎麼實現的,下面是applyMiddleware的源碼,其中對dispatch方法進行了compose處理,處理後的dispatch在每次調用時都會鏈式調用中間件函數。服務器

export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {cookie

const store = createStore(...args)
let dispatch = () => {
  throw new Error(
    'Dispatching while constructing your middleware is not allowed. ' +
      'Other middleware would not be applied to this dispatch.'
  )
}

const middlewareAPI = {
  getState: store.getState,
  dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

return {
  ...store,
  dispatch
}

}
}
compose的源碼以下,其主要做用是依次調用函數數組中的中間件函數,並將前一個函數的返回結果做爲後一個函數的入參,從而實現了在調用dispath時調用其餘方法的需求。app

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)))
}
2、koa中間件
一樣的,咱們來看下koa中間件的用法。首先實例化一個Koa對象,而後經過對象中的use方法添加中間件函數,最後調用listen方法啓動node服務器

const app = new Koa();
const logger = async function(ctx, next) {
let res = ctx.res;

// 攔截操做請求 request
console.log(<-- ${ctx.method} ${ctx.url});

await next();

// 攔截操做響應 request
res.on('finish', () => {

console.log(`--> ${ctx.method} ${ctx.url}`);

});
};

app.use(logger)

app.use((ctx, next) => {
ctx.cookies.set('name', 'jon');
ctx.status = 204;

await next();
});

app.use(async(ctx, next) => {
ctx.body = 'hello world';
})

const server = app.listen();
咱們來看下koa中間件的實現,咱們經過實例的use方法添加中間件,在調用實例的listen方法後,在callback中會對中間件進行compose處理,最後在handleRequest中調用處理過的中間件。

listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}

use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {

fn = convert(fn);

}
this.middleware.push(fn);
return this;
}

callback() {
const fn = compose(this.middleware);

if (!this.listenerCount('error')) this.on('error', this.onerror);

const handleRequest = (req, res) => {

const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);

};

return handleRequest;
}

handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
koa中的compose方法和redux中的compose方法原理是同樣的,都是用洋蔥模型的方式依次調用中間件函數,可是koa-compose是經過Promise的方式實現的。咱們來看下koa中compose方法的實現。

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!')

}

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, dispatch.bind(null, i + 1)));
  } catch (err) {
    return Promise.reject(err)
  }
}

}
}
3、面向切面編程
經過對redux和koa中間件實現的簡單分析,你們應該對面向切面編程有了一個簡單的理解,下面一部份內容就是詳細介紹面向切面編程,以及若是不用面向切面編程的方式,咱們還能用什麼方式來知足需求,以及這些方式有什麼問題。

面向切面編程(Aspect Oriented Programming,也叫面向方面編程)是一種非侵入式擴充對象、方法和函數行爲的技術。
核心思想是經過對方法的攔截,在預編譯或運行時進行動態代理,實如今方法被調用時能夠以對業務代碼無侵入的方式添加功能。

好比像日誌、事務等這些功能,和核心業務邏輯沒有直接關聯,經過切面的方式和核心業務邏輯進行剝離,讓業務同窗只需關心業務邏輯的開發,當須要用到這些功能的時候就把切面插拔到業務流程的某些節點上,作到了切面和業務的分離。

咱們舉個例子,來幫助理解面向切面編程的使用場景。

農場的水果包裝流水線一開始只有採摘-清洗-貼標籤三步。

clipboard.png

爲了提升銷量,想加上兩道工序分類和包裝但又不能干擾原有的流程,同時若是沒增長收益能夠隨時撤銷新增工序。

clipboard.png

最後在流水線的中的空隙插上兩個工人去處理,造成採摘-分類-清洗-包裝-貼標籤的新流程,並且工人能夠隨時撤回。

回到AOP的做用這個問題。

AOP就是在現有代碼程序中,在不影響原有功能的基礎上,在程序生命週期或者橫向流程中加入/減去一個或多個功能。

咱們經過一個實際問題來分析AOP的好處。

如今有一個類Foo,類中包含了方法doSomething,我想在每次方法doSomething執行前和執行後打印一段日誌,想實現的效果以下:

class Foo {
doSomething() {

let result;

// dosomething

return result

}
}

const foo = new Foo()
foo.doSomething(1, 2)

// before doSomething
// after doSomething, result: result
3.1 修改源代碼
最簡單粗暴的方法,就是重寫doSomething方法

class Foo {
doSomething() {

console.log(`before doSomething`)
let result;

// dosomething

console.log(`after doSomething, result: ${result}`)
return result

}
}

const foo = new Foo()
foo.doSomething(1, 2)

// before doSomething
// after doSomething, result: result
這樣的壞處很明顯,須要改動原有的代碼,是侵入性最強的一種作法。若是代碼邏輯複雜,修改代碼也會變得困難。若是想用相似的方法分析其餘方法,一樣須要修改其餘方法的源代碼。

3.2 繼承
class Bar extends Foo {
doSomething () {

console.log(`before doSomething`)

const result = super.doSomething.apply(this, arguments)

console.log(`after doSomething, result: ${result}`)

return result

}
}

const bar = new Bar()
bar.doSomething(1, 2)

// before doSomething
// after doSomething, result: result
用繼承的方式避免了修改父類的源代碼,可是每一個使用new Foo的地方都要改爲new Bar。

3.3 重寫類方法
class Foo {
doSomething() {

let result;

// dosomething

return result

}
}

const _doSomething = Foo.prototype.doSomething
Foo.prototype.doSomething = function() {
if (_doSomething) {

console.log(`before doSomething`)

const result = _doSomething.apply(this, arguments)

console.log(`after doSomething, result: ${result}`)

return result

}
}

const foo = new Foo()
foo.doSomething(1, 2)

// before doSomething
// after doSomething, result: result
這樣就多了中間變量_doSomething,也增長了開發成本。

3.4 職責鏈模式
咱們能夠經過在Function的原型上添加before和after函數來知足咱們的需求。

Function.prototype.before = function (fn) {
var self = this;
return function () {

fn.apply(this, arguments);
self.apply(this, arguments);

}
}
Function.prototype.after = function (fn) {
var self = this;
return function () {

const result = self.apply(this, arguments);
fn.call(this, result);

}
}

class Foo {
doSomething() {

let result;

// dosomething

return result

}
}

const foo = new Foo()
foo.doSomething.after((result) => {
console.log(after doSomething, result: ${result})
}).before(() => {
console.log('before doSomething')
})()

// before doSomething
// after doSomething, result: result
從代碼上已經實現了徹底解耦,也沒有中間變量,可是卻有一長串的鏈式調用,若是處理不當,代碼可讀性及可維護性較差。

3.5 中間件
咱們能夠借用中間件思想來分解前端業務邏輯,經過next方法層層傳遞給下一個業務。首先要有個管理中間件的對象,咱們先建立一個名爲Middleware的對象:

function Middleware(){
this.cache = [];
}
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();
}
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、總結
本文一開始簡單分析了redux及koa中間件的實現方式,而後總結介紹了面向切面編程的原理。最後用一個例子介紹不一樣的方式實現對方法的擴充,經過示例咱們總結出用面向切面的編程的方式能下降代碼耦合度,提升可重用性,並且使代碼更簡潔。

Referenceskoa-compose源代碼redux中compose的實現Koa.js的AOP設計編寫可維護代碼之「中間件模式」使用JavaScript攔截和跟蹤瀏覽器中的HTTP請求

相關文章
相關標籤/搜索