在Javascript中進行面向切面程式設計

samciu發表於2019-04-26

什麼是面向切面程式設計?

面向切面程式設計(Aspect-oriented programming,AOP)是一種程式設計正規化。做後端 Java web 的同學,特別是用過 Spring 的同學肯定對它非常熟悉。AOP 是 Spring 框架裡面其中一個重要概念。可是在 Javascript 中,AOP 是一個經常被忽視的技術點。

場景

假設你現在有一個牛逼的日曆彈窗,有一天,老闆讓你統計一下每天這個彈窗裡面某個按鈕的點選數,於是你在彈窗裡做了埋點;

過了一個星期,老闆說使用者反饋這個彈窗好慢,各種卡頓。你想看一下某個函式的平均執行時間,於是你又在彈窗里加上了效能統計程式碼。

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

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

關注點分離

分離業務程式碼和資料統計程式碼(非業務程式碼),無論在什麼語言中,都是AOP的經典應用之一。從核心關注點中分離出橫切關注點,是 AOP 的核心概念。

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

  • Node.js 日誌log
  • 埋點、資料上報
  • 效能分析、統計函式執行時間
  • 給ajax請求動態新增引數、動態改變函式引數
  • 分離表單請求和驗證
  • 防抖與節流

裝飾器(Decorator)

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

在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 優雅的來組織資料上報、效能分析、統計函式的執行時間、動態改變函式引數、外掛式的表單驗證等程式碼。

參考

相關文章