在JavaScript中進行面向切面編程

什麼是面向切面編程?

面向切面編程(Aspect-oriented programming,AOP)是一種編程範式。作後端 Java web 的同窗,特別是用過 Spring 的同窗確定對它很是熟悉。AOP 是 Spring 框架裏面其中一個重要概念。但是在 Javascript 中,AOP 是一個常常被忽視的技術點。javascript

場景

假設你如今有一個牛逼的日曆彈窗,有一天,老闆讓你統計一下天天這個彈窗裏面某個按鈕的點擊數,因而你在彈窗裏作了埋點;前端

過了一個星期,老闆說用戶反饋這個彈窗好慢,各類卡頓。你想看一下某個函數的平均執行時間,因而你又在彈窗里加上了性能統計代碼。java

時間久了,你會發現你的業務邏輯裏包含了大量的和業務無關的東西,即便是一些你已經封裝過的函數。react

那麼 AOP 就是爲了解決這類問題而存在的。web

關注點分離

分離業務代碼和數據統計代碼(非業務代碼),不管在什麼語言中,都是AOP的經典應用之一。從核心關注點中分離出橫切關注點,是 AOP 的核心概念。ajax

在前端的常見需求中,有如下一些業務可使用 AOP 將其從核心關注點中分離出來編程

  • Node.js 日誌log
  • 埋點、數據上報
  • 性能分析、統計函數執行時間
  • 給ajax請求動態添加參數、動態改變函數參數
  • 分離表單請求和驗證
  • 防抖與節流

裝飾器(Decorator)

提到 AOP 就要說到裝飾器模式,AOP 常常會和裝飾器模式混爲一談。json

在 ES6+ 以前,要使用裝飾器模式,一般經過Function.prototype.before作前置裝飾,和Function.prototype.after作後置裝飾(見《Javascript設計模式和開發實踐》)。後端

Javascript 引入的 Decorator ,和 Java 的註解在語法上很相似,不過在語義上沒有一丁點關係。Decorator 提案提供了對 Javascript 的類和類裏的方法進行裝飾的能力。(儘管只是在編譯時運行的函數語法糖)設計模式

埋點數據上報

由於在使用 React 的實際開發中有大量基於 Class 的 Component,因此我這裏用 React 來舉例。

好比如今頁面中有一個button,點擊這個button會彈出一個彈窗,與此同時要進行數據上報,來統計有多少用戶點擊了這個登陸button。

import React, { Component } from 'react';
import send from './send';

class Dialog extends Component {

    constructor(props) {
        super(props);
    }

    @send
    showDialog(content) {
        // do things
    }

    render() {
        return (
            <button onClick={() => this.showDialog('show dialog')}>showDialog</button>
        )
    }
}

export default Dialog;
複製代碼

上面代碼引用了@send裝飾器,他會修改這個 Class 上的原型方法,下面是@send裝飾器的實現

export default function send(target, name, descriptor) {
    let oldValue = descriptor.value;

    descriptor.value = function () {
        console.log(`before calling ${name} with`, arguments);
        return oldValue.apply(this, arguments);
    };

    return descriptor;
}
複製代碼

在按鈕點擊後執行showDialog前,能夠執行咱們想要的切面操做,咱們能夠將埋點,數據上報相關代碼封裝在這個裝飾器裏面來實現 AOP。

前置裝飾和後置裝飾

上面的send這個裝飾器實際上是一個前置裝飾器,咱們能夠將它再封裝一下使它能夠前置執行任意函數。

function before(beforeFn = function () { }) {
    return function (target, name, descriptor) {
        let oldValue = descriptor.value;

        descriptor.value = function () {
            beforeFn.apply(this, arguments);
            return oldValue.apply(this, arguments);
        };

        return descriptor;
    }
}
複製代碼

這樣咱們就可使用@before裝飾器在一個原型方法前切入任意的非業務代碼。

function beforeLog() {
    console.log(`before calling ${name} with`, arguments);
}
class Dialog {
    ...
    @before(beforeLog)
    showDialog(content) {
        // do things
    }
    ...
}
複製代碼

@before裝飾器相似,能夠實現一個@after後置裝飾器,只是函數的執行順序不同。

function after(afterFn = function () { }) {
    return function (target, name, descriptor) {
        let oldValue = descriptor.value;

        descriptor.value = function () {
            let ret = oldValue.apply(this, arguments);
            afterFn.apply(this, arguments);
            return ret;
        };

        return descriptor;
    }
}
複製代碼

性能分析

有時候咱們想統計一段代碼在用戶側的執行時間,可是又不想將打點代碼嵌入到業務代碼中,一樣能夠利用裝飾器來作 AOP。

function measure(target, name, descriptor) {
    let oldValue = descriptor.value;

    descriptor.value = function () {
        let ret = oldValue.apply(this, arguments);
        performance.mark("startWork");
        afterFn.apply(this, arguments);
        performance.mark("endWork");
        performance.measure("work", "startWork", "endWork");
        performance
          .getEntries()
          .map(entry => JSON.stringify(entry, null, 2))
          .forEach(json => console.log(json));
        return ret;
    };

    return descriptor;
}
複製代碼

在要統計執行時間的類方法前面加上@measure就好了,這樣作性能統計的代碼就不會侵入到業務代碼中。

class Dialog {
    ...
    @measure
    showDialog(content) {
        // do things
    }
    ...
}
複製代碼

小結

面向切面編程的重點就是將核心關注面分離出橫切關注面,前端能夠用 AOP 優雅的來組織數據上報、性能分析、統計函數的執行時間、動態改變函數參數、插件式的表單驗證等代碼。

參考

相關文章
相關標籤/搜索