代码插桩技术可以让咱们在不更改已有源码的前提下,从外部注入、拦截各类自定的逻辑。这为施展各类黑魔法提供了巨大的想象空间。下面咱们将介绍浏览器环境中一些插桩技术的原理与应用实践。前端
前端插桩的基本理念,能够用这个问题来表达:假设有一个被业务普遍使用的函数,咱们是否可以在既不更改调用它的业务代码,也不更改该函数源码的前提下,在其执行先后注入一段咱们自定义的逻辑呢?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 呀~