zhaobinglong / myBlog

https://zhaobinglong.github.io/myBlog/
MIT License
7 stars 0 forks source link

APP和H5通信原理 #118

Open zhaobinglong opened 3 years ago

zhaobinglong commented 3 years ago

判断终端

由于各个平台的通信方式不一致,所以第一步需要判断当前的终端,再决定用什么通信方式


//判断访问终端
function browserVersion(){
    var u = navigator.userAgent;
    return {
        trident: u.indexOf('Trident') > -1, //IE内核
        presto: u.indexOf('Presto') > -1, //opera内核
        webKit: u.indexOf('AppleWebKit') > -1, //苹果、谷歌内核
        gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1,//火狐内核
        mobile: !!u.match(/AppleWebKit.*Mobile.*/), //是否为移动终端
        ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), //ios终端
        android: u.indexOf('Android') > -1 || u.indexOf('Adr') > -1, //android终端
        iPhone: u.indexOf('iPhone') > -1 , //是否为iPhone或者QQHD浏览器
        iPad: u.indexOf('iPad') > -1, //是否iPad
        webApp: u.indexOf('Safari') == -1, //是否web应该程序,没有头部与底部
        weixin: u.indexOf('MicroMessenger') > -1, //是否微信 (2015-01-22新增)
        qq: u.match(/\sQQ/i) == " qq" //是否QQ
    };

容器

参考

https://blog.csdn.net/frontend_frank/article/details/109281763

zhaobinglong commented 3 years ago

H5向IOS通信

在ios中,并没有现成的api让js去调用native的方法,但是UIWebView与WKWebView能够拦截h5内发起的所有网络请求。所以我们的思路就是通过在h5内发起约定好的特定协议的网络请求,如'jsbridge://bridge2.native?params=' + encodeURIComponent(obj)然后带上你要传递给ios的参数;然后在客户端内拦截到指定协议头的请求之后就阻止该请求并解析url上的参数,执行相应逻辑

// 这是一个H5调用IOS分享的例子
  function createIframe(url){
    var url = 'jsbridge://getShare?title=分享标题&desc=分享描述&link=http%3A%2F%2Fwww.douyu.com&cbName=jsCallClientBack';
    var iframe = document.createElement('iframe');
    iframe.style.width = '1px';
    iframe.style.height = '1px';
    iframe.style.display = 'none';
    iframe.src = https://segmentfault.com/a/url;
    document.body.appendChild(iframe);
    setTimeout(function() {
        iframe.remove();
    }, 100);
  }

IOS向H5通信

Native调用Javascript语言,是通过UIWebView组件的stringByEvaluatingJavaScriptFromString方法来实现的,该方法返回js脚本的执行结果。

// IOS swift code
 webview.stringByEvaluatingJavaScriptFromString("window.methodName()")

从上面代码可以看出它其实就是执行了一个字符串化的js代码,调用了window下的一个对象,如果我们要让native来调用我们js写的方法,那这个方法就要在window下能访问到。但从全局考虑,我们只要暴露一个对象如JSBridge给native调用就好了。

调用客户端原生方法的回调函数也将绑在window下供客户端成功反调用,实际上一次调用客户端方法最后产生的结果是双向互相调用。

zhaobinglong commented 3 years ago

H5向Android客户端方法

通过schema方式,客户端使用shouldOverrideUrlLoading方法对url请求协议进行解析。这种js的调用方式与ios的一样,使用iframe来调用native方法。通过在webview页面里直接注入原生js代码方式,使用addJavascriptInterface方法来实现。

// android JAVA code
  class JSInterface {
    @JavascriptInterface
    public String getShare() {
        //...
        return "share";
    }
}
webView.addJavascriptInterface(new JSInterface(), "AndroidNativeApi");

Android客户端调用H5方法

在安卓APP中,客户端通过webview的loadUrl进行调用:

// android JAVA code
 webView.loadUrl("javascript:window.jsBridge.getShare()");

H5端将方法绑定在window下的对象即可,无需与IOS作区分 上面的代码就是在页面的window对象里注入了AndroidNativeApi对象。在js里可以直接调用原生方法。

zhaobinglong commented 3 years ago

某H5项目封装好的通信代码

/* eslint-disable */
/**
 * 引入该mixin后使用方式:
 * 1.在appFunConst.js中定义好方法名,
 * 2.在需要引入的vue文件data中定义registFunName,格式:registFunName: [GET_UPLPAD, RETURN_UPLPAD],,
 * 3.在vue文件中调用:this.bridgeApp(GET_UPLPAD,RETURN_UPLPAD, params);
 * bridgeApp函数的详细参数,搜索该文件,见定义处
 */
import { judgeAuthUtils, getUserInfo } from '@/libs/utils'
import { getButtonsPermissions } from '@/api/user/index'
export default {
  data () {
    return {
      browserInfo: null,
      userBtnPermissions: null
    }
  },
  async mounted () {
   await this.getUserPermission()
  },
  created () {

    this.getBrowser()
    // 注册方法
    if (!this.browserInfo.isWeb && this.registFunName) {
      console.log('bridge-app 注册的方法:', this.registFunName)
      this.setWindowFun(this.registFunName)
    }
  },
  methods: {
    setWindowFun (funName) {
      funName.forEach(item => {
        window[item] = (params) => {
          console.log('bridge-app.js ~ setWindowFun 调用window方法~ 方法名和params', item, params)
          if (params && item.includes('return')) params = this.appDataToJson(params)
          console.log('--', item);
          console.log('--', params);
          console.log('--', this);
          console.log(this[item])
          this[item](params)
        }
      })
    },
    // 注册事件监听,初始化
    setupWebViewJavascriptBridge (callback) {
      if (this.browserInfo.android) {
        if (window.WebViewJavascriptBridge) {
          callback(WebViewJavascriptBridge)
        } else {
          // 添加dom事件
          document.addEventListener(
            'WebViewJavascriptBridgeReady',
            function () {
              callback(WebViewJavascriptBridge)
            },
            false
          )
        }
      }
      if (this.browserInfo.iPhone) {
        if (window.WebViewJavascriptBridge) {
          return callback(WebViewJavascriptBridge)
        }
        if (window.WVJBCallbacks) {
          return window.WVJBCallbacks.push(callback)
        }
        window.WVJBCallbacks = [callback]
        var WVJBIframe = document.createElement('iframe')
        WVJBIframe.style.display = 'none'
        WVJBIframe.src = 'https://__bridge_loaded__'
        document.documentElement.appendChild(WVJBIframe)
        setTimeout(function () {
          document.documentElement.removeChild(WVJBIframe)
        }, 0)
      }
    },
    /**
     * 注册app和H5通信关系
     * @param {*} jsCallAppFuncName H5调用APP同名方法
     * @param {*} appCallJsFuncName App调用H5方法
     * @param {*} params H5向app发送数据
     */
    bridgeApp (jsCallAppFuncName, appCallJsFuncName, params) {
      // console.log(navigator.userAgent);
      if ((this.browserInfo.isWeb && jsCallAppFuncName === 'openPath') || (this.browserInfo.isWechatMinWeb && jsCallAppFuncName === 'openPath')) {
        this.$router.push({
          path: params.url,
          query: this.getUrlVars(params.url)
        })
        return
      }
      // console.log('注册app和H5通信关系', appCallJsFuncName, jsCallAppFuncName, params)
      // 回调函数,接收java发送来的数据
      this.setupWebViewJavascriptBridge(function (bridge) {
        if (jsCallAppFuncName) {
          // console.log("file: bridge-app.js ~ H5调用APP同名方法(已注册) ~ 方法名:", jsCallAppFuncName, '参数', params)
          /* jsCallAppFuncName是js调用app的方法名,params则是h5传给appapp的参数,function是调用成功后js收到app的回调。 */
          /* 前端可以传params为json对象,但是在发给APP的时候,要序列化为字符串 */
          if (params) {
            params = JSON.stringify(params)
          }
          bridge.callHandler(jsCallAppFuncName, params, function (responseData, responseCallback) {
            // TODO 这里有个优化,其实是可以直接接收到app返回的数据,待研究联调测试
            console.log('!!!js调用app方法' + jsCallAppFuncName + '之后,app成功的回调信息:' + responseData + ',responseCallback=' + responseCallback)
          })
        }
        if (appCallJsFuncName) {
          console.log('file: bridge-app.js ~ app调用H5方法(已注册) ~ 方法名:', appCallJsFuncName)
          /* appCallJsFuncName是js注册的方法,供app调用,callbackJsFunc是调用后js执行的回调 */
          bridge.registerHandler(appCallJsFuncName, function (data, responseCallback) {
            console.log('app调用H5方法' + appCallJsFuncName + '之后, app返回的信息:' + data)
            responseCallback(data)
          })
        }
      })
    },
    getBrowser () {
      var userAgent = navigator.userAgent
      var version = navigator.appVersion
      var language = (navigator.browserLanguage || navigator.language).toLowerCase()
      // 浏览器信息
      this.browserInfo = {
        trident: userAgent.indexOf('Trident') > -1,
        presto: userAgent.indexOf('Presto') > -1,
        webKit: userAgent.indexOf('AppleWebKit') > -1,
        gecko: userAgent.indexOf('Gecko') > -1 && userAgent.indexOf('KHTML') === -1,
        mobile: !!userAgent.match(/AppleWebKit.*Mobile.*/),
        ios: !!userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/),
        android: userAgent.indexOf('Android') > -1 || userAgent.indexOf('Linux') > -1,
        iPhone: userAgent.indexOf('iPhone') > -1,
        iPad: userAgent.indexOf('iPad') > -1,
        webApp: userAgent.indexOf('Safari') === -1,
        language: language,
        version: version,
        isWeb: localStorage.getItem('outApp') === '1', // 判断是否在浏览器中打开
        isWechatMinWeb: userAgent.indexOf('MicroMessenger') > -1 // 判断是否在微信小程序
      }
      return this.browserInfo
    },
    // 处理ios 和 Android 返回的数据
    appDataToJson (data) {
      if (this.browserInfo.ios) {
        // ios不需要转,直接返回
        return data
      } else {
        // console.log("🚀 ~ file: bridge-app.js ~ line 137 ~ appDataToJson ~ data", data, typeof data)
        const res = {}
        if (!data) {
          return res
        }
        if (data instanceof Object) {
          return data
        }
        if (data.indexOf('{') === -1) {
          return data
        }
        // console.log('is android', JSON.parse(data))
        return JSON.parse(data)
      }
    },
    // 获取url后的query参数
    getUrlVars (url) {
      var hash
      var myJson = {}
      var hashes = url.slice(url.indexOf('?') + 1).split('&')
      for (var i = 0; i < hashes.length; i++) {
        hash = hashes[i].split('=')
        myJson[hash[0]] = hash[1]
      }
      return myJson
    },
    // 获取用户按钮权限
    judgeAuth (btncode) {
      let flag = false
      this.userBtnPermissions = JSON.parse(this.userBtnPermissions)
      flag = judgeAuthUtils(btncode, this.$route.path, this.userBtnPermissions)
      return flag
    },
    getUserPermission () {
      const userInfo = getUserInfo()
      const key = userInfo ? userInfo.expireTime : null
      const sessionData = localStorage.getItem(key)
      if (sessionData) {
        this.userBtnPermissions = sessionData
      } else {
        getButtonsPermissions().then(res => {
          localStorage.setItem(key, JSON.stringify(res))
          this.userBtnPermissions = localStorage.getItem(key)
        })
      }
    }
  },
  beforeDestory () {
  }
}