H5与原生app的交互

1,588 阅读4分钟

为了应对一些经常变更或者时效性短的页面开发,工作中往往会使用原生app里面嵌套前端h5页面的快速开发方式,这就不可避免的需要从原生app拿到相关数据,或者调用原生的页面或者方法; 中间需要用到JavascriptBridge

先讲ios端的webview;有两个可以选择 ①UIWebView ios端先通过JSContext获取js上下文,然后通过这个上下文,进行 OC & JS 的双端交互。

 _jsContext = 
 [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

_jsContext.exceptionHandler = ^(JSContext *context, JSValue *exception) {
    NSLog(@"%@",@"获取 WebView JS 执行环境失败了!");
};

② WKWebView。 通过 userContentController 把需要观察的 JS 执行函数注册起来然后通过一个协议方法,将所有注册过的 JS 函数执行的参数传递到此协议方法中。 注册需要执行的函数

 [webView.configuration.userContentController addScriptMessageHandler:self name:@"jsFunc"];

在js中就可以调用了

window.webkit.messageHandlers.jsFunc.postMessage({name : "李四",age : 22});

然后引出这边的主角 WebViewJavaScriptBridge WebViewJavaScriptBridge 用于 WKWebView & UIWebView 中 OC 和 JS 交互。

它的基本原理是:

  • 把 OC 的方法注册到桥梁中,让 JS 去调用。
  • 把 JS 的方法注册在桥梁中,让 OC 去调用。

第一步先使用注册

oc端 略 js端 如下

function setupWebViewJavascriptBridge(callback) {
    // WebViewJavaScriptBridge用于 WKWebView & UIWebView 中 OC 和 JS 交互。
    if (window.WebViewJavascriptBridge) { 
        return callback(WebViewJavascriptBridge); 
    }
    if (window.WVJBCallbacks) { 
        return window.WVJBCallbacks.push(callback); 
     }
    // 创建一个 WVJBCallbacks 全局属性数组,并将 callback 插入到数组中。
    window.WVJBCallbacks = [callback];
    // 创建一个iframe 元素
    const WVJBIframe = document.createElement('iframe');
    // 该元素不显示
    WVJBIframe.style.display = 'none';
    // 设置 iframe 的 src 属性
    WVJBIframe.src = 'https://__bridge_loaded__';
    // 把 iframe 添加到当前文导航上
    document.documentElement.appendChild(WVJBIframe);
    // 主线程执行完后再移除
    setTimeout(() => { document.documentElement.removeChild(WVJBIframe) }, 0)
}

第二步 oc端和js端分别都想WebViewJavaScriptBridge注入方法

oc端

 [_jsBridge registerHandler:@"scanClick" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"dataFrom JS : %@",data[@"data"]);
        
        responseCallback(@"扫描结果 : www.baidu.com");
    }];
  • scanClick 是 OC block 的一个别名。
  • block 本身,是 JS 通过某种方式调用到 scanClick 的时候,执行的代码块。
  • data ,由于 OC 这端由 JS 调用,所以 data 是 JS 端传递过来的数据。
  • responseCallback OC 端的 block 执行完毕之后,往 JS 端传递的数据。

js端

// 这里主要是注册 OC 将要调用的 JS 方法。
setupWebViewJavascriptBridge(function(bridge){
    // 声明 OC 需要调用的 JS 方法。
    bridge.registerHanlder('testJavaScriptFunction',function(data,responseCallback){
        // data 是 OC 传递过来的数据.
        // responseCallback 是 JS 调用完毕之后传递给 OC 的数据
        alert("JS 被 OC 调用了.");
        responseCallback({data: "js 的数据",from : "JS"});
    })
});

示例,客户端提供下面几个api

const api = {
  // 拿取客户端用户信息
  getUserInfo: {
    version: '1.1.8',
    successCallback: 'callback',
    failCallback: null
  },
  // 关闭网页
  closeWebView: {
    version: '4.2.8',
    successCallback: null,
    failCallback: null
  },
  // 跳转原生页面
  jump: {
    version: '4.2.8',
    successCallback: null,
    failCallback: null
  }
}

我们比方获取客户端用户信息接口,oc端注册好后将注册的方法名告诉给我们了, 这时,我们就需要先function 一个它setupWebViewJavascriptBridge, 拿到WebViewJavaScriptBridge对象;

然后用WebViewJavaScriptBridge.callHandler('getUserInfo',{uid: 'xx', sid: 'xxxxx'}),调用oc端的注册的getUserInfo方法,并且传递参数;

这时我们需要承接oc端调用后的返回值,那只能通过oc端成功后,调用js的方法,把返回值放到函数参数中返回来;所以需要js端也向WebViewJavaScriptBridge中注册一个方法,以供oc端调用

WebViewJavaScriptBridge.registerHandler('jsFunForOcCallback', data => { console.log('oc端方式被调用成功后返回给js的值', data) })

如果还需要传递失败的函数,那就要再继续注册一个失败的js函数供客户端调用

** 以上是js端调用oc端信息 **

还有oc端直接调用js端的,这时js端就只需要注册一个函数,供oc端调用即可

案例参考

另外复制上我们项目对ios 安卓的一套注册

/**
 * 贪吃蛇大作战JSBridge
 * wiki地址: http://wiki.17qq.me/pages/viewpage.action?pageId=590045
 */

import { Toast } from '../comps'
import { compareVersion } from '../utils/util'
import events from '../events'

const isIOSNewWebview = !navigator.userAgent.match(/Android/i)

function checkTCSApp () {
  return new Promise((resolve, reject) => {
    if (window.Tcsdzz) {
      return resolve()
    }
    if (window.notInTcsdzz) {
      return reject(new Error('请在******内打开'))
    }
    let count = 0
    const timer = setInterval(() => {
      if (window.Tcsdzz) {
        clearInterval(timer)
        resolve()
      } else {
        count++
        if (count > 20) {
          clearInterval(timer)
          window.notInTcsdzz = true
          reject(new Error('请在贪吃蛇大作战内打开'))
        }
      }
    }, 100)
  })
}

function initWKBridge () {
  return new Promise(resolve => {
    if (window.WebViewJavascriptBridge) {
      return resolve(window.WebViewJavascriptBridge)
    }
    if (window.WVJBCallbacks) {
      return window.WVJBCallbacks.push(resolve)
    }
    window.WVJBCallbacks = [resolve]
    const WVJBIframe = document.createElement('iframe')
    WVJBIframe.style.display = 'none'
    WVJBIframe.src = 'https://__bridge_loaded__'
    document.documentElement.appendChild(WVJBIframe)
    setTimeout(() => { document.documentElement.removeChild(WVJBIframe) }, 0)
  })
}

const api = {
  certifySuccess: {
    version: '4.2.8',
    successCallback: null,
    failCallback: null
  },
  closeWebView: {
    version: '4.2.8',
    successCallback: null,
    failCallback: null
  },
  jump: {
    version: '4.2.8',
    successCallback: null,
    failCallback: null
  },
  track: {
    version: '4.2.8',
    successCallback: null,
    failCallback: null
  },
  notifyRedDot: {
    version: '4.2.8',
    successCallback: null,
    failCallback: null
  },
  alertAward: {
    version: '4.2.8',
    successCallback: null,
    failCallback: null
  },
  share: {
    version: '4.2.8',
    successCallback: 'successCallback',
    failCallback: 'cancelCallback'
  },
  checkAd: {
    version: '4.2.8',
    successCallback: 'adCheckCallback',
    failCallback: null
  },
  showAd: {
    version: '4.2.8',
    successCallback: 'adShowCallback',
    failCallback: null
  },
  canWatchAd: {
    version: '4.2.8',
    successCallback: 'hasAdCheckCallback',
    failCallback: null
  },
  getDisplayCutout: {
    version: '4.2.8',
    successCallback: 'jsGetDisplayCutoutCallback',
    failCallback: null
  },
  httpPost: {
    version: '4.2.8',
    successCallback: 'httpCallback',
    failCallback: null
  },
  getUserInfo: {
    version: '4.2.8',
    successCallback: 'callback',
    failCallback: null
  },
  buyProduct: {
    version: '4.2.8',
    successCallback: 'buyCallback',
    failCallback: null
  },
  switchAccount: {
    version: '4.3.18',
    successCallback: null,
    failCallback: null
  },
  unzipLottiePack: {
    version: '4.2.11',
    successCallback: 'successCallback',
    failCallback: null
  },
  getCardInfo: {
    version: '4.2.8',
    successCallback: null,
    failCallback: null
  },
  getTimeDiff: {
    version: '4.2.8',
    successCallback: null,
    failCallback: null
  },
  editInvitationCard: {
    version: '4.2.8',
    successCallback: null,
    failCallback: null
  },
  inviteFriend: {
    version: '4.2.8',
    successCallback: null,
    failCallback: null
  },
  playMusic: {
    version: '4.2.8',
    successCallback: null,
    failCallback: null
  },
  enterRoom: {
    version: '4.2.8',
    successCallback: null,
    failCallback: null
  },
  viewUserDetail: {
    version: '4.2.8',
    successCallback: null,
    failCallback: null
  }
}

window.refreshWebFromNative = function () {
  events.$emit('tcs:page_refuse')
}
window.updateWord = function (content) {
  events.$emit('onEditContent', content)
}

if (isIOSNewWebview) {
  initWKBridge().then(bridge => {
    bridge.registerHandler('refreshWebFromNative', () => {
      events.$emit('tcs:page_refuse')
    })
    bridge.registerHandler('updateWord', ([content]) => {
      events.$emit('onEditContent', content)
    })
  })
}

export default {
  methods: {
    $tcs (method, params) {
      const config = api[method] || null
      if (!config) {
        throw new Error(`Tcsdzz.${method} is undefined`, '方法不存在')
      }
      if (config.version !== '4.2.8' && compareVersion(config.version) === 1) {
        Toast('请升级到最新版本', 5000)
        return Promise.reject(new Error('请升级到最新版本'))
      }
      if (isIOSNewWebview) {
        if (method === 'getCardInfo' || method === 'getTimeDiff') {
          return new Promise((resolve) => {
            initWKBridge().then(bridge => {
              console.log(`callHandler ${method}`, params)
              bridge.callHandler(method, params, resolve)
            }).catch(err => {
              Toast(err.message)
            })
          })
        }
        return new Promise((resolve, reject) => {
          initWKBridge().then(bridge => {
            if (config.successCallback) {
              const successCallbackName = `${method}_success_${Date.now()}_${Math.floor(Math.random() * 100)}`
              bridge.registerHandler(successCallbackName, data => {
                console.log(`recv => ${method}`, data)
                resolve(data[0])
              })
              params[config.successCallback] = successCallbackName
            }

            if (config.failCallback) {
              const failCallbackName = `${method}_fail_${Date.now()}_${Math.floor(Math.random() * 100)}`
              bridge.registerHandler(failCallbackName, reject)
              params[config.successCallback] = failCallbackName
            }
            console.log(`callHandler ${method}`, params)
            if (method === 'playMusic') {
              bridge.callHandler(method, { isPlay: params })
              return
            }
            if (method === 'viewUserDetail') {
              bridge.callHandler(method, { uid: params })
              return
            }
            bridge.callHandler(method, params)
          }).catch(err => {
            Toast(err.message)
          })
        })
      } else if (config.successCallback || config.failCallback) {
        return new Promise((resolve, reject) => {
          checkTCSApp().then(() => {
            if (config.successCallback) {
              const successCallbackName = `${method}_success_${Date.now()}_${Math.floor(Math.random() * 100)}`
              console.log(successCallbackName)
              window[successCallbackName] = params => {
                console.log(`recv => ${method}`)
                resolve(params)
              }
              params[config.successCallback] = successCallbackName
            }

            if (config.failCallback) {
              const failCallbackName = `${method}_fail_${Date.now()}_${Math.floor(Math.random() * 100)}`
              window[failCallbackName] = reject
              params[config.successCallback] = failCallbackName
            }
            console.log(`Tcsdzz.${method}`, params)
            window.Tcsdzz[method](JSON.stringify(params))
          }).catch(reject)
        })
      } else {
        return new Promise((resolve, reject) => {
          checkTCSApp().then(() => {
            if (method === 'viewUserDetail' || method === 'playMusic') {
              resolve(window.Tcsdzz[method](params))
              return
            }
            resolve(params ? window.Tcsdzz[method](JSON.stringify(params)) : window.Tcsdzz[method]())
          }).catch(reject)
        })
      }
    }
  }
}