代碼插樁技術可以讓咱們在不更改已有源碼的前提下,從外部注入、攔截各類自定的邏輯。這爲施展各類黑魔法提供了巨大的想象空間。下面咱們將介紹瀏覽器環境中一些插樁技術的原理與應用實踐。前端
前端插樁的基本理念,能夠用這個問題來表達:假設有一個被業務普遍使用的函數,咱們是否可以在既不更改調用它的業務代碼,也不更改該函數源碼的前提下,在其執行先後注入一段咱們自定義的邏輯呢?vue
舉個更具體的例子,若是業務邏輯中有許多 console.log
日誌代碼,咱們可否在不改動這些代碼的前提下,將這些 log 內容經過網絡請求上報呢?一個簡單的思路是這樣的:git
console.log
替換爲該函數。若是但願咱們的解法具有通用性,那麼不難將第一步中的操做泛化爲一個高階函數:github
function withHookBefore (originalFn, hookFn) {
return function () {
hookFn.apply(this, arguments)
return originalFn.apply(this, arguments)
}
}
複製代碼
因而,咱們的插樁代碼就很簡潔了。只須要形如這樣:瀏覽器
console.log = withHookBefore(console.log, (...data) => myAjax(data))
複製代碼
原生的 console.log
會在咱們插入的邏輯以後繼續。下面考慮這個問題:咱們可否從外部阻斷 console.log
的執行呢?有了高階函數,這一樣是小菜一碟:前端框架
function withHookBefore (originalFn, hookFn) {
return function () {
if (hookFn.apply(this, arguments) === false) {
return
}
return originalFn.apply(this, arguments)
}
}
複製代碼
只要鉤子函數返回 false
,那麼原函數就不會被執行。例以下面就給出了一種清爽化控制檯的騷操做:網絡
console.log = withHookBefore(console.log, () => false)
複製代碼
這就是在瀏覽器中「偷天換日」的基本原理了。app
單純的函數替換還不足以完成一些較爲 HACK 的操做。下面讓咱們考慮一個更有意思的場景:如何捕獲瀏覽器中全部的用戶事件?框架
你固然能夠在最頂層的 document.body
上添加各類事件 listener 來達成這一需求。但這時的問題在於,一旦子元素中使用 e.stopPropagation()
阻止了事件冒泡,頂層節點就沒法收到這一事件了。難道咱們要遍歷全部 DOM 中元素並魔改其事件監聽器嗎?比起暴力遍歷,咱們能夠選擇在原型鏈上作文章。函數
對於一個 DOM 元素,使用 addEventListener
爲其添加事件回調是再正常不過的操做了。這個方法其實位於公共的原型鏈上,咱們能夠經過前面的高階插樁函數,這樣劫持它:
EventTarget.prototype.addEventListener = withHookBefore(
EventTarget.prototype.addEventListener,
myHookFn // 自定義的鉤子函數
)
複製代碼
但這還不夠。由於經過這種方式,真正添加的 listener 參數並無被改變。那麼,咱們可否劫持 listener 參數呢?這時,咱們實際上須要這樣的高階函數:
這個函數大概長這樣:
function hookArgs (originalFn, argsGetter) {
return function () {
var _args = argsGetter.apply(this, arguments)
// 在此魔改 arguments
for (var i = 0; i < _args.length; i++) arguments[i] = _args[i]
return originalFn.apply(this, arguments)
}
}
複製代碼
結合這個高階函數和已有的 withHookBefore
,咱們就能夠設計出完整的劫持方案了:
hookArgs
替換掉傳入 addEventListener
的各個參數。listener
回調。將這個回調替換爲 withHookBefore
的定製版本。listener
添加的鉤子中,執行咱們定製的事件採集代碼。這個方案的基本邏輯結構大體形如這樣:
EventTarget.prototype.addEventListener = hookArgs(
EventTarget.prototype.addEventListener,
function (type, listener, options) {
const hookedListener = withHookBefore(listener, e => myEvents.push(e))
return [type, hookedListener, options]
}
)
複製代碼
只要保證上面這段代碼在全部包含 addEventListener
的實際業務代碼以前執行,咱們就能超越事件冒泡的限制,採集到全部咱們感興趣的用戶事件了 :)
在咱們理解了對 DOM API 插樁的原理後,對於前端框架的 API,就能夠照貓畫虎地搞起來了。好比,咱們可否在 Vue 中收集甚至定製全部的 this.$emit
信息呢?這一樣能夠經過原型鏈劫持來簡單地實現:
import Vue from 'vue'
Vue.prototype.$emit = withHookBefore(Vue.prototype.$emit, (name, payload) => {
// 在此發揮你的黑魔法
console.log('emitting', name, payload)
})
複製代碼
固然了,對於已經封裝出一套完善 API 接口的框架,經過這種方式定製它,極可能有違其最佳實踐。但在須要開發基礎庫或開發者工具的時候,相信這一技術是有其用武之地的。舉幾個例子:
到此爲止,咱們已經介紹了插樁技術的基本概念與若干實踐。若是你感興趣,一個好消息是咱們已經將經常使用的插樁高階函數封裝爲了開箱即用的 NPM 基礎庫 runtime-hooks
,其中包括了這些插樁函數:
withHookBefore
- 爲函數添加 before 鉤子withHookAfter
- 爲函數添加 after 鉤子hookArgs
- 魔改函數參數hookOutput
- 魔改函數返回值歡迎在 GitHub 上嚐鮮我司這一開源項目,也歡迎你們關注這個前端專欄噢 :)
P.S. 咱們 base 廈門的前端團隊活躍招人中,簡歷求砸 xuebi at gaoding.com 呀~