hybrid-jssdk-backbone

0.1.6 • Public • Published

hybrid-jssdk-backbone

可用于web与app之间进行混合开发的jssdk开发骨架,通过约定的方式与app快速打通服务。

背景

网页与app之间的交互,是非常重要的开发内容。如果网页想要与app发生交互,app必须给webview注入特殊地全局对象,以便网页中的脚本能够借助这个全局对象访问app提供的api。app如何给webview注入全局对象,安卓端可使用这个库JsBridge, iOS可以使用这个库WebViewJavascriptBridge,它们都会给网页注入一个WebViewJavascriptBridge的全局对象。也就是说,网页与app之间的交互问题,其实就是使用WebViewJavascriptBridge这个对象的问题。那使用它难吗,当然不难,前面两个库的文档都有用法介绍;但是这一块的代码,决不应该随随便便写,应该朝着SDK的目标去写,这样可以统一管理所有网页与app之间的交互逻辑,尤其当这些技术服务要应用于多个产品的时候,SDK更能发挥价值。hybrid-jssdk-backbone就是为了简化SDK的封装而产生的。

hybrid-jssdk-backbone的作用是帮助你封装你的产品的SDK,毕竟每个产品在做网页与app交互时,app能提供哪些api,各个产品都是不同的,所以hybrid-jssdk-backbone不主动注册app的api,它不知道app会提供哪些api,但是它提供了registerApi这个方法,可以帮助你来注册自己的项目或产品中,跟app的开发同事一起协商出来的app调用;同时它约定了一套注册api的统一规则,以便能够把api的调用结果和回调函数约束成同一个方式,使sdk更易于使用;第三,它提供了一个ready这样的函数,它会返回Promise实例,通过它注册then回调,在此回调内调用sdk,会更加安全,毕竟WebViewJavascriptBridge对象的获得是一个异步过程:

sdk.ready().then(sdk => {
    if(sdk) {
        // call app's api through sdk, like `sdk.openNativeView(...)`
    } else {
        // sdk is not available
    }
})

接下来看看如何利用hybrid-jssdk-backbone封装一个自己的SDK。

如何使用

安装

npm install hybrid-jssdk-backbone --save

第一步:构造SDK实例

import HybridSdk from 'hybrid-jssdk-backbone'

const sdk = new HybridSdk({
    debug: process.env.NODE_ENV !== 'production',
    isNative() {
        return window.navigator.userAgent.indexOf('MyProduct) > -1
    }
})

这个就是为了构造出一个可以进行扩展的SDK实例。HybridSdk的构造函数支持以下几个参数:

  • isNative
    • default: false,
    • type: Function
    • desc: 你需要传入一个函数,以便SDK内部能够用来识别当前是在自己app的原生的webview环境中。这个地方千万注意,记得一定要提醒你的app开发同事,往webview的userAgent里面注入产品的标识和版本号,就像微信一样,直接通过userAgent就能判断出是不是在微信的客户端。
  • timeout
    • default: 0,
    • type: Number
    • desc:如果isNative处理好,这个timeout参数毫无价值。它只是为了兼容那些一开始没有想到给userAgent注入自身标识的产品,因为一旦产品已经发布,就不得不去兼容老版本,而老版本没有自身标识,所以导致网页在老版本里无法立马就判断这是自己的产品环境,所以提供timeout参数,就是为了延迟一下后去判断WebViewJavascriptBridge对象是否存在,从而判断出是否是native环境,以便后面介绍的ready函数返回的Promise还能被resolve。
  • debug
    • default: false
    • desc: 开启后,会在控制台打印日志,便于真机调试。
  • logger
    • default: 'hybrid-jssdk'
    • default: 控制台打印时的前缀。

一定要在产品的第一版就往userAgent里注入产品标识,保证isNative能够准确判断是否是自身产品的webview环境。

第二步:注册一个api

上一步构建出来的sdk,可通过registerApi来注册api。注册的api是客户端提供给网页进行调用的,所以每一个api,都离不开客户端配合一起开发。

sdk.registerApi('openNativeView', {
    parseData(apiName, response) {
        
    },
    beforeInvoke(apiName, params) {
        
    },
    afterInvoke(apiName, nativeData) {

    }
})

registerApi这个函数接收2个参数,第一个参数是apiName,也就是你跟app同事约定的api名称。第二个参数,是一个options,它用来自定义以下三个回调函数:

  • parseData

    • default: Function
    • type: Function
    • desc: 这个回调函数用来对api调用后,app返回的原生信息进行解析,它有两个参数,第一个是对应的api名称apiName,第二个是app返回的原生信息是一个字符串response。为了统一api的调用结果和api回调函数的使用,约定:
      • response必须用json格式
      • response所包含的json数据,必须有一个message字段,它是一个字符串,且这个message字段,必须按照apiName:ok|cancel|fail的格式进行组织。比如openNativeView这个api,message有三种组织形式,分别是openNativeView:ok,openNativeView:cancel,openNativeView:fail,代表接口调用成功,调用取消,以及调用失败的含义。当使用sdk.openNativeView时,是根据message决定要调用哪个回调函数的。ok|cancel|fail不是全部都要的,但至少要有ok,其它两个取决于api的逻辑是否需要它。如果某个api调用,会引发app弹出对话框,那么当用户在对话框内点击取消的时候,就可以回调cancelmessage。 这个option有默认值,使用的是下面这个函数逻辑:
    function defaultDataParser(apiName, response) {
        try {
            let data = JSON.parse(response)
            if (isObjectType(data, 'Object')) {
                return data
            } else {
                return {
                    message: response
                }
            }
        } catch (e) {
            logError(`${apiName} parse error:`, e)
            return {
                message: response
            }
        }
    }

    如果app端遵照以上约定的方式实现api,则此回调函数应该不需要设置。

  • beforeInvoke

    • default: Function
    • type: Function
    • desc: 这个函数在api被触发前调用,用来处理要传递给app对应api的参数,它接收2个参数,第一个参数是对应的api名称apiName,第二个参数是api被调用时传入的参数params。此函数内可对params进行修改,然后才会invoke到app。此函数可返回一个新对象,将直接替代原params,传递至app进行调用。
  • afterInvoke

    • default: Function
    • type: Function
    • desc: 这个函数在parseData之后调用,可对parseData之后的数据,做进一步加工(比如当app没有按约定写response的时候)。它接收2个参数,第一个参数是对应的api名称apiName,第二个参数是parseData的返回值nativeData。此函数内可对nativeData进行修改,此函数也可返回一个新对象,将直接替代原nativeData,进行后续的逻辑。后续的逻辑其实非常简单,就是根据nativeDatamessage属性,判断出api调用的结果,然后调用对应的api的回调函数,后续的核心代码:
      let message = nativeData.message
      log(`${apiName} message:`, nativeData.message)
      
      if (!message) {
          return
      }
      
      let semiIndex = message.indexOf(':')
      switch (message.substring(semiIndex + 1)) {
          case "ok":
              success(nativeData)
              break
          case "cancel":
              cancel(nativeData)
              break
          default:
              fail(nativeData)
      }
      complete(nativeData)

当使用sdk.registerApi('openNativeView')之后,sdk实例上就会多出一个api方法openNativeView,后续在功能开发当中,直接使用sdk.openNativeView(...)即可触发对app的原生调用。

如果一切按约定编码,那么sdk.registerApi在注册api的时候,不需要第二个参数。

第三步:使用sdk注册的api

使用以下方式,来使用上面注册的api:

sdk.ready().then(sdk => {
    if(sdk) {
        sdk.openNativeView({
            link: '....'
        }, {
            success() {

            },
            fail() {

            },
            cancel() {

            },
            complete() {

            }
        });
    }
})

每一个api在注册后,都通过sdk[apiName]的方式调用,这个调用接收2个参数,第一个参数是api调用所需要的参数object,第二个参数是一个options对象,用来配置api调用的回调函数。所有通过registerApi注册的api,都采用相同的回调函数:success fail cancel complete,含义如其字面意思一样;这个函数也可以只传递一个参数,把success fail cancel complete混合进第一个参数里面,在调用的时候,会把success fail cancel complete分离出来,就像这样:

sdk.ready().then(sdk => {
    if(sdk) {
        sdk.openNativeView({
            link: '....',
            success() {

            },
            fail() {

            },
            cancel() {

            },
            complete() {

            }
        });
    }
})

另外sdk还提供了一个ready函数,这个函数调用后会返回一个Promise,在它的then回调内,可直接判断回调参数是否为真,继续是否进行native的调用。因为sdk内部需要初始化WebViewJavascriptBridge对象,而这个对象的注入过程是异步的,所以单独封装了一个ready函数。

APP端如何写

android开发示例:

webView.registerHandler("openNativeView", new BridgeHandler() {
    @Override
    public void handler(String data, CallBackFunction function) {
        // todo finish openNativeView
        function.onCallBack("{\"message\": \"openNativeView:ok\"}");
    }
});

iOS开发示例:

[self.bridge registerHandler:@"openNativeView" handler:^(id data, WVJBResponseCallback responseCallback) {
	responseCallback(@"{\"message\": \"openNativeView:ok\"}");
}];

强调

  • app端一定要通过userAgent注入产品标识
  • app端写api的回调数据,一定要转为json格式,且要包含message属性

其它方法

为了便于扩展,sdk还提供了以下几个实例方法:

  • getBridge()

    这个方法调用后返回底层的bridge对象,不过可能为空,所以要注意调用时机。

  • toJson(value: string)

    这个方法调用后将传入的字符串进行JSON.parse(value)的转换,如果转换出错,则返回null,否则返回转换后的值。

  • invoke (apiName: string, params:object, callback:function)

    这个方法接收三个参数,通过getBridge()返回的bridge对象,直接调用客户端的api。 这是最直接、最原始的调用方式,所以这个方式调用客户端的api,不会有上面所有描述的那些服务。

  • register (apiName: string, response:string, callback:function)

    这个方法接收三个参数,通过getBridge()返回的bridge对象,注册给客户端进行调用的前端的api。 注意是前端的api!因为网页提供方法给app调用的场景实际上并不多,所以本库也未对这样的场景进行过多的封装。但是也不排除有需要这个方式的场景,所以提供出来,方便扩展。下面介绍event-bus正好需要这个。

event-bus

如果网页是一个单页应用,那么event-bus可能是需要的一个服务。我另外写的一个库vue-event-bus提供了在vue应用中进行全局消息管理的能力。单纯地一个网页容器内,使用event-bus是不需要借助app提供服务的,那么当你想在app内,打开多个webview来展示产品场景呢?这时就得考虑要做横跨多个原生webview页面的event-bus处理了,因为从A页打开B页,然后B页里派发消息,需要A页面进行响应的场景,是非常常见的。

如果想实现跨多页的event-bus,可以参考以下代码的做法:

function randomString (t) {
    return '.' + t.replace(/[xy]/g, function (c) {
        const r = Math.random() * 16 | 0
        const v = c === 'x' ? r : (r & 0x3 | 0x8)
        return v.toString(16)
    })
}

let pageId = randomString('xxxxxyyyyy')
let webDispatchApi = 'webEventBusDispatch'
let nativeDispatchApi = 'nativeEventBusDispatch'

// 只有app版本支持以上两个api的时候,才能开放以下的功能
const eventBus = Vue.prototype.$eventBus.core
const trigger = eventBus.trigger.bind(eventBus)
const webDispatch = sdk.registerApi(webDispatchApi).bind(sdk)

sdk.register(nativeDispatchApi, (message, responseCallback) => {
    let event = sdk.toJson(message)
    // 如果消息是从自己所在的webview转发出来的,则下面的trigger不会处理
    if (event.pageId !== pageId) {
        trigger(event.type, ...(sdk.toJson(event.data)))
    }
    responseCallback(`${nativeDispatchApi}:ok`)
})

eventBus.trigger = (event, ...data) => {
    // 让本webview内的eventBus保持正常的使用模式
    trigger(event, ...data)

    // 借助app作为跳板,将本webview内的消息派发到其它webview
    webDispatch({
        type: event,
        pageId: pageId,
        data: JSON.stringify(data)
    }, {})
}

这个做法依赖于app与网页之间,进行双向的api配置。首先app得给网页提供一个webEventBusDispatch的api,方便网页调用,网页利用这个api把网页内的event传给app;app在实现这个api的时候,利用app自己的event-bus(android)或者是notification(iOS)的能力,把这些消息派发给其它的webview页面;每个webview页面可以监听app自己在上一步派发的消息,并把这些数据再传回各自webview内的网页;如何传回呢?网页必须注册一个nativeEventBusDispatch的方法给app,这样app调用这个方法,就可以把数据传回来了。总之,这个方案的思路,就是利用app做跳板,把某个webview下的网页派发的消息,传递到其它webview页面,当然也包括消息源所在的webview。

上面代码中,还有一个pageId的变量,这个变量,可以屏蔽掉网页派发给app,然后app又派发给自己的消息;毕竟在同一个webview内的消息传递,直接借助网页内event-bus本身的能力就够了。

Readme

Keywords

Package Sidebar

Install

npm i hybrid-jssdk-backbone

Weekly Downloads

1

Version

0.1.6

License

MIT

Unpacked Size

46.4 kB

Total Files

9

Last publish

Collaborators

  • liuyunzhuge