Skip to content
可用于web与app之间进行混合开发的jssdk开发骨架,通过约定的方式与app快速打通服务。
JavaScript
Branch: master
Clone or download

Latest commit

Fetching latest commit…
Cannot retrieve the latest commit at this time.

Files

Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
dist 0.1.2 Mar 6, 2020
src 0.1.2 Mar 6, 2020
.babelrc 0.1.0 Jan 7, 2020
.editorconfig 0.1.0 Jan 7, 2020
.gitignore 0.1.0 Jan 7, 2020
.npmignore 0.1.0 Jan 7, 2020
README.md update Jan 10, 2020
package.json 0.1.1 Jan 10, 2020
rollup.config.js 0.1.0 Jan 7, 2020

README.md

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本身的能力就够了。

You can’t perform that action at this time.