vivipure / blog

Funkun's blog.
https://funkun.hashnode.dev/
2 stars 0 forks source link

提供业务能力,SDK的开发 #10

Open vivipure opened 2 years ago

vivipure commented 2 years ago

一.业务背景

部门负责的业务主要是 H5 营销物料的制作,会有第三方使用我们的服务制作营销物料。作为营销产品,第三方往往需要私有化部署,自己保存相关的数据。当第三方需要进行数据采集或者在我们现有的框架上进行第二次开发时,就需要我们提供相应的SDK能力给对方。

旧代码中暴露的方式是通过全局的回调函数,但是随着业务的复杂度提升,所需要的回调函数也会越来越多。而且客户有进行二次开发的需求,简单的回调函数不能满足该需求。

因此决定根据在现有的框架的基础上开发一套SDK,暴露自身的业务能力和数据给第三方。

二. 设计原则

在进行SDK设计时,我的设计原则是:

  1. 对现有业务代码侵蚀小,不污染业务逻辑
  2. 接口易懂,方便调用
  3. 不影响当前业务
  4. 扩展性强

三.实操过程

3.1 发布订阅

首先是事件分发,这里我采用了 发布订阅 的设计模式。当业务逻辑执行时,主动分发相关事件,提供给订阅者使用。

class EventEmitter {
    listener= {}
    $on(eventName, handler) {
        !this.listener[eventName] && (this.listener[eventName] = [])
        this.listener[eventName].push(handler)
    }
    $emit(eventName, data) {
        if (listener[eventName]) {
            this.listener[eventName].forEach(handler => {
                handler(data)
            })
        }
    }
}

上面就是一个简单的 发布订阅 设计模式,在此基础可以进行功能的丰富。例如直接进行全部事件的的订阅,取消订阅等逻辑。

3.2 阻止默认事件

对于业务系统的事件,在事件发生时,可以提供给用户阻止默认事件发生的能力,方便用户进行自定义操作。于是我模仿了 DOMEventListener 的模式,在执行一些事件前提供阻止事件的能力。

用户在使用时,可以这么进行调用

sdk.$on('before:<EventName>', (e) => {
    e.preventDefault()
})

在进行事件设计时,一般的业务都会分发两个事件 before:<EventName>after:<EventName>.

那在具体代码中是怎么实现这个逻辑的呢?

我是这么实现的, 先定义事件响应结果的类


class EventResponse {
    data = {}
    constructor(data, eventName, eventHandler) {
        this.data = data
        this.type = eventName
        this.preventDefault = () => eventHandler.isPrevent = true
    }
}

然后在 EventEmitter 中,将 响应的实例传递给每个订阅者

...
$emit(eventName, data) {
    let eventHandler = {
        isPrevent: false,
    }
    const eventResponse = new EventResponse(data, eventName, eventHandler)

    if (listener[eventName]) {
            this.listener[eventName].forEach(handler => {
                handler(eventResponse)
            })
    }
    return eventHandler.isPrevent
}
...

如果有订阅者调用 preventDefault 方法,则会让 isPeventfalse。然后业务代码根据返回的值决定是否继续业务逻辑,当然此处只支持同步的阻止,异步阻止并无实际的业务意义。

接下来,我们看看业务代码如何进行事件分发

3.3 事件代理

查看了旧的业务代码,业务相关的功能函数中充斥着各种回调,埋点的逻辑,实际的功能逻辑被大幅度污染。

因此我决定不在现有函数中进行事件的分发,而是利用 函数代理,实现事件的分发。

函数代理很简单,就是将原函数进行包裹,混入一些其他逻辑。

function registProxy(orgin, methodName) {
    const originMethod = origin[methodName]
    origin[methodName] = function () {
        // 其他混入逻辑
        originMethod.apply(this, arguments)
    }
}

这里我主要是混入一些 hook, 然后和之前的事件分发结合起来 ,于是我的代码是这样的

function registProxy({
    origin,
    methodName,
    beforeHook,
    afterHook,

}) {
    const originMethod = origin[methodName]
    orgin[methodName] = function (...args) {
        const isPrevent = beforeHook(...args)
        if (isPrevent) return
        originMethod(...args)
        afterHook && afterHook(...args)
    }
}

function registSyncHook(origin, methodName, eventCode) {
    registProxy({
        origin,
        methodName,
        beforeHook(...args) {
            return sdk.$emit('before:' + eventCode, args)
        },
        afterHook: (...args) => {
        return iw_emitter.$emit('after:' + eventCode, args)

        },
    })
}

这里通过注入前后 hook 实现了原有函数的代理, 使用 eventCode 和 被代理的函数, 实现了 事件的分发和默认事件的阻止。

到了目前这一步,其实已经能够满足基本的需求了,但是实际业务中包含很多异步的逻辑,通过回调函数实现函数的结束,这种情况 afterHook 就无法正常起作用了。

其实对异步的处理也很简单,对于拥有回调函数的的函数我们可以这么进行处理

 function registProxy({
    orgin,
    methodName,
    beforeHook,
    afterHook,
+      isAsync
 }) {
    ...
    // 在beforeHook改变参数中的回调函数,
+   let curArgs = args
+   const emitResponse = beforeHook(...args)
+   if (emitResponse.isPrevent) return
-      const isPrevent = beforeHook(...args)
-      if (isPrevent) return 
+   if (emitResponse.args) {
+       curArgs = emitResponse.args
+   }
+   orginMethod(...curArgs)
+   if (isAsync) return
    ...
}

function registAsyncHook(origin, method, eventCode, injectArgsFunc) {
    registProxy({
        ...
        beforeHook: (...args) => {
            // injectArgsFunc 函数更改被代理函数的 回调函数
            // 在回调函数中放入 afterhook 的逻辑
            const newArgs = injectArgsFunc(args)
            const isPrevent = iw_emitter.$emit('before:' + eventCode, args)
            // 返回新的参数和是否阻止的字段
            return {
                isPrevent,
                args: newArgs,
            }
        },
        isAsync: true
    })

}

通过对被代理函数 回调函数参数的处理,我们实现了 afterHook 对异步函数的兼容。

这是实际使用的例子

const injectTestArgsFunc = eventCode => (args) => {
    const {
        2: callback
        } = args
    const newCallback = () => {
        sdk.$emit(`after:${eventCode}`, args)
        callback && callback()
    }
    const newArgs = [...args]
    newArgs.splice(2, 1, newCallback)
    return newArgs
}

registAsyncHook(window, 'test', 'test:new', injectTestArgsFunc('test:new'))

对于支持 promise 的处理就相对简单了,这里就不进行讨论了。

3.4 总体处理

当我们将上面的工具函数写好之后,就可以对框架中需要进行暴露的业务函数进行代理,注入相关 hook, 并将相关的 eventCode 整理成文档,提供给第三方进行使用。对于函数的代理,我们可以写一个函数进行处理,在 SDK 初始化时才进行代理。这样处理的好处在于如果不使用 SDK,对现有的业务逻辑没有一点影响,而且也不影响在线的 debug

function initRegistProxy() {
    registAsyncHook(...)
    registSyncHook(...)
    ...
}

class EventEmitter {
    ...

    init() {
        initRegistProxy()
    }
    ...
}

const sdk = new EventEmitter()

// 用户使用时
sdk.init()
sdk.$on('before:<EventCode>',()=>{})

3.5 用户主动触发事件

这个逻辑也好处理,就是 发布者 和 订阅者 的身份对换即可。 业务事件函数订阅相关的 event, 用户可以使用 SDK 进行sdk.$emit('<eventCode>')发布事件即可。

四. 写在最后

经过这次 SDK 开发,加深了我对设计模式的理解,好的设计模式对于代码的解耦非常有帮助。

同时我也学会了通过 函数代理 来对函数进行扩展,避免对函数内部的业务逻辑产生影响。

当然 SDK 的开发最重要的还是文档,好的文档描述可以让开发者快速接入 SDK. 对于双方的对接效率有很大的提升。