基於原型鏈劫持的前端代碼插樁實踐

代碼插樁技術可以讓咱們在不更改已有源碼的前提下,從外部注入、攔截各類自定的邏輯。這爲施展各類黑魔法提供了巨大的想象空間。下面咱們將介紹瀏覽器環境中一些插樁技術的原理與應用實踐。前端

插樁基礎概念

前端插樁的基本理念,能夠用這個問題來表達:假設有一個被業務普遍使用的函數,咱們是否可以在既不更改調用它的業務代碼,也不更改該函數源碼的前提下,在其執行先後注入一段咱們自定義的邏輯呢?vue

舉個更具體的例子,若是業務邏輯中有許多 console.log 日誌代碼,咱們可否在不改動這些代碼的前提下,將這些 log 內容經過網絡請求上報呢?一個簡單的思路是這樣的:git

  1. 封裝一個「先執行自定義邏輯,而後執行原有 log 方法的函數」。
  2. 將原生 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

對 DOM API 的插樁

單純的函數替換還不足以完成一些較爲 HACK 的操做。下面讓咱們考慮一個更有意思的場景:如何捕獲瀏覽器中全部的用戶事件?框架

你固然能夠在最頂層的 document.body 上添加各類事件 listener 來達成這一需求。但這時的問題在於,一旦子元素中使用 e.stopPropagation() 阻止了事件冒泡,頂層節點就沒法收到這一事件了。難道咱們要遍歷全部 DOM 中元素並魔改其事件監聽器嗎?比起暴力遍歷,咱們能夠選擇在原型鏈上作文章。函數

對於一個 DOM 元素,使用 addEventListener 爲其添加事件回調是再正常不過的操做了。這個方法其實位於公共的原型鏈上,咱們能夠經過前面的高階插樁函數,這樣劫持它:

EventTarget.prototype.addEventListener = withHookBefore(
  EventTarget.prototype.addEventListener,
  myHookFn // 自定義的鉤子函數
)
複製代碼

但這還不夠。由於經過這種方式,真正添加的 listener 參數並無被改變。那麼,咱們可否劫持 listener 參數呢?這時,咱們實際上須要這樣的高階函數:

  1. 把原函數的參數傳入自定義的鉤子中,返回一系列新參數。
  2. 用魔改後的新參數來調用原函數。

這個函數大概長這樣:

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 接口的框架,經過這種方式定製它,極可能有違其最佳實踐。但在須要開發基礎庫或開發者工具的時候,相信這一技術是有其用武之地的。舉幾個例子:

  • 基於對 console.log 的插樁,可讓咱們實現跨屏的日誌收集(好比在你的機器上實時查看其餘設備的操做日誌)
  • 基於對 DOM API 的插樁,可讓咱們實現對業務無侵入的埋點,以及用戶行爲的錄製與回放。
  • 基於對組件生命週期鉤子的插樁,可讓咱們實現更精確而無痛的性能收集與分析。
  • ……

總結

到此爲止,咱們已經介紹了插樁技術的基本概念與若干實踐。若是你感興趣,一個好消息是咱們已經將經常使用的插樁高階函數封裝爲了開箱即用的 NPM 基礎庫 runtime-hooks,其中包括了這些插樁函數:

  • withHookBefore - 爲函數添加 before 鉤子
  • withHookAfter - 爲函數添加 after 鉤子
  • hookArgs - 魔改函數參數
  • hookOutput - 魔改函數返回值

歡迎在 GitHub 上嚐鮮我司這一開源項目,也歡迎你們關注這個前端專欄噢 :)

P.S. 咱們 base 廈門的前端團隊活躍招人中,簡歷求砸 xuebi at gaoding.com 呀~

相關文章
相關標籤/搜索