Author: Xi Shu square stool elegant set produced

Those of you who have used Express, KOA, or Redux will know that they all have the concept of middleware (middleware in the front-end sense, not a common service between platforms and applications). In Redux we can use the functionality of Redux-Thunk and Loger in a middleware way. In KOA we can handle the request context through middleware.

By middleware, we can add before some method performs unified processing (such as login check, print operation logs, etc.), the design idea of middleware is the thought of aspect oriented programming, put some has nothing to do with the business logic to pull away, the need to use again in the scene, to reduce the coupling, improve the reusability, and make the code more concise.

Let’s take a look at how redux and KOA implement middleware through source code, and finally detail some of the problems that facet programming solves.

Redux middleware

Let’s take a look at the use of redux middleware. Redux exposes the applyMiddleware method, which takes an array of functions as arguments and passes the return value of the applyMiddleware method as a second argument to the createStore method.

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) )Copy the code
Copy

To see how redux middleware works, here is the source code for applyMiddleware, which is composed to the Dispatch method, and the dispatch chain calls the middleware function each time it is called.

export default functionapplyMiddleware(... middlewares) {returncreateStore => (... args) => { 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
    }
  }
}
Copy the code
Copy

Compose’s source code is as follows. Its main function is to call the middleware functions in the array of functions in turn, taking the result of one function as an input parameter to the next function, thus implementing the need to call other methods when dispath is called.

export default functioncompose(... funcs) {if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  returnfuncs.reduce((a, b) => (... args) => a(b(... args))) }Copy the code
Copy

Second, KOA middleware

Similarly, let’s look at the use of KOA middleware. A Koa object is instantiated, the middleware function is added through the use method in the object, and the Listen method is called to start the Node server

const app = new Koa();
const logger = async function(ctx, next) {
  letres = ctx.res; // Intercept operation 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();
Copy the code
Copy

Let’s look at the implementation of KOA middleware. We add middleware through the use method of the instance. After calling the listen method of the instance, the middleware will be composed in the callback, and finally the processed middleware will be called in the handleRequest.

listen(... args) { const server = http.createServer(this.callback());returnserver.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);
}
Copy the code
Copy

The compose method in KOA is the same as the compose method in Redux, which calls middleware functions sequentially in the onion model, but koA-Compose is implemented in a Promise way. Let’s look at the implementation of the compose method in KOA.

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)
      }
    }
  }
}
Copy the code
Copy

Section oriented programming

Through analyzing redux and koa middleware to implement simple, everybody should be of aspect oriented programming with a simple understanding, the following part is detail aspect oriented programming, and if it’s not aspect oriented programming, we also can use what means to meet the demand, as well as the way what’s the problem.

Aspect Oriented Programming (ASPECT-oriented Programming) is a non-invasive technique to extend the behavior of objects, methods and functions. The core idea is to intercept methods and dynamically proxy them at precompile or run time so that functions can be added when methods are called in a non-intrusive way to business code.

These functions such as such as logging, transactions, and no direct link to the core business logic, and core business logic by means of plane, business students need to care about the development of the business logic, if need to use these functions, the plane some nodes of the plug to the business process, did cross section and the separation of the business.

Let’s use an example to help you understand the use scenario of faceted programming.

The farm’s fruit-packing assembly line starts with three steps: picking, washing and labeling.

In order to increase sales, we want to add two processes of classification and packaging without interfering with the original process. At the same time, if there is no increase in income, we can cancel the new processes at any time.

Finally, two workers were inserted into the gap of the assembly line to deal with it, forming a new process of picking, sorting, cleaning, packaging and labeling, and workers can withdraw at any time.

Back to the question of AOP’s role.

AOP is the process of adding/subtracting one or more functions in an existing code program without compromising the original functions in the program lifecycle or horizontal flow.

Let’s examine the benefits of AOP through a practical problem.

Now that I have a class Foo that contains the method doSomething, I want to print a log before and after each execution of the method doSomething.

class Foo {
  doSomething() {
    let result;

    // dosomething

    return result
  }
}

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

// before doSomething
// after doSomething, result: result
Copy the code
Copy

3.1 Modifying source Code

The simplest and most crude way to do this is to rewrite the doSomething method

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
Copy the code
Copy

The downside of this is obvious, and the need to change the original code is the most invasive approach. If the code is logically complex, it can also be difficult to change the code. If you want to analyze other methods in a similar way, you also need to modify the source code of the other methods.

3.2 inheritance

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
Copy the code
Copy

Inheritance avoids modifying the parent’s source code, but every place where new Foo is used needs to be changed to New Bar.

3.3 Override class methods

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
Copy the code
Copy

This increases the intermediate variable _doSomething and also increases the development cost.

3.4 Chain of responsibility mode

We can meet this requirement by adding before and after functions to the prototype Function.

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
Copy the code
Copy

Complete decoupling is achieved from the code, and there are no intermediate variables, but there are a long chain of calls, and if handled improperly, the code is not readable and maintainable.

3.5 the middleware

We can borrow middleware ideas to break down the front-end business logic and pass it on to the next business through the next method. To start with an object to manage Middleware, we’ll create an object called 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(End of the '1');
});
middleware.use(function (next) {
  console.log(2); next(); console.log(End of the '2');
});
middleware.use(function (next) {
  console.log(3); console.log(End of the '3');
});
middleware.use(function (next) {
  console.log(4); next(); console.log('end of 4'); }); middleware.handleRequest(); // The output is as follows: // 1/2/3/3 End // 2 End // 1 EndCopy the code
Copy

Four,

This paper starts with a brief analysis of the implementation of REdux and KOA middleware, and then summarizes the principle of section-oriented programming. Finally, an example is used to introduce different ways to realize the extension of the method. Through the example, we conclude that the method of section-oriented programming can reduce the coupling degree of code, improve the reusability, and make the code more concise.

References

  • Koa – compose the source code
  • The implementation of compose in REdux
  • AOP design of KOA.js
  • “Middleware Patterns” for writing maintainable Code
  • Use JavaScript to intercept and track HTTP requests in the browser