1, the preface

After a period of exploration and research, as well as repeated adjustment and verification, finally finished the functor module. Let’s explore the use and implementation of front-end Middleware. The original intention of writing middleware module is based on the reflection and supplement of function combination module. Originally intended to enrich and perfect on the basis of the function combination of the article, but the overall length is slightly longer, the content is slightly complex, simply write a new article.

2, concepts,

Take a look at baidu Baike’s definition:

Middleware is an independent system software service program with which distributed application software shares resources between different technologies. Middleware sits on top of the operating system of client server and manages computing resources and network communication. In this sense, middleware can be expressed as an equation: middleware = platform + communication.

In short, middleware is a kind of software between application system and system software. The purpose is to connect various parts of the application system or different applications on the network to realize resource sharing and function sharing.

The basic category we are talking about is functional programming, and middleware here is not really middleware. To be more precise, it is a design pattern, a programming idea, and a combination of functions. The most widely used scenario of this middleware design pattern is the application model of data Request and data Response in Express and KOA frameworks, which is usually called the Onion model.

The onion model is executed from the outside in, and then from the inside out. Let’s take a look at the implementation of a simple example:

const fn1 = (data) = >{
    console.log('enter fn1')
    data.step1 = 'step1'
    let rst = fn2(data)
    console.log('exit fn1')

    return rst
}

const fn2 = (data) = >{
    console.log('enter fn2')
    data.step2 = 'step2'
    const rst = fn3(data)
    console.log('exit fn2')
    return rst
}

const fn3 = (data) = >{
    console.log('enter fn3')
    data.step3 = 'step3'
    console.log(data)
    console.log('exit fn3')
    return data
}

fn1({name:'Lucy'})
Copy the code

The execution result

enter fn1
enter fn2
enter fn3
{ name: 'Lucy'.step1: 'step1'.step2: 'step2'.step3: 'step3' }
exit fn3
exit fn2
exit fn1
Copy the code

A brief analysis of the execution process:

  • Fn1 receives data, processes it, and sends it to FN2 to start the fN2 execution process
  • Fn2 receives data, processes it, and submits it to FN3 to start the fN3 execution process
  • Fn3 accepts the data, processes it and returns it to FN2, and returns the execution right to FN2
  • Fn2 gets the processing result of FN3, returns it to FN1, and gives the execution right back to FN1
  • Fn1 gets the result of FN2 and continues the execution

The above example is very brief, the purpose is to give an intuitive and clear description of the middleware design model. In practice, however, the problem would be too complex to adopt this programming approach of coupling calls to each other. Let’s take a look at koA’s approach to this type of problem and see the benefits and advantages of middleware:

const app = new Koa()

app.use(async (ctx, next)=>{
    console.log('enter 1')
    await next()
    console.log('exit 1')
})

app.use(async (ctx, next)=>{
    console.log('enter 2')
    await next()
    console.log('exit 2')
})

app.use(async (ctx, next)=>{
    console.log('enter 3')
    await next()
    console.log('exit 3')})Copy the code

The core middleware module of KOA is koA-compose. There are many source code analyses of KoA-compose, which we will cover below, but are not the core of this article. Our goal is to understand how Middleware works so we can use it better. Let’s explore several ways to implement Middleware.

3. Realization of middleware

So Function Composition

According to the above example and application analysis, the calling process of the Onion model is basically similar to Function Composition or Pipeline. These are series composition functions, ways of controlling the flow of data. The difference is that the middleware can call Next actively, while the next of the function composition is called automatically. Active invocation is more flexible, controlling the timing of next calls and intercepting and embellishing input data as well as output data. In other words, middleware can control two-way data flow, while function composition can only control one-way data flow. Now let’s review the implementation of function composition discussed earlier, and on this basis, discuss the implementation principle of middleware.

The realization principle and application example of function combination

function compose(. funcs) {
    return function (input) {
        return funcs.reverse().reduce((result, next) = > next.call(this, result), input)
    }
}

const fn1 = (d) = > d + 1
const fn2 = (d) = > d * 2
const fn3 = (d) = > Math.pow(d, 2)

compose(fn3, fn2, fn1)(2) / / 36
Copy the code

Compose principle is based on the automatic iteration of reduce cycle. The whole process of data flow one-way, data 2 flows in from FN1, and flows through FN2 and FN3. After three times of function processing, the final output result is 36.

The entire execution process can be simplified as:

f3(f2(f1(input)))
Copy the code

On the basis of Compose, there are few changes that need to be made to implement middleware. Simply wrap the next execution in a function, cache it temporarily, and pass it to the next function to wait for an active call. The principle is not difficult to understand, the transfer of execution results, into the transfer of execution process. Note that compose executes from the inside out, while Middleware executes from the outside in and out again. Pay attention to the sequence of parameter transmission.

Let’s adjust the compose function to see how it works.

function middleware(. funcs) {
    return funcs.reverse().reduce((result, next) = > (arg) = > next.call(this, arg, result), () = >{})}const fn1 = (data, next) = > {
    console.log('enter 1')
    data.step1 = 'step1'
    next(data)
    console.log(data)
    console.log('exit 1')}const fn2 = (data,next) = > {
    console.log('enter 2')
    data.step2 = 'step2'
    next(data)
    console.log('exit 2')}const fn3 = (data,next) = > {
    console.log('enter 3')
    data.step3 = 'step3'
    next()
    console.log('exit 3')
}

middleware(fn1,fn2,fn3)
Copy the code

The output

enter 1
enter 2
enter 3
exit 3
exit 2
{ name: 'Lucy'.step1: 'step1'.step2: 'step2'.step3: 'step3' }
exit 1
Copy the code

To facilitate understanding, let’s break down the Reduce execution process:

/ / for the first time
(arg1)=>f3(arg1, (arg2)={})
/ / the second time
(arg1)=>f2(arg1, (arg2) = >f3(arg2, (arg3)={}))
/ / the third time
(arg1)=>f1(arg1,(arg2) = >f2(arg2,(arg3) = >f3(arg3,(arg4)={})))
Copy the code

In practice, most applications of middleware patterns involve asynchronous scenarios. Middleware also lacks support for asynchronous processing. Next, I tweaked to add asynchronous support for Middleware. There are two ways to add asynchronous support. One is to convert an execution result into a Promise. One is to make the next function async… Await.

Let’s take a look at the Promise based approach:


function middleware(. funcs) {
    return funcs.reverse().reduce((result, next) = > (arg) = > Promise.resolve(next.call(this, arg, result)), () = > Promise.resolve())
}

const getData = () = > new Promise((resolve) = > setTimeout(() = > resolve(), 2000))

const fn1 = async (data, next) => {
    console.log('enter 1')
    data.step1 = 'step1'
    await next(data)
    console.log(data)
    console.log('exit 1')}const fn2 = async (data, next) => {
    console.log('enter 2')
    data.step2 = 'step2'
    await getData()
    await next(data)
    console.log('exit 2')}const fn3 = async (data, next) => {
    console.log('enter 3')
    data.step3 = 'step3'
    await next(data)
    console.log('exit 3')
}

middleware(fn1, fn2, fn3)({ name: 'Lucy' })

Copy the code

The execution result

enter 1
enter 2
enter 3
exit 3
exit 2
{ name: 'Lucy'.step1: 'step1'.step2: 'step2'.step3: 'step3' }
exit 1

Copy the code

Now let’s look again with async… The await implementation supports asynchronous thinking:

function middleware(. funcs) {
    return funcs.reverse().reduce((result, next) = > {
        return async (arg) => {
            await next.call(this, arg, await result)
        }
    }, async() => {})}Copy the code

There are many ways to implement compose, and many ways to implement Middleware on top of compose, such as for… Of the middleware.

function middleware(. funcs) {
    return (input) = > {
        let result = arg= > arg

        for (let next of funcs.reverse()) {
            result = ((next, result) = > async (arg) => await next(arg, result))(next, result)
        }

        return result(input)
    }
}
Copy the code

In general, the basic principle of middleware is based on the combination of functions that transfer execution results into transfer execution process functions. Convert automatic execution to manual execution next; Switch from controlling one-way data flow to being able to control two-way data flow. Middleware can also be split, composed, manageable, and readable.

Look at Compose from a different Angle

We’ll talk about middleware based on Compose from a data transfer perspective. The basic principle is to convert the result of a pass execution into a pass execution procedure function. Let’s think about this from a different Angle: what if the combination function executes as a function?

const fn1 = (next) = > (data) = > {
    console.log('enter fn1')
    next(data)
    console.log('exit fn1')}const fn2 = (next) = > (data) = > {
    console.log('enter fn2')
    next(data)
    console.log('exit fn2')}const fn3 = (next) = > (data) = > {
    console.log('enter fn3')
    console.log(next(data))
    console.log('exit fn3')}let cm = compose(fn1, fn2, fn3)
let fc = cm(() = > {})
fc({ name: 'Lucy' })
Copy the code

Execution process:

// compose(fn1,fn2,fn3) is composed
cm = (input) = >f1(f2(f3(input)))

// enter cm(()=>{})
fc = function rst1(data){
    ~(function rst2(data){
        ~(function rst3(data){
            input()
        })()
    })()
}

/ / the third step
fc({name:'Lucy'})
Copy the code

The results of

enter fn1
enter fn2
enter fn3
{ name: 'Lucy' }
exit fn3
exit fn2
exit fn1
Copy the code

koa-compose

The most widely used node.js developer is the KOA framework, which is based on middleware and whose core module is Koa-compose. Koa-compose’s implementation code is very concise and has a lot to learn and reference.

Let’s look at the koa-compose implementation code.


function compose(middleware) {

    return function (context, next) {
        let index = -1
        return dispatch(0)
        function dispatch(i) {
            // Avoid repeated calls
            if (i <= index) return Promise.reject(new Error('next() called multiple times'))
            index = i
            let fn = middleware[i]
            Compose's last callback function
            if (i === middleware.length) fn = next
            if(! fn)return Promise.resolve()
            try {
                // Core executes code, cache executes next, and supports asynchrony
                return Promise.resolve(fn(context, function next() {
                    return dispatch(i + 1)}}))catch (err) {
                return Promise.reject(err)
            }
        }
    }
}

Copy the code

Koa-compose is implemented in much the same way as middleware, which we discussed earlier. The idea is to transfer the execution result into the execution process. The difference is that koa-compose is based on recursive loop iteration, whereas we are based on reduce or for.. Of. Koa-compose handles the details well, such as parameter validation and multiple calls to Next.

A brief discussion of repeated calls to next. In theory, a middleware should only execute once in an execution flow. In other words, the same Next should only be called once, and if it is called more than once, an exception should be thrown.

Let’s adjust the FN2 on top


const fn2 = async (data, next) => {
    console.log('enter 2')
    data.step2 = 'step2'
    await getData()
    await next(data)
    await next(data)
    console.log('exit 2')
}

compose([fn1, fn2, fn3])({ name: 'Lucy' }, (data) = >{
    console.log(data)
})
Copy the code

The results, reported UnhandledPromiseRejectionWarning: Error: next () called multiple times

enter 1
enter 2
enter 3
exit 3
(node:52325) UnhandledPromiseRejectionWarning: Error: next() called multiple times
Copy the code

A complete Middleware

In the previous section, we discussed how to implement Middleware based on compose, so let’s implement a complete middleware. First, Middleware needs to have a property that manages all middlewares; Second, you need a method like app.use to add middlewares. In addition, you need a method that triggers the execution of the Middlewares process.

function Middleware(. middlewares) {
    const stack = middlewares

    const push = (. middlewares) = >{ stack.push(... middlewares)return this
    }

    const execute = async (context, callback) => {
        let prevIndex = -1
        const runner = (index) = > {
            if (index === prevIndex) {
                throw new Error('next() called multiple times')
            }
            prevIndex = index
            let middleware = stack[index]
            if (prevIndex === stack.length) middleware = callback

            if(! middleware)return Promise.resolve()

            try {
                return Promise.resolve(middleware(context, () = > {
                    return runner(index + 1)}}))catch (err) {
                return Promise.reject(err)
            }
        }

        return runner(0)}return { push, execute }
}

let middleware = Middleware()
middleware.push(fn1)
middleware.push(fn2)
middleware.push(fn3)

middleware.execute({ name: 'Lucy' }, (ctx) = > console.log(ctx))
Copy the code

The Middleware typescript version and the Middleware class version are shown below

The typescript version Middleware

type Next = () = > Promise<void> | void
type TMiddleware<T> = (context: T, next: Next) = > Promise<void> | void
type IMiddleware<T> = {
    push: (. middlewares: TMiddleware
       
        []
       ) = > void
    execute: (context: T, callback: Next) = > Promise<void>}function Middleware<T> (. middlewares: TMiddleware
       
        []
       ) :IMiddleware<T> {
    const stack: TMiddleware<T>[] = middlewares

    const push: IMiddleware<T>['push'] = (. middlewares) = >{ stack.push(... middlewares)return this
    }

    const execute: IMiddleware<T>['execute'] = (context, callback) = > {
        let prevIndex = -1
        const runner = async (index: number): Promise<void> = > {if (index === prevIndex) {
                throw new Error('next() called multiple times')
            }
            prevIndex = index
            let middleware = stack[index]

            if (prevIndex === stack.length) middleware = callback

            if(! middleware)return Promise.resolve()

            try {
                return Promise.resolve(middleware(context, () = > {
                    return runner(index + 1)}}))catch (err) {
                return Promise.reject(err)
            }
        }

        return runner(0)}return { push, execute }
}

Copy the code

Middleware classes

type Next = () = > Promise<void> | void
type TMiddleware<T> = (context: T, next: Next) = > Promise<void> | void
type IMiddleware<T> = {
    push: (. middlewares: TMiddleware
       
        []
       ) = > void
    execute: (context: T, callback: Next) = > Promise<void>}class Middleware<T> implements IMiddleware<T>{
    stack: TMiddleware<T>[] = []

    staticcreate<T>(... middlewares: TMiddleware<T>[]) {return newMiddleware(... middlewares) }constructor(. middlewares: TMiddleware
       
        []
       ) {
        this.stack = middlewares
    }

    public push(. middlewares: TMiddleware
       
        []
       ) {
        this.stack.push(... middlewares)return this
    }

    public execute(context? , callback?) {
        let prevIndex: number = -1
        const runner = async (index: number): Promise<void> = > {if (index === prevIndex) {
                throw new Error('next() called multiple times')
            }
            prevIndex = index
            let middleware = this.stack[index]

            if (prevIndex === this.stack.length) middleware = callback

            if(! middleware)return Promise.resolve()

            try {
                return Promise.resolve(middleware(context, () = > {
                    return runner(index + 1)}}))catch (err) {
                return Promise.reject(err)
            }
        }

        return runner(0)}}Copy the code

4. Practical application

The application of middleware in the field of Web service development is the most extensive and well-known node.js development. The most typical and used framework is the KOA framework. Koa is a new Web framework built by the same people behind Express. The basic core module is middleware, which is used to control the Request data flow Request and Response data flow Response.

const Koa = require('koa')
const app = new Koa()

app.use(async ctx => {
    ctx.body = 'Hello World'
})

app.use(async (ctx, next) => {
    const start = Date.now()
    await next()
    const ms = Date.now() - start
    ctx.set('X-Response-Time'.`${ms}ms`)
})

app.use(async (ctx, next) => {
    await getData()
    next()
})

app.listen(3000)
Copy the code

Middleware is widely used in the field of front-end development. Network data request logs, performance monitoring, and user behavior burying point information are typical application scenarios of intermediate price. This secondary business information is important, but it is not the main business process. Moreover, these auxiliary business information is scattered and coupled in each link of the main business process of the project, which is difficult to manage. The larger the project is, the more difficult it is to maintain. These secondary businesses can be decoupled from the main business process and developed and maintained as separate modules using middleware. This conforms to the function singleness of module design, and can improve the maintainability and portability of module.

Below we pass a concrete example, demonstrate the application of middleware in front end.

service.js

export const getData = (url, param) = > new Promise((resolve) = > setTimeout(() = > resolve({
    name: 'Joe'.age: 28.work: 'drivers'
}), 2000))
Copy the code

index.js

import { Middleware } from './middleware.js'
import { log } from './log-middleware.js'
import { getData } from './service.js'

export const fetchPerson = (url, params) = > {
    let mw = Middleware()
    mw.push(log)

    mw.push(async (ctx, next) => {
        let person = await getData(url, params)
        ctx.res = person
        next()
    })

    mw.execute({ url, params }, ctx= > {
        console.log(ctx.res)
    })
}

export default {
    start() {
        fetchPerson('http://www.abc.com/api/v1/person', {
            id: '123456'}}})Copy the code

log-middleware.js

export const log = async (ctx, next) => {
    let { url, params } = ctx
    let log = {
        startTime: Date.now(),
        request: {
            url,
            params
        }
    }

    await next()

    log.response = ctx.res
    log.endTime = Date.now()

    console.log(`send log info:\n`, log)
}
Copy the code

index.html

<! DOCTYPEhtml>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0, the minimum - scale = 1.0, the maximum - scale = 1.0, user - scalable = 0">
    <title>Document</title>
</head>

<body>
    <script type="module">
        import app from './index.js'
        app.start()
    </script>
</body>

</html>
Copy the code

Results:

5. Reference materials

  • Muniftanjim. Dev/blog/basic -…

  • Blog.csdn.net/github_3814…

  • Github.com/koajs/compo…