目錄: javascript
What
(是什麼)-Why
(爲何)-How
(怎麼用)-Where
(哪裏用)闡述方法論;AOP
編程思想;JavaScript Decorator
的弊端;Decorator
;Decorator
如何傳參; 先了解一下火於後端的一個編程思想:AOP
( Aspect Oriented Programming :面向切面編程)。 也叫作面向方法編程,是經過預編譯方式和運行期動態代理的方式實現不修改源代碼的狀況下給程序動態統一添加功能的技術 。詳見:AOP 面向切面編程。歸納文章主要思想:前端
AOP
面對業務處理過程當中的某個步驟或階段,下降業務流程步驟間的耦合度;Aspect
切面AOP
是OOP
(封裝、繼承,多態)的補充和完善,AOP
實現分散關注點;AOP
是典型的代理模式體現; JavaScript
:同爲C
系列語言,Java
的AOP
那麼好用,我也要(磨刀霍霍向豬羊,期待的小眼神)。java
Decorator
無疑是對AOP
最有力的設計,在ES5
時代,能夠經過 Object.defineProperty
來對對象屬性/方法 進行訪問修飾,但用起來須要寫一堆東西;在ES6
時代,能夠經過Proxy
來對對象屬性 / 方法進行訪問修飾。Decorator
已經在ES7
的提案中,也就是叫你敬請期待;藉助Babel轉碼工具,咱們能夠先吃螃蟹。webpack
Decorator
,能夠不侵入原有代碼內部的狀況下修改類代碼的行爲,處理一些與具體業務無關的公共功能,常見:日誌,異常,埋碼等。ES7 Decorator提案描述以下:ios
A decorator is:git
思想卻是理解了,上面的翻譯可能有出入,由於老感受第4點翻譯的不夠貼切,歡迎斧正。github
首先拋開「迎合」後端開發人員的Class
寫法,對應會引入的相關概念和特性,固然隨着前端業務的發展,有時候也須要對應的特性。好比:private
,static
等見於Java
的特性,現現在經過Class
語法糖能在前端實現一樣的特性,換湯不換藥,語法糖底層仍是經過原生JavaScript
來模擬實現相關特性的。前端Class
編寫風格更加"後端",那麼就更容易吸引一大波後端人員學習和使用Javascript
,Javascript
一統編程界「指日可待」。後端都學Javascript
了,這讓純前端壓力山大,咱們要加快學習的腳步才行,技多不壓身,觸類旁通學習,把後端的空氣都咬完,讓後端沒法呼吸。web
其次舉個栗子闡述爲何要用Decorator
:現實生活中咱們可能也遇到過,百度過一個商品後,打開淘寶京東後,淘寶京東便能精準的推薦該商品的相關廣告,大數據時代,咱們慢慢愈來愈透明。轉換爲專業術語:埋碼。express
埋碼具體需求以下:大數據時代,數據就是金錢,業務方須要統計用戶對某些功能的使用狀況,好比使用時間,頻率,用戶習慣等。對應後端會提供一個埋碼接口,用戶調用功能的時候,前端須要 侵入式 的在全部須要統計的功能前調用後端埋碼接口。編程
原始寫法:
// 埋碼 監聽用戶使用狀況
function monitor(name) {
console.log(`調用監聽接口,發送監聽數據 : ${name}`)
}
class PageAPI {
onWatch() {
monitor('侵入式:帥哥靚妹X訪問了xxx')
console.log('原訪問頁面邏輯')
}
onLike() {
monitor('侵入式:帥哥靚妹X點讚了xxx')
console.log('原點贊正常邏輯')
}
onAttention() {
monitor('侵入式:帥哥靚妹X關注了xxx')
console.log('原關注正常邏輯')
}
onBack(){
console.log('退出相關邏輯,不須要監聽')
}
}
const page = new PageAPI()
// 各類暗示點贊,關注,各位看官你懂的,哈哈
page.onWatch()
page.onLike()
page.onAttention()
page.onBack()
複製代碼
打印結果:
使用Decorator
寫法以下:
// 埋碼 監聽用戶使用狀況
function monitor(name) {
// 注意:
// 實際中埋碼數據是從一個單例store裏面取
// 好比,用戶名,訪問時間等
// 操做類型可做爲`Decorator`參數
console.log(`調用監聽接口,發送監聽數據 : ${name}`)
}
/** Decorator 定義: 1. an expression(一個表達式) 2. that evaluates to a function(等價於一個函數) 3. that takes the target, name, and decorator descriptor as arguments(參數有target,name,descriptor ) 4. and optionally returns a decorator descriptor to install on the target object(可選的返回一個裝飾器描述符以安裝在目標對象上) * @param {*} name */
const monitorDecorator = (name) => { // Decorator 定義2
return (target, propertyKey, descriptor) => {// Decorator 定義3
const func = descriptor.value
return { // Decorator 定義4
get() {
return (...args) => {
monitor(name) // 埋碼
func.apply(this, args) // 原來邏輯
}
},
set(newValue) {
return newValue
}
}
}
}
class PageAPI {
@monitorDecorator('Decorator:帥哥靚妹X訪問了xxx') // Decorator 定義1
onWatch() {
console.log('原訪問頁面邏輯')
}
@monitorDecorator('Decorator:帥哥靚妹X點讚了xxx')
onLike() {
console.log('原點贊正常邏輯')
}
@monitorDecorator('Decorator:帥哥靚妹X關注了xxx')
onAttention() {
console.log('原關注正常邏輯')
}
onBack() {
console.log('退出相關邏輯,不須要監聽')
}
}
const page = new PageAPI()
// 各類暗示點贊,關注,各位看官你懂的,哈哈
page.onWatch()
page.onLike()
page.onAttention()
page.onBack()
複製代碼
打印結果:
通過上面的栗子應該能直觀的感覺到面向切面編程核心:非侵入式,解耦。
不火的緣由主要爲:
ES7
提案中,還未獲得官方支持;Function
寫法支持不友善,不少用戶和框架依然都用Function寫法,好比:Vue 3.0
、React Hook
等都推崇Function
寫法,畢竟Javascript
從骨子裏就是用Function編程。Decorator
暫時不能串聯,存在覆蓋問題; 目前標準還未支持Decorator,可是Babel已經支持相關寫法,咱們能夠經過get
、set
來模擬實現。根據Decorator
不一樣的切入點能夠分爲:Class
,Method
和Property
三種Decorator
。順帶介紹一下原生Function如何實現面向切面編程。
在自我搭建的Webpack
項目中使用Decorator
,運行項目編譯失敗,終端報錯,並提供了對應的解決方法。按照提示操做,便能在自我搭建的webpack項目使用Decorator
了。
另外,親測,在新版Vue-cli
項目中已經默認支持Decorator
寫法了
切入點爲Class
,修飾整個Class
,能夠讀取和修改類的方法和屬性。須要傳遞參數,能夠經過高階的函數來實現傳遞參數,以下面的classDecoratorBuilder
。
// 埋碼 監聽用戶使用狀況
function monitor(name) {
console.log(`調用監聽接口,發送監聽數據 : ${name}`)
}
const classDecoratorBuilder = (dataMap) => {
return target => {
// ! 此處不能用 Object.entries(target.prototype) --> []
Object
.getOwnPropertyNames(target.prototype)
.forEach(key => {
console.log(target)
if (!['onBack'].includes(key)) { // 屏蔽某些操做
const func = target.prototype[key]
target.prototype[key] = (...args) => {
monitor(dataMap[key] || '埋碼數據') // 埋碼
func.apply(this, args) // 原來邏輯
}
}
})
return target
}
}
const dataMap = {
onWatch: 'class Decorator:帥哥靚妹X訪問了xxx',
onLike: 'class Decorator:帥哥靚妹X點讚了xxx',
onAttention: 'class Decorator:帥哥靚妹X關注了xxx',
}
const classDecorator = classDecoratorBuilder(dataMap)
@classDecorator
class PageAPI {
onWatch() {
console.log('原訪問頁面邏輯')
}
onLike() {
console.log('原點贊正常邏輯')
}
onAttention() {
console.log('原關注正常邏輯')
}
onBack() {
console.log('退出相關邏輯,不須要監聽')
}
}
const page = new PageAPI()
// 各類暗示點贊,關注,各位看官你懂的,哈哈
page.onWatch()
page.onLike()
page.onAttention()
page.onBack()
複製代碼
運行結果以下:
切入點爲Method
,修飾方法,和Class Decorator
功能類似,能額外的獲取修飾的方法名。詳見 Why 中的栗子。這裏就不贅述了。
切入點爲屬性,修飾屬性,和Class註解功能功能相同,能額外的獲取修飾的屬性名。
const propertyDecorator = (target, propertyKey) => {
Object.defineProperty(target, propertyKey, {
get() {
return 'property-decorator-value'
},
set(val) {
return val
}
})
}
class Person {
@propertyDecorator
private name = 'default name'
sayName(){
console.log(`class Person name = ${this.name}`)
}
}
new Person().sayName()
複製代碼
運行結果以下:
Decorator
優先級,串聯 Java的Decorator
功能強大,不只有豐富的Decorator
,並且Decorator
還能夠串聯。壞消息:親測JavaScript Decorator
不能串聯,存在覆蓋問題,也就是優先級關係:Method Decorator
> Class Decorator
。當一個Method
上定義了Decorator
,則Class Decorator
則不起做用。但願ES7
標準能解決這個痛點。
const classDecoratorBuilder = (name) => {
return target => {
Object
.getOwnPropertyNames(target.prototype)
.forEach(key => {
const func = target.prototype[key]
target.prototype[key] = (...args) => {
console.log(`>>>>> class-decorator ${name}`)
func.apply(this, args)
}
})
return target
}
}
const methodDecoratorBuilder = (name) => {
return (target, propertyKey, descriptor) => {
const func = descriptor.value
return {
get() {
return (...args) => {
console.log(`>>>>> method-decorator ${name}`)
func.apply(this, args)
}
},
set(newValue) {
return newValue
}
}
}
}
const classDecorator1 = classDecoratorBuilder(1)
const classDecorator2 = classDecoratorBuilder(2)
const methodDecorator1 = methodDecoratorBuilder(1)
const methodDecorator2 = methodDecoratorBuilder(2)
const propertyDecorator = (target, propertyKey) => {
Object.defineProperty(target, propertyKey, {
get() {
return 'property-decorator-value'
},
set(val) {
return val
}
})
}
// Decorator不能串聯
// @classDecorator1
@classDecorator2
class Person {
@propertyDecorator
private name = 'default name'
// @methodDecorator1 // 不能串聯,會報錯
@methodDecorator2 // class Decorator會被覆蓋
sayName() {
console.log('sayName : ', this.name)
}
eat(food) {
console.log('eat : ', food)
}
}
const person = new Person()
person.sayName()
person.eat('rice')
複製代碼
運行結果以下:
Decorator
」 Decorator
目前只能應用於Class
,不能用於修飾Function
,由於Function
的執行上下文是不肯定的,太靈活了。可是AOP
編程思想是先進的,合理的。咱們能夠採用不一樣的形式來實現Function
的AOP
,雖然沒Decorator
那麼優雅。經過這種方式還能夠解決Decorator
串聯的痛點。
function monitor(name) {
console.log(`調用監聽接口,發送監聽數據 : ${name}`)
}
const functionAOP = (name, fn) => {
return (...args) => {
monitor(name)
fn.apply(this, args)
}
}
let onWatch = (pageName) => {
console.log('原訪問頁面邏輯,訪問頁面:', pageName)
}
let onLike = (pageName) => {
console.log('原點贊正常邏輯,求點贊:', pageName)
}
let onAttention = (author) => {
console.log('原關注正常邏輯,求關注:', author)
}
// 相似`Decorator`
onWatch = functionAOP(
'****我串聯啦****',
functionAOP('functionAOP:帥哥靚妹X訪問了xxx', onWatch)
)
onLike = functionAOP('functionAOP:帥哥靚妹X點讚了xxx', onLike)
onAttention = functionAOP('functionAOP:帥哥靚妹X關注了xxx', onAttention)
onWatch('JavaScript好用還未火的`Decorator`@Decorator')
onLike('JavaScript好用還未火的`Decorator`@Decorator')
onAttention('JS強迫症患者')
複製代碼
運行結果以下:
AOP在前端的應用場景包括日誌記錄、統計、安全控制、事務處理、異常處理、埋碼等與業務關聯性不強的功能。上面栗子已經詳細介紹了AOP在埋碼上的應用,下面再詳細介紹一個經常使用場景:異常處理。
一個好的應用,用戶體驗要良好,當用戶使用核心功能,不管功能是否成功,都但願獲得一個信息反饋,而不是感受不到功能是否有運行,是否成功。核心功能運行成功的時候彈出消息:xxx功能成功;失敗的時候彈出錯誤:xxx功能失敗,請xxx之類。
廢話很少說,直接擼代碼。因爲是模擬代碼,一是爲了節省時間,二是爲了各位看官能夠一覽無遺,博主就不拆解文件了。合理的結構應該將API
,Decorator
,頁面邏輯拆解到對應文件中,以供複用。
生成模擬接口的公共代碼:
const promiseAPIBuilder = (code) => { // 模擬生成各類接口
return new Promise((resolve, reject) => {
setTimeout(() => {
if (code === 0) {
resolve({
code,
message: 'success',
data: []
})
} else if (code === 404) {
reject({
code,
message: '接口不存在'
})
} else {
reject({
code,
message: '服務端異常'
})
}
}, 1000)
})
}
複製代碼
咱們能夠修改axios
攔截器,當狀態code
非0的時候一概認爲功能失敗,統一reject
錯誤信息,最後在API
調用處catch
內統一作錯誤信息彈出。相應弊端:多處接口調用處都須要增長與業務無關的catch
方法或者用try catch
處理。
const api = {
successAPI() {
return promiseAPIBuilder(0)
},
error404API() {
return promiseAPIBuilder(404)
},
errorWithoutCatchAPI() { // 沒有catch error
return promiseAPIBuilder(500)
}
}
const successAPI = async () => {
const res = await api
.successAPI()
.catch(error => console.log(`多個catch的error : ${error.message}`))
if (!res) return
console.log('接口調用成功後的邏輯1')
}
successAPI()
const error404API = async () => {
const res = await api
.error404API()
.catch(error => console.log(`消息提示:多個catch的error : ${error.message}`))
if (!res) return
console.log('接口調用成功後的邏輯2')
}
error404API()
const errorWithoutCatchAPI = async () => {
const res = await api.errorWithoutCatchAPI() // error 沒有 catch
if (!res) return
console.log('接口調用成功後的邏輯3')
}
errorWithoutCatchAPI()
複製代碼
運行結果:
定義全局異常處理函數。相應弊端:狀況多的話須要作不少case
判斷,由於引用不少沒攔截的異常都會跑到全局異常處理函數。
const api = {
successAPI() {
return promiseAPIBuilder(0)
},
error404API() {
return promiseAPIBuilder(404)
},
errorWithoutCatchAPI() { // 沒有catch error
return promiseAPIBuilder(500)
}
}
// 統一處理
window.addEventListener('unhandledrejection', (event) => {
if (event.reason.code === 404) {
console.log(` 消息提示:統一catch的error, 須要經過if或者switch判斷處理流程 : ${event.reason.message} `)
}
})
const successAPI = async () => {
const res = await api.successAPI() // error 沒有 catch
if (!res) return
console.log('接口調用成功後的邏輯1')
}
successAPI()
const error404API = async () => {
const res = await api.error404API() // error 沒有 catch
if (!res) return
console.log('接口調用成功後的邏輯2')
}
error404API()
const errorWithoutCatchAPI = async () => {
const res = await api.errorWithoutCatchAPI() // error 沒有 catch
if (!res) return
console.log('接口調用成功後的邏輯3')
}
errorWithoutCatchAPI()
複製代碼
運行結果:
Decorator
Decorator
修飾API
接口管理文件。雖然說也有Class
寫法的限制,可是咱們能夠經過其餘方式避開這個限制。注意帶*號的代碼
// ****** catch error Decorator 構造器
const showTipDecoratorBulder = (errorHandler) => (target, propertyKey, descriptor) => {
const func = descriptor.value
return {
get() {
return (...args) => {
return Promise
.resolve(func.apply(this, args))
.catch(error => {
errorHandler && errorHandler(error)
})
}
},
set(newValue) {
return newValue
}
}
}
// ****** 構造一個提示錯誤的`Decorator`
const showTipDecorator = showTipDecoratorBulder((error) => {
console.log(`Decorator error 消息提示 : ${error.message}`)
})
// ****** class 寫法避開限制
class PageAPI {
@showTipDecorator
successAPI() {
return promiseAPIBuilder(0)
}
@showTipDecorator
error404API() {
return promiseAPIBuilder(404)
}
errorWithoutCatchAPI() {
return promiseAPIBuilder(500)
}
}
const api = new PageAPI()
const successAPI = async () => {
const res = await api.successAPI() // error 沒有 catch
if (!res) return
console.log('接口調用成功後的邏輯1')
}
successAPI()
const error404API = async () => {
const res = await api.error404API() // error 沒有 catch
if (!res) return
console.log('接口調用成功後的邏輯2')
}
error404API()
const errorWithoutCatchAPI = async () => {
const res = await api.errorWithoutCatchAPI() // error 沒有 catch
if (!res) return
console.log('接口調用成功後的邏輯3')
}
errorWithoutCatchAPI()
複製代碼
運行結果:
附送:如何判斷一個函數爲AsyncFucntion
。
/** * 附送:如何判斷一個函數爲AsyncFucntion */
const asyncFn = async _ => _
const fn = _ => _
// AsyncFucntion非JS內置對象,不能直接經過以下方式判斷
// console.log('<<<< asyncFn instanceof AsyncFucntion <<<', asyncFn instanceof AsyncFucntion)
console.log('<<<< asyncFn instanceof Function <<<', asyncFn instanceof Function) // true
console.log('<<<< fn instanceof Function <<<', fn instanceof Function) // true
const AsyncFucntion = Object.getPrototypeOf(async _ => _).constructor
console.log('<<<< asyncFn instanceof AsyncFucntion <<<', asyncFn instanceof AsyncFucntion) // true
console.log('<<<< fn instanceof AsyncFucntion <<<', fn instanceof AsyncFucntion) // false
複製代碼
運行結果:
都看到這裏了,點個贊,關注再走唄。