Skip to content

Instantly share code, notes, and snippets.

@lughino
Forked from unbug/Middleware.js
Created February 11, 2017 10:35
Show Gist options
  • Save lughino/e5dbe82eac2fe7e5b263807af82f96cf to your computer and use it in GitHub Desktop.
Save lughino/e5dbe82eac2fe7e5b263807af82f96cf to your computer and use it in GitHub Desktop.
Powerful Javascript Middleware Pattern Implementation, apply middleweares to any object.
'use strict';
/* eslint-disable consistent-this */
let middlewareManagerHash = [];
/**
* Composes single-argument functions from right to left. The rightmost
* function can take multiple arguments as it provides the signature for
* the resulting composite function.
*
* @param {...Function} funcs The functions to compose.
* @returns {Function} A function obtained by composing the argument functions
* from right to left. For example, compose(f, g, h) is identical to doing
* (...args) => f(g(h(...args))).
*/
export function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg;
}
funcs = funcs.filter(func => typeof func === 'function');
if (funcs.length === 1) {
return funcs[0];
}
const last = funcs[funcs.length - 1];
const rest = funcs.slice(0, -1);
return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args));
}
/**
* Manage and apply middlewares for an object.
* Middleware functions are functions that have access to the target function and it's arguments,
* and the target object and the next middleware function in the target function cycle.
* The next middleware function is commonly denoted by a variable named next.
*
* Middleware functions can perform the following tasks:
* - Execute any code.
* - Make changes to the function's arguments.
* - End the target function.
* - Call the next middleware in the stack.
*
* If the current middleware function does not end the target function cycle,
* it must call next() to pass control to the next middleware function. Otherwise,
* the target function will be left hanging.
*
* e.g.
* ```
* const walk = target => next => (...args) => {
* this.log(`walk function start.`);
* const result = next(...args);
* this.log(`walk function end.`);
* return result;
* }
* ```
*
* Middleware object is an object that contains function's name as same as the target object's function name.
*
* e.g.
* ```
* const Logger = {
* walk: target => next => (...args) => {
* console.log(`walk function start.`);
* const result = next(...args);
* console.log(`walk function end.`);
* return result;
* }
* }
* ```
*
* Function's name start or end with "_" will not be able to apply middleware.
*
* @example
*
* ## Basic
*
* We define a Person class.
* // the target object
* class Person {
* // the target function
* walk(step) {
* this.step = step;
* }
*
* speak(word) {
* this.word = word;
* }
* }
*
* Then we define a middleware function to print log.
*
* // middleware for walk function
* const logger = target => next => (...args) => {
* console.log(`walk start, steps: ${args[0]}.`);
* const result = next(...args);
* console.log(`walk end.`);
* return result;
* }
*
* Now we apply the log function as a middleware to a Person instance.
*
* // apply middleware to target object
* const p = new Person();
* const middlewareManager = new MiddlewareManager(p);
* middlewareManager.use('walk', walk);
* p.walk(3);
*
* Whenever a Person instance call it's walk method, we'll see logs from the looger middleware.
*
* ## Middleware object
* We can also apply a middleware object to a target object.
* Middleware object is an object that contains function's name as same as the target object's function name.
*
* const PersonMiddleware {
* walk: target => next => step => {
* console.log(`walk start, steps: step.`);
* const result = next(step);
* console.log(`walk end.`);
* return result;
* },
* speak: target => next => word => {
* word = 'this is a middleware trying to say: ' + word;
* return next(word);
* }
* }
*
* // apply middleware to target object
* const p = new Person();
* const middlewareManager = new MiddlewareManager(p);
* middlewareManager.use(PersonMiddleware);
* p.walk(3);
* p.speak('hi');
*
* ## middlewareMethods
* Or we can use `middlewareMethods` to define function names for middleware target within a class.
*
* class CuePointMiddleware {
* constructor() {
* //Define function names for middleware target.
* this.middlewareMethods = ['walk', 'speak'];
* }
* log(text) {
* console.log('Middleware log: ' + text);
* }
* walk(target) {
* return next => step => {
* this.log(`walk start, steps: step.`);
* const result = next(step);
* this.log(`walk end.`);
* return result;
* }
* }
* speak(target) {
* return next => word => {
* this.log('this is a middleware tring to say: ' + word);
* return next(word);
* }
* }
* }
*
* // apply middleware to target object
* const p = new Person();
* const middlewareManager = new MiddlewareManager(p);
* middlewareManager.use(new PersonMiddleware())
* p.walk(3);
* p.speak('hi');
*
*/
export class MiddlewareManager {
/**
* @param {object} target The target object.
* @param {...object} middlewareObjects Middleware objects.
* @return {object} this
*/
constructor(target, ...middlewareObjects) {
let instance = middlewareManagerHash.find(function (key) {
return key._target === target;
});
// a target can only has one MiddlewareManager instance
if (instance === undefined) {
this._target = target;
this._methods = {};
this._methodMiddlewares = {};
middlewareManagerHash.push(this);
instance = this;
}
instance.use(...middlewareObjects);
return instance;
}
_applyToMethod(methodName, ...middlewares) {
if (typeof methodName === 'string' && !/^_+|_+$/g.test(methodName)) {
let method = this._methods[methodName] || this._target[methodName];
if (typeof method === 'function') {
this._methods[methodName] = method;
if (this._methodMiddlewares[methodName] === undefined) {
this._methodMiddlewares[methodName] = [];
}
middlewares.forEach(middleware =>
typeof middleware === 'function' && this._methodMiddlewares[methodName].push(middleware(this._target))
);
this._target[methodName] = compose(...this._methodMiddlewares[methodName])(method.bind(this._target));
}
}
}
/**
* Apply (register) middleware functions to the target function or apply (register) middleware objects.
* If the first argument is a middleware object, the rest arguments must be middleware objects.
*
* @param {string|object} methodName String for target function name, object for a middleware object.
* @param {...function|...object} middlewares The middleware chain to be applied.
* @return {object} this
*/
use(methodName, ...middlewares) {
if (typeof methodName === 'object') {
Array.prototype.slice.call(arguments).forEach(arg => {
// A middleware object can specify target functions within middlewareMethods (Array).
// e.g. obj.middlewareMethods = ['method1', 'method2'];
// only method1 and method2 will be the target function.
typeof arg === 'object' && (arg.middlewareMethods || Object.keys(arg)).forEach(key => {
typeof arg[key] === 'function' && this._applyToMethod(key, arg[key].bind(arg));
});
});
} else {
this._applyToMethod(methodName, ...middlewares);
}
return this;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment