什麼是面向切面程式設計?
面向切面程式設計(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 優雅的來組織資料上報、效能分析、統計函式的執行時間、動態改變函式引數、外掛式的表單驗證等程式碼。
參考
-
《Javascript設計模式和開發實踐》