primers

To add abort to the HTTP request method in the library, I refer to the axios source code and take a look at the axios interceptor implementation, plus a little knowledge of the middle of Redux, Express, and koa-compose. So with this active thinking we first think about the middleware we have used, implement some simple middleware, and then compare some middleware implementations in the community, and further think about it.

synchronous

const fns = [fn1, fn2, fn3];
for(var fn of fns){
    fn()
}
Copy the code

asynchronous

Assume the following code: asynchronously multiply by 2

const double = (val) = > {
  return new Promise((resolve, reject) = > {
    setTimeout(() = > {
      resolve(val * 2);
      console.log(val * 2);
    }, 1000);
  });
};
const fn1 = double;
const fn2 = double;
const fn3 = double;
const fns = [fn1, fn2, fn3];
Copy the code

Then () to ensure sequential execution, the object code is as follows:

fn1(2)
  .then((result) = > {
    return fn2(result);
  })
  .then((result) = > {
    return fn3(result);
  })
  .then((result) = > {
    console.log(result);
    return result;
  });
Copy the code

For ease of use, we need to write a generator that generates the code above. Is also very simple

function compose(fns) {
  return (val) = > {
    let p = Promise.resolve(val);
    // loop. Then ()
    for (let fn of fns) {
      // Make sure fn() returns promise
      p = p.then((res) = > Promise.resolve(fn(res)));
    }
    return p;
  };
}
const c = compose(fns);
Copy the code

Refer to the community

Axios

// Use middleware
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
    synchronous: options ? options.synchronous : false.runWhen: options ? options.runWhen : null
  });
  return this.handlers.length - 1;
};
Copy the code

Adding Middleware

  1. This is a big pity
  2. Rejected: Indicates the failed callback
  3. Synchronous: Whether the Request middleware is executed synchronously
  4. RunWhen: The runtime determines whether the middleware will be added
module.exports = function dispatchRequest(config) {
  var adapter = config.adapter || defaults.adapter;
  return adapter(config).then(function onAdapterResolution(response) {
    return response;
  }, function onAdapterRejection(reason) {
    return Promise.reject(reason);
  });
};
Copy the code
  1. DispatchRequest: Actual request function
  2. Adapter: An adapter that ADAPTS requests from node and browser.
Axios.prototype.request = function request(config) {
  // filter out skipped interceptors
  var requestInterceptorChain = [];
  var synchronousRequestInterceptors = true;
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
      return;
    }

    synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;

    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  var promise;
  // If not synchronous interceptors, both request and response interceptors are called with promise.then
  if(! synchronousRequestInterceptors) {var chain = [dispatchRequest, undefined];

    Array.prototype.unshift.apply(chain, requestInterceptorChain);
    chain.concat(responseInterceptorChain);

    promise = Promise.resolve(config);
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift());
    }

    return promise;
  }

  // If it is a synchronous interceptor, request is called synchronously first, then promise.then calls the request and response interceptors
  var newConfig = config;
  // Build the key code for the PRMISE chain
  while (requestInterceptorChain.length) {
    var onFulfilled = requestInterceptorChain.shift();
    var onRejected = requestInterceptorChain.shift();
    try {
      newConfig = onFulfilled(newConfig);
    } catch (error) {
      onRejected(error);
      break; }}try {
    promise = dispatchRequest(newConfig);
  } catch (error) {
    return Promise.reject(error);
  }
  // Key code to build the Promise chain
  while (responseInterceptorChain.length) {
    promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
  }

  return promise;
};
Copy the code

With our active thinking in the asynchronous method is a way of thinking, simple implementation, easy to understand:

  1. Define request interceptors and Response interceptors, manually inserted indispatchRequestBefore and after.
  2. Based on whether the Request interceptor is synchronized
  3. If both request interceptors are asynchronous, build one through a while looppromise.then(fulfilled, rejected).then(fulfilled, rejected)The chain call to.
  4. If one of the request interceptors is synchronous, the request interceptor is executed through the while loop, the new configuration is retrieved, and a new one is constructed through the while looppromise.then(fulfilled, rejected).then(fulfilled, rejected)The chain call to.

Redux

Source code for compose

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

An 🌰 :

// Middleware 1
function add(next) {
  return (num) = > {
    next(num + 1);
  };
}
// Middleware 2
function multiple(next) {
  return (num) = > {
    setTimeout(() = > {
      next(num * 2);
    }, 2000);
  };
}
// Target function
function printf(num){
    console.log(num)
}

const compute = compose(add, multiple)(printf)
compute(1)
Copy the code

Break it down:

const compute = (. args) = > {
  return ((next) = > {
    return (num) = > {
      next(num + 1);
    };
  })(
    ((next) = > {
      return (num) = > {
        setTimeout(() = > {
          next(num * 2);
        }, 2000); }; }) (... args) ); };Copy the code

Let’s simplify the structure as follows

(... args) => (function m1(next) {
    // ...
  })(
    (function m2(next) {
      // ...
    })(
      (function printf(next) {
        // ...}) (... args) ) );Copy the code

Using the array.prototype. reduce method, pass the first middleware as an argument to the previous middleware, i.e

  1. Combination:compose([add, multiple])(printf)Converted toadd(mulitple(printf)), the steps are as follows:
  2. willprintfAs amutiplethenextParameter, run the command firstmutiple(next).(num)=>{setTimeout(()=>{console.log(num*2)})}
  3. Returns the result asaddthenextParameters,(num) => setTimeOut(()=>console.log((num+1)*2))
  4. add(next)Finally return a functioncompute
  5. Execution: Final callcompute()When theadd.multiple.printfperform

In simple terms, reverse load, forward execution. And it has the characteristics

  1. Flexible organization: you can either add a layer to wrap the function’s common parameters, or you can pass parameters next
  2. Static generation: The compose process is the process of generating a function

It’s actually called pipe: see JavaScript pipe for details

Why does redux middleware have to return a function when koa-compose, Expres, does not?

Express

Handle -> router.handle -> (layer.handle_request -> (route.dispatch -> layer.handle_request)… .

Layer is an object that stores the relationship between path and Handle (one or more middleware)

  1. A: refers toapp.useIn this case, Handle defines its own middleware function
  2. More than one:router.get(), handle isroute.dispatch“, and recursively call their own defined Middleware functions

The core is a two-level recursion, which simplifies the code as follows :(not the same)

function express() {
  var funcs = []; // Array of functions to be executed

  var app = function (req, res) {
    var i = 0;

    function next() {
      var task = funcs[i++]; // Retrieve the next function in the array of functions
      if(! task) {// Return if the function does not exist
        return;
      }
      task(req, res, next); // Otherwise, execute the next function, noting that there is no return
    }

    next();
  };

  The /** * use method adds a function to an array of functions *@param task* /
  app.use = function (task) {
    funcs.push(task);
  };

  return app; // Return the instance
}
Copy the code
  1. Array storage: Use arrays to store all middleware functions.
  2. Functions are middleware: theapp.get.app.use.router.getFunctions in the middleware.
  3. Executor: Executes the next middleware by calling next.
    1. Next:()=>task[0]()
    2. Next:()=>task[1]()
    3. Next:()=>task[2]()

Executing next for the first time executes task[0], and next for task[0] executes task[1]… , up to the last task

What’s the difference between Express and Redux?

Koa-compose

Koa-compose is an example:

import compose from 'koa-compose';
async m1(ctx, next){
    console.log('first before');
    await next();
    console.log('first after');
}
async m1(ctx , next){
    console.log('second before');
    await next();
    console.log('second after');
}
const c = compose([m1, m2])
c(null.() = >{
    console.log("done")})// Run the result
/**
* first before
* second before
* done
* second after
* first after
* /
Copy the code

The source code is as follows:

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array! ')
  for (const fn of middleware) {
    if (typeoffn ! = ='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]
      // When the middleware is finished executing, assign the value to the object function passed in externally, namely the onion heart (the last function normally executed).
      if (i === middleware.length) fn = next
      // Terminate the condition, return promise
      if(! fn)return Promise.resolve()
      try {
        // Make sure fn() returns a promise and changes the next argument to the middleware
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
Copy the code

Parsing source code:

  1. Termination returns: If! Fn, returnPromise.resolve()
  2. Recursive return:Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
  3. The onion the heart:i === middleware.lengthwhenfn=next
  4. Next =m1,next=m2, and finally next=next make forward calls by continuously assigning the middleware’s next function (external target function next in non-closure), and the code after await is equivalent to calling promise.then.

To disassemble the 🌰 above, the koa-compose conversion executes the following code:

Promise.resolve(m1(context, () = > {
  return Promise.resolve(m2(ctx, () = > {
    return Promise.resolve(next(ctx, () = > {
      return Promise.resolve(); })); })); })); A bit abstract, expand m1, m2, next to simplify as follows (omit passing parameters) :Promise.resolve(
    (async() = > {console.log('first before');
      await Promise.resolve(
        (async() = > {console.log('second before');
          await Promise.resolve(
            (() = > {
              console.log('done');
            })()
          )
          console.log('second after')}) ())console.log('first after')}) ())Copy the code
  1. Koa and Express are implemented in the same way. Why is Express not the Onion model?
  2. Why must promise.resolve() be returned?

Answer questions

  1. Why does redux middleware have to return a function when koa-compose, Expres, does not?

A: Because redux has a combination process, when redux is combined, it needs to take the execution result of the next middleware as the next parameter of the previous middleware. That is to say, the execution result of the middleware is not a function, and an error will be reported if it is passed to the previous middleware: Next is not a function

  1. What’s the difference between Express and Redux?

Redux is generated statically in compose(… Middlewares)(doSth) already determines what next is and thus the order of execution, while Express dynamically determines what next is

  1. What is the difference between KOA and Express? Why is Express not an Onion model?

Answer: Because expres calls task(res, req, next) without return, the final call is not await promise, but await undefind, so the onion model is not formed.

  1. Why must promise.resolve() be returned?

A: The result of compose must return a promise case to prevent only the request middleware from reporting an error for the synchronization function. Resolve () : for koa-compose, koa: for koa-compose, koa:

  • Test case for KOA-compose

Error: Next function should return Promise

  • Test cases for KOA
 it('should merge properties'.() = > {
    app1.use((ctx, next) = > {
      assert.equal(ctx.msg, 'hello');
      ctx.status = 204;
    });

    return request(app1.listen())
      .get('/')
      .expect(204);
  });
Copy the code

Error: handleRequest error: 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

You can also see here that the final step in the KOA Onion model is to process the response after the middleware executes. Express handles the response in the last middleware.

conclusion

The differences between the four middleware

  1. Axios is the simplest, direct promise chain calls
  2. Redux syntax needs to return functions, which is not very convenient to write, but it has a special idea. It uses reduce, generates statically, and takes the next intermediate execution result as the next parameter of the previous middleware.
  3. For koA-compose, express and KoA-compose have the same idea. They both use recursions (actuators) to execute dynamically, from the first middleware to the last middleware. The difference is that KoA-compose implements the Onion model by returning a promise.

Middleware implementations, in essence, use these two things about the order of execution:

  1. Function call stack: Next intervenes in the execution order
  2. Promise.then: Asynchronous sequential invocation or onion model is implemented through promise.then.