AOP在前端领域的实践

本文首发于个人博客 https://maclaren0920.github.io

AOP的基本概念

什么是AOP?首先我们看一下来自维基百科的一段简单概述:

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与装饰,如“对所有方法名以‘set*’开头的方法添加后台日志”。该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。面向切面的程序设计思想也是面向切面软件开发的基础。

AOP这个词相对于后端开发来说会比较常见,比如java中大名鼎鼎的spring,反之前端却比较陌生,是一个常常被忽视的技术,其主要和javascript是一门多范式、弱面向对象的语言有一定的关系。

为什么需要AOP

面向对象编程是针对业务处理中软件实体、行为和属性进行的封装和抽象,而面向切面编程是针对具体业务中核心功能和其他业务中的职责和逻辑分离,来减少系统的耦合度。

面向切面编程(即AOP)是一种新的编程方式,但是并不是为了替代面向对象编程,它只是对面向对象的一种补充和完善,它们两者是一种相辅相成的关系。

使用场景

在前端常见的需求中,以下业务场景中经常能看到AOP的影子:

  • 表单验证
  • 数据埋点、数据上报
  • 基于老旧项目扩展新需求
  • 异常处理

下面我们来看下AOP在代码中的具体实现方式。

应用实例

前面说了一大堆的理论,好像感觉并没什么用,下面我用实例案例来看看AOP的实际应用。

1. 数据埋点、数据上报

在实际业务开发中,我们可能会有数据数据埋点的需求,比较传统的实现方式应该是这样:

const sendPoint = () => {
    axios('https://www.xxx.com');
    ...
}
const onSubmit = () => {
    sendPoint();
    
    // 提交逻辑...
}

onSubmit();

这好像并没有什么问题,但是onSubmit函数并不依赖于sendPoint,甚至在onSubmit中sendPoint完全是多余的,那么我们用AOP来优化下它:

首选实现下AOP函数:

Function.prototype.before = function(fn) {
  const self = this;

  return function () {
    fn.apply(this, arguments);
    return self.apply(this, arguments);
  }
}

然后调用onSubmit就变成了:

onSubmit.before(sendPoint)();

你可能不太喜欢这种基于原型扩展的方式,这样污染了原型,我们同样可以用普通函数来实现:

const before = function (originalFn, fn) {
  const self = this;

  return function () {
    fn.apply(this, arguments);
    return originalFn.apply(this, arguments);
  }
}

然后修改下onSubmit函数的定义:

const onSubmit = before(() => {
    sendPoint();
    
    // 提交逻辑...
}, sendPoint);

onSubmit();

这样onSubmit和sendPoint函数的逻辑就完全分离了。

2. 表单验证

表单验证是实际开发中非常常见的需求,在有些项目中可能会存在非常多的表单;每次在提交的接口时,都需要做数据校验来确保传递给后端的数据是正确的。


const validate = (params) => {
    if (!params.username) {
        return false;
    }
    
     if (!params.password) {
        return false;
    }
    ...
    return true;
}

const onFormSubmit = () => {
    const bool = validate(params);
    if (bool) {
        axios('xxxx')
        ...
    } esle {
    ...
    }
}

onFormSubmit();

以上代码是经常会遇到的,仔细分析下,onFormSubmit只是一个提交接口,却承担了提交数据到后台和数据校验的职责,接下来我们用AOP来对它们进行优化:

这里参考以上数据埋点的示例,AOP函数的实现方式基本一致,只是表单验证这里的参数校验函数需要返回一个boolean值,我们对此进行一下优化:

Function.prototype.before = function(fn) {
  const self = this;

  return function () {
    const bool = fn.apply(this, arguments);
    if (!bool) {
        return;
    }
    return self.apply(this, arguments);
  }
}

然后调用onFormSubmit的方式就变成了:

onFormSubmit.before(validate)();

这样onFormSubmit是不是就变成了一个非常干净的提交接口,和数据校验的逻辑分开了呢,并且后续如果有数据校验的的变动,只需要修改validate函数即可;并不需要修改提交接口的代码,这也完全符合软件设计中的SRP职责。

3. 基于老旧项目扩展新需求

在实际开发中,经常会遇到需要在之前老项目的基础上增加一些新的需求,但是该项目可能开发年代比较久远,之前的开发人员可能已经各奔东西,你却需要在此基础上来修改代码,作为开发人员往往会面临这种局面,参考以下代码:

function execute () {
    // big code blocks
    ... 以下省略几百行代码
}

你需要在execute函数中增加一些新的逻辑,但是在big code blocks中有大量复杂的业务逻辑,你并不敢随意修改,这可能会导致意想不到的bug出现,这时候我们用AOP来对它进行扩展,基于以上数据埋点的示例,我们来实现下AOP函数:

Function.prototype.after = function(fn) {
  const self = this;

  return function () {
    const result = self.apply(this, arguments);
    fn.apply(this, arguments);
    return result;
  }
}

然后之前的调用方式变成了:

execute.after(() => {
    // 新增的业务需求
    ...
})();

以上就实现对之前原有逻辑的扩展,在不修改之前代码的提前了,增加新的业务逻辑代码。

4. 异常处理

基于以上表单的验证的示例,在提交接口中往往会遇到异常处理的情况,如果在多个接口中异常做一个同样的处理,传统的方式是在方法中加一个异常捕获,具体如下:

const handlerError = (err) => {
    ...
}

const fetchData = async () => {
    try {
         await axios('https://xxxxx.com/xxx', data);
    } catch (error) {
        // handler error
        handlerError(error);
    }
}

let fetchData1 = async () => {
    try {
         await axios('https://yyyyy.com/xxx', data);
    } catch (error) {
        // handler error
        handlerError(error);
}

fetchData();
fetchData1();

如果是多个方法中都需要做这样的处理,那么将会使代码中充斥着大量的try catch,这将使代码变的非常臃肿和难以维护,其实catch中的异常处理和实际的业务功能并没有什么必然关系。

下面我们用AOP的方式将它们的之间的逻辑进行分离,因为异常处理所依赖的前置条件是接口调用,因为它是异步的,所以实现方式和以上表单略有不同,我们需要返回一个异步函数。

首先实现下异常处理的AOP函数:

const errorCatch = (fn, handlerError) => {
  const self = this;
  return async function () {
    try {
      return await fn.apply(this, arguments);
    } catch (err) {
      handlerError.call(this, err);
    }
  }
}

然后在定义fetchData函数时就变成了:

const fetchData = errorCatch(async () => {
    await axios('https://xxxxx.com/xxx', data);
}, handlerError);

这样fetchData和handlerError的逻辑就完全分开了,并且fetchData中也不再充斥着try catch。

扩展

以上的示例中AOP函数的实现有两种方式:

  • 原型扩展
  • 函数劫持

在ES6之后推出了装饰器,我们一样可以用装饰器来实现:

function before() {
  return function (target, property, descriptor) {
    const fn = descriptor.value;
    descriptor.value = function() {
      console.log(`before...`)
      let value = fn.apply(this, arguments);
      return value;
    };
  }
}

function after() {
  return function (target, property, descriptor) {
    const fn = descriptor.value;
    descriptor.value = function() {
      let value = fn.apply(this, arguments);
      console.log(`after...`)
      return value;
    };
  }
}

定义一个测试类:
class Person {
  @before()
  @after()
  run() {
    console.log(`run log`);
  }
}

const p = new Person();
p.run();

// before ...
// run log
// after ...

以上代码通过ES6装饰器的方式实现了以上示例的AOP功能,在使用的时候通过类似于java中注解@的方式来使用AOP函数。

但是通过装饰器的实现有个问题,装饰器必须使用在类中或者类的方法上,对于传统定义的函数并不支持这种用法,因为在js有函数存在提升。

AOP函数的实现方式有多种,具体使用哪种方式来实现,完全取决于自己的喜好和具体的应用场景。

总结

以上通过4个常见的场景来说明AOP的实际应用和落地,但实际业务开发中往往有更多场景也同样会遇到这样的问题,这需要大家对函数所承载的职责和实现有更充分的理解。所谓的AOP其实就是设计模式中装饰者模式的一种实现,其原理就是对函数劫持和扩展。

通过以上示例总结出AOP的几个优点:

  • 将核心代码逻辑和其他业务逻辑进行分离,减少业务间的耦合度
  • 在不修改原有代码的前提下,扩展新的业务逻辑代码,践行开闭原则
  • 减少重复代码
  • 提高系统的高内聚、低耦合特性

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352

推荐阅读更多精彩内容

  • 本文同步发表在豆米的博客:豆米的博客 1、前言 如今的编程模型有很多种,常用的是面向过程编程(POP)、面向对象编...
    小兀666阅读 1,219评论 2 6
  • 设计模式 一、单例模式 definition:保证一个类仅有一个实例,并提供一个访问它的全局访问点。 普通单例模式...
    了凡和纤风阅读 3,875评论 0 3
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,621评论 18 399
  • 一. Java基础部分.................................................
    wy_sure阅读 3,810评论 0 11
  • 简介 先给出before和after这2个“切面”函数. 顾名思义,就是让一个函数在另一个函数之前或者之后执行,巧...
    Allan要做活神仙阅读 1,437评论 0 1