preface

I don’t want to use too much space to elaborate on this topic. But some concepts and the original intention of the design is necessary to explain clearly, in order to make clear its deep inner logic. This is the principle that I always follow, “know what is, know what is.” First of all, this paper will simply explain the concept; Furthermore, with the development of front-end languages ES5, ES6 and Typescript, the implementation of front-end AOP is discussed with examples. Finally, take a look at the AOP application scenarios, related frameworks, and development patterns in real projects.

What is the AOP?

AOP is the abbreviation of Aspect Oriented Programming, meaning: Aspect Oriented Programming. The main intent is to separate logging, performance statistics, security controls, transaction handling, exception handling, etc., from the business logic code and separate them into methods that do not direct the business logic, thus changing these behaviors without affecting the business logic code.

In short, the “elegant” decoupling of “assistive logic” from “business logic.”

Briefly, some of the concepts in AOP

It’s actually a related concept in AspectJ. AspectJ is a faceted framework that extends the Java language. Its excellent design ideas are worth our reference and study.

The main concepts associated with AspectJ are the following:

  • Joinpoint: Represents a point clearly defined in a program, typically including method calls, access to class members, and execution of exception handler blocks

  • Pointcut: Represents a set of JoinPoints, either combined through logical relationships or gathered through wildmatches, regular expressions, and so on, that define where enhanced Advice will be placed. For example, “Submit and reset method call execution for all classes”

  • Aspects: Consists of pointcuts and enhancements, including both the concrete implementation of the enhancement logic and the definition of join points

  • Advice: Concrete enhancement logic implementation. Generally, according to different places, it can be divided into Before, AfterReturning, AfterThrowing, After and Around

  • Weaving: The process of applying a slice to a target object to create a new proxy object

For the sake of understanding, we can compare a business process to a loaf of bread, and the loaf of bread is a business module or function. Descriptions like “the front of a section” and “the back of a section” are called joinpoints; When peanut butter, bread and lettuce are packaged into separate modules for logical processing, it is an Aspect. An aspect can define Advice, Pointcut, or join point into Advice; Weaving cuts of bread to make a sandwich is called Weaving.

A simple implementation of early front-end AOP

Most of the front-end programming ideas, design patterns, frameworks and tools are implemented by referring to the mature design ideas already applied by the server, and AOP is no exception. We’ve already talked about some AspectJ concepts. Obviously, the next step is to implement front-end AOP programming along the lines of its usage.

From the previous analysis of AspectJ’s concepts, a brief summary can be drawn: AOP programming is about adding a custom action before or after the original execution without breaking the original code. Here the “old actions” are functions, as are the “enhanced actions”, and so are packaging them into a single action. Obviously, based on the nature of the javascript language, functional programming is the best way to implement front-end AOP programming.

Look at the following simple implementation:

/ * * *@param {Function} OldFn *@param {Function} Before function *@param {Function} After the function *@return {Function} New function * /
var adviceAction = function(oldFn, before, after){
    return function(){
        var args = [].slice.call(arguments)
        try{
            / / enhancement
            before.apply(null, args)
        }catch(e){}

        / / function
        var rst = oldFn.apply(this, args)

       try{
            / / enhancement
            after.apply(null, args)
        }catch(e){}

        return rst
    }
}
Copy the code

Call execution:

function run(){
    console.log('run')}function before(jp){
    console.log('before action')}function after(jp){
    console.log('after action')}var newFn = adviceAction(run, before, after)

newFn()
Copy the code

The execution result

The PointCut of the “oldFn call executes “that is matched by passing the parameter; Before and after correspond to Advice; The process of creating newFn corresponds to Weaving. In addition, you can simply simulate the JoinPoint information for each enhancement by referring to AspectJ usage.

var adviceAction = function(oldFn, before, after){
    return function(){
        var args = [].slice.call(arguments)

        var joinPoint = {
            target:null.key: oldFn.name,
            method: oldFn.bind(this),
            context: this.args: args
        }

        try{
            / / enhancement
            before.apply(null, [joinPoint])
        }catch(e){}

        / / function
        var rst = oldFn.apply(this, args)

        try{
            / / enhancement
            after.apply(null, [joinPoint])
        }catch(e){}

        return rst
    }
}
Copy the code

Test call:

/ /... omit

function before(jp){
    console.log('before action:' +jp.key)
}

function after(jp){
    console.log('after action:' + jp.key)
}

/ /... omit
Copy the code

The execution result

At this point, you can begin to see the rudiments of front-end AOP programming. The previous examples have simply implemented Before and After notifications. There is also no discussion of AfterReturning, AfterThrowing, and implementation of Around. Next, we’ll implement a simple, complete AspectJS with a simple simulation.

  • Methods identified by Before are pre-methods that are executed Before the execution of the target method, that is, Before the join point.

  • The Around wrap notification method can contain the above four notification methods, and the wrap notification is the most comprehensive. And the surround notification must have a return value, which is the return value of the target method. JoinPoint requires an invoke method to execute the original action.

Let’s say we need to compute the execution time of the original function

function around(joinPoint){
    var startTime = Date.now()
    var rst = joinPoint.invoke()
    var howLong = Dte.now - startTime

    return rst
}
Copy the code
  • The AfterThrowing exception notification method is executed only if an exception occurs in the join point method, otherwise it is not. So JoinPoint requires an exception parameter
try{
    var rst = oldFn.apply(this, args)
    // returning
    return rst
}catch(err){
    joinPoint.exception = err
    throwing.apply(null, [joinPoint])
}finally{
   //after
}
Copy the code
  • AfterReturning notification methods are executed only after the join point methods are successfully executed, or not if the join point methods are abnormal. The return notification method will not execute until the target method has successfully executed, and the return notification method can get the result of the target method (join point method) after execution. Therefore, the corresponding JoinPoint should be accompanied by the execution result.
try{
    var rst = oldFn.apply(this, args)
    joinPoint.result = rst
    returning.apply(null,[joinPoint])
    return rst
}catch(err){
    // throwing
}finally{
   //after
}
Copy the code
  • The After post-notification method is executed After the join point method completes, whether the join point method succeeds or an exception occurs
// brefore
try{
    // around or oldFn
    // returning
}catch(err){
    // throwing
}finally{
   after.apply(null, [joinPoint])
}
Copy the code

Here is a complete implementation of this abbreviated version of the AOP tool

/ * * *@param {Function} Target Object or class *@param {String} MethodKey Method name *@param {Object} Advices to strengthen {before? :Function, afeter? : Function, throwing? : Fuction, around? : Function, returning? : Function} */
var adviceAction = function (target, methodKey, advices) {
    var context = target
    target = typeof target === 'function' ? target.prototype : target
    var oldFn = target[methodKey]

    Object.defineProperty(target, methodKey, {
        value: _decorator,
        writeable:true
    })

    function _decorator() {
        var args = [].slice.call(arguments)
        var that = this,
            rst

        var joinPoint = {
            target: target,
            key: methodKey,
            method: oldFn.bind(this),
            context: this.args: args
        }

        if (advices) {
            var before = advices.before,
                after = advices.after,
                throwing = advices.throwing,
                around = advices.around,
                returning = advices.returning
        }

        if (typeof before === 'function') {
            try {
                // Pre-enhanced
                before.apply(null, [joinPoint])
            } catch (e) { }
        }

        try {
            if (typeof around === 'function') {
                var invoke = function () {
                    var _args = [].slice.call(arguments)
                    return oldFn.apply(that, args.concat(_args))
                }

                rst = around.apply(null[Object.assign({}, joinPoint, {
                    invoke: invoke
                })])
            } else {
                rst = oldFn.apply(that, args)
            }

            if (typeof returning === 'function') {
                joinPoint.result = rst
                // post-enhance
                returning.apply(null, [joinPoint])
            }

            return rst
        } catch (err) {
            if (typeof throwing === 'function') {
                joinPoint.exception = err
                throwing.apply(null, [joinPoint])
            }
        } finally {
            if (typeof after === 'function') {
                after.apply(null, [joinPoint])
            }
        }
    }
}
Copy the code

Here is a simple example. Similarly, we intend to weave in the enhancement before and after the execution of the Run method of the Person class. According to the previous implementation idea:

function Person(name, age){
    this.name = name
    this.age = age
}

Person.prototype.run = function(){
    console.log(this.name + ': runing! ')}// Weave in the enhanced method
adviceAction(Person, 'run', {before:before, after:after})

var p = new Persion('the old'.28)
p.run()
Copy the code

This is an implementation of AOP for a single class method. Obviously, there are several problems:

3. An application class does not always correspond to a Aspect. 4

To sum up, a more reasonable application of class woven section Aspect would look like this:

var waveing = function(TargetClass, [Aspect1, Aspect2,...]){
    / /...
}
Copy the code

This raises another question, what should a Aspect look like? How should it be defined?

As mentioned earlier, the aspects include joinPoint and advice. The method name corresponds to joinPoint and the corresponding enhancement function corresponds to advice. So a simple Aspect would look like this:

var Aspect = {
    joinPoint1:{ before? :function{... }, after? :function{... }, around? :function{... }, throwing? :function{... }, returning? :function{...}
    },
    joinPoint2:{ before? :function{... }, after? :function{... }, around? :function{... }, throwing? :function{... }, returning? :function{... }}... }Copy the code

Accordingly we create an woven waveing function

var waveing = function(TargetClass, Aspects){
    Aspects.forEach(function(aspect){
        for(var key in aspect){
            if (aspect.hasOwnProperty(key)){
                var advices = aspect[key]
                adviceAction(TargetClass, key, advices)
            } 
        }
    })
    / /...
}
Copy the code

Take a look at the full example:

function Person(name, age) {
    this.name = name
    this.age = age
}

Person.prototype.run = function () {
    console.log(this.name + ': runing! ')
    return 'running'
}

Person.prototype.work = function () {
    console.log(this.name + ': working! ')
    return 'working'
}

function before(jp) {
    console.log('before action:' + jp.key)
}

function after(jp) {
    console.log('after action:' + jp.key)
}

function throwing(jp) {
    console.log('throw action:' + jp.exception)
}

function around(jp) {
    console.log('around action:' + jp.key)
    return jp.invoke()
}

function returning(jp) {
    console.log('returning action:' + jp.result)
}

var Aspect = {
    run: {
        before: before,
        after: after
    },
    work: {
        throwing: throwing,
        around: around,
        returning: returning
    }
}

waveing(Person, [Aspect])

var p = new Person('the old'.28)

p.run()

p.work()
Copy the code

At this point, a simplified version of the front-end AOP tool is complete. But there are still some problems.

Such as:

1. Application classes and Aspect many-to-many relationships discussed above; 2. Implementation of Aspect classes; 3. 4. Application of AOP and problems encountered and their solutions

Due to space constraints, we will continue this discussion in the next article, continuing to refine the AOP tools we implemented above.

This is just the beginning, we have much more to discuss……