Skip to content

Instantly share code, notes, and snippets.

@darrenscerri
Last active July 11, 2023 02:59
Show Gist options
  • Save darrenscerri/5c3b3dcbe4d370435cfa to your computer and use it in GitHub Desktop.
Save darrenscerri/5c3b3dcbe4d370435cfa to your computer and use it in GitHub Desktop.
A very minimal Javascript (ES5 & ES6) Middleware Pattern Implementation
var Middleware = function() {};
Middleware.prototype.use = function(fn) {
var self = this;
this.go = (function(stack) {
return function(next) {
stack.call(self, function() {
fn.call(self, next.bind(self));
});
}.bind(this);
})(this.go);
};
Middleware.prototype.go = function(next) {
next();
};
class Middleware {
use(fn) {
this.go = (stack => next => stack(fn.bind(this, next.bind(this))))(this.go);
}
go = next => next();
}
// Inspired by: https://github.com/kolodny/exercises/tree/master/middleware
var middleware = new Middleware();
middleware.use(function(next) {
var self = this;
setTimeout(function() {
self.hook1 = true;
next();
}, 10);
});
middleware.use(function(next) {
var self = this;
setTimeout(function() {
self.hook2 = true;
next();
}, 10);
});
var start = new Date();
middleware.go(function() {
console.log(this.hook1); // true
console.log(this.hook2); // true
console.log(new Date() - start); // around 20
});
@Curtis017
Copy link

Nice work! inspired me to make my own using typescript https://gist.github.com/Curtis017/ce072208f5160e8c24b77cc662f10fec

@hieusmiths
Copy link

I hope you can write and explain detail it. Thank so much.

@recursive-beast
Copy link

recursive-beast commented Jul 25, 2020

@darrenscerri Thanks for this nice piece of code, it helped me learn about call stacks and closures.

I just wanted to point out that this bind is unnecessary since it's not used inside the bound function.

What was the intention behind using it anyway?
the "this" in this case refers to the global object, maybe you were trying to bind self instead?

@aronanda
Copy link

aronanda commented Sep 14, 2020

Well done, it took me a bit to "wrap" my head around.
Here's my version with parameters, optional object definition and you can pipe the use() method:
https://gist.github.com/aronanda/d31bb47918145a5aace6005f172e035d

class Middleware {
  constructor(obj) {
    obj = obj || this
    Object.defineProperty(this, '__obj', { value: obj })
    Object.defineProperty(this, 'go', { value: function go(...args) {
      args[args.length - 1].apply(obj, args.slice(0, -1))
    }, writable: true })
  }
  use(fn) {
    this.go = (stack => (...args) => stack(...args.slice(0, -1), () =>
      fn.call(this.__obj, ...args.slice(0, -1), args[args.length - 1]
        .bind(this.__obj, ...args.slice(0, -1)))))(this.go)
    return this
  }
}

// Example:
const middleware = new Middleware(/* define object or defaults to this */)

middleware.use(function (req, res, next) {
  setTimeout(() => {
    this.hook1 = true
    req.step++
    res.step++
    next()
  }, 10)
})

middleware.use(function (req, res, next) {
  setTimeout(() => {
    this.hook2 = true
    req.step++
    res.step++
    next()
  }, 10)
}).use((req, res, next) => {
  // chainable
  setTimeout(() => {
    req.step++
    res.step++
    next()
  }, 10)
})

const start = new Date()
const req = Object.create(new class Req {}(), { step: { value: 1, enumerable: true, writable: true }})
const res = Object.create(new class Res {}(), { step: { value: 1, enumerable: true, writable: true }})
middleware.go(req, res, function (req, res) {
  console.log(this) // Middleware { hook1: true, hook2: true }
  console.log(req) // Req { step: 4 }
  console.log(res) // Res { step: 4 }
  console.log(new Date() - start) // around 30ms
})

@yejinjian
Copy link

Inspired by you,I update my pipeline.js,like this:

util.js

/**
 * 数据去重
 * @param arr
 * @returns {any[]}
 */
export function uniq(arr) {
    return [...new Set(arr)];
}

/**
 * 数组合并去重
 * @param arr
 * @returns {*[]}
 */
export function union(...arr){
    return uniq(arr.reduce((a,b)=> a.concat(b),[]))
}

pipeline.js

import {union} from './utils';


/**
 * 核心
 * @param ctx
 * @param next
 */
const defGo = (ctx, next) => next(ctx);
/**
 * 类似koa-compose方法
 * @param tasks
 * @param dispatch
 * @param context
 * @returns {*}
 */
export const compose = (tasks, dispatch = defGo, context) =>{
    return tasks.reduce((stack,valve) => async (ctx,next)=> await valve(ctx, stack.bind(context,ctx,next)), dispatch);
}

/**
 * 管道模式
 */
export default class Pipeline {
    #valves = [] //中间件
    #basic = (e) => e
    #go = defGo

    constructor(valves = []) {
        this.addValve(...valves);
    }

    /**
     * 最终执行函数
     * @param basic
     */
    setBasic(basic) {
        if (typeof basic !== 'function') {
            throw new Error('basic must be a function');
        }
        this.#basic = basic;
        return this;
    }

    /**
     * 添加阀门
     * @param valves
     * @returns {boolean}
     */
    addValve(...valves) {
        //合并去重
        valves = valves.filter((valve) => typeof valve === 'function');
        this.#valves = union(this.#valves, valves);
        this.#go = compose(valves, this.#go);
        return this;
    }

    /**
     * 删除valves
     */
    clearValve(){
        this.#go = defGo;
        this.#valves = [];
        return this;
    }

    /**
     * 删除阀门
     * @param valves
     */
    removeValve(...valves){
        this.#valves = this.#valves.filter((valve)=> !valves.includes(valve));
        this.#go = compose(this.#valves, defGo);
        return this;
    }

    /**
     * 执行管道中的内容
     * @param params
     * @param basic
     * @returns {*}
     */
    invoke(params, basic) {
        if(basic  && typeof basic === 'function'){
            this.#basic = basic;
        }
        return this.#go(params, this.#basic);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment