Hybrid App 实战之 JSBridge 通信

2,361 阅读3分钟

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

原生和 H5 混合开发,通过 JSBridge 通信,常用方式为注入 API 和 拦截 URL SCHEME实现。实现拦截 URL SCHEME,获取本地存储权限,客户端头部交互,打开谷歌应用商店等功能

混合(Hybrid App)开发:原生和 H5 混合开发,通过 webview 内嵌 H5 实现

  • 优点:
    • 开发效率高,节约时间,一套代码可适用 Android,IOS,也可在微信或浏览器中访问 H5 链接
    • 代码维护方便,版本更新快,节省成本
    • 更新和部署比较方便,升级版本不需要应用商店审核
  • 缺点:
    • 功能无法自定义,需要客户端支持
    • 加载缓慢,网络要求高
    • 安全性比较低

JSBridge 通信

JSBridge 就是一个 Native 与 JS 的双向通信通道:

  • Native 只通过一个固定的桥对象调用 JS
    • 直接执行拼接好的 JS 代码,JS 的方法必须在全局的 window 上
  • JS 也只通过固定的桥对象调用 Native,推荐注入 API
      1. 注入 API:通过 WebView 提供的接口,向 JS 的 window 中注入对象或方法,让 JS 调用时,直接执行相应的 Native 代码逻辑
      1. 拦截 URL SCHEME:Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作
      • iframe.src:url 长度有限制。iOS 采用 Ajax 发送同域请求的方式,并将参数放到 head 或 body 里,解决长度隐患,但 WKWebView 不支持这种方式
      • location.href:连续调用 Native,很容易丢失一些调用;创建请求,耗时会长

项目实战

项目前端基于 NuxtJS 服务端框架,实现方案就是外层是一个 Android 原生壳子,内嵌 H5 页面,当然 H5 页面也可单独运行。其中 JSBridge 为挂载在 window 上的桥接对象

  • 注入 API:在window上挂载对象和方法
    1. window.JSBridge:可判断是否为 app 环境
    2. 挂载了 getAppInfo()函数,用于传递 app 信息,例如版本号、渠道等等
    3. 本地存储权限判断(checkPermission, requestPermission),具体可看 getPermission()
    4. 也可挂载一些其他的方法变量和方法
  • 拦截 URL SCHEME
    1. initTitle:初始化头部的命令,具体可看 setHeaderConfig()
    2. openUrl:跳转链接处理,具体可看 jumpNative()

基础使用

// 与原生交互方法:拦截 URL SCHEME
const executeUrlCommand = (url) => {
  const frame = document.createElement("iframe");
  frame.width = "1px";
  frame.height = "1px";
  frame.style.display = "none";
  frame.src = url;
  document.body.appendChild(frame);

  setTimeout(() => {
    document.body.removeChild(frame);
  }, 100);
};

// 客户端交互 获取本地存储权限
const getPermission = () => {
  // 是否有存储权限,true-有,false-没有
  const permission = window.JSBridge.checkPermission(
    "android.permission.WRITE_EXTERNAL_STORAGE"
  );

  if (permission) {
    // 没有的话,需要请求开启权限
    window.JSBridge.requestPermission(
      "android.permission.WRITE_EXTERNAL_STORAGE"
    );
    return false;
  }
};

// layouts/default.vue
export default {
  mounted() {
    this.getAppInfo();
    this.setHeadConfig();
    this.nativeInteract();
  },
  methods: {
    isApp() {
      // 判断是否是在app环境
      let isApp = false;
      if (/JSBridge/i.test(navigator.userAgent) || window.JSBridge) {
        isApp = true;
      }
      return isApp;
    },
    getAppInfo() {
      // 获取app信息
      if (this.isApp()) {
        const appInfo = JSON.parse(window.JSBridge.getAppInfo());
      }
    },
    // 设置头部信息,与客户端交互
    setHeaderConfig() {
      // 头部信息主要包含title,左侧按钮(文案,事件),右侧按钮(文案,事件)
      const config = {
        showBack: true,
        title: "首页",
        backEvent: "window.history.back()",
        leftText: "",
        leftClickEvent: "() => {}",
        rightText: "",
        rightClickEvent: "() => {}",
      };
      if (this.isApp()) {
        executeUrlCommand(
          `JSBridge://initTitle?backShow=${config.showBack}&backEvent=${config.backEvent}&leftText=${config.leftText}&leftClickEvent=${config.leftClickEvent}&rightText=${config.rightText}&rightClickEvent=${config.rightClickEvent}`
        );
      }
    },
  },
};

跳转处理

// 客户端交互 跳转链接处理
const jumpNative = (jumpType, jumpUrl) => {
  let command = "";
  if (jumpType === "apkUrl") {
    // apk链接
    command =
      "JSBridge://openUrl?url=" +
      encodeURIComponent(jumpUrl) +
      "&exitCurrent=false";
  } else if (jumpType === "gpUrl") {
    // 谷歌应用商店链接
    jumpToGooglePlay(jumpUrl);
  } else {
    executeUrlCommand(command);
  }
};

// 打开谷歌应用商店
const jumpToGooglePlay = (url) => {
  const t = 1000;
  let isApp = true;

  setTimeout(() => {
    if (!isApp) {
      window.location.href = url;
    }
  }, 2000);

  const addIframe = (id) => {
    const t1 = Date.now();
    const iframe = document.createElement("iframe");
    iframe.id = "goPlay";
    iframe.src = "market://details?" + id;
    iframe.style.display = "none";
    document.body.appendChild(iframe);
    setTimeout(() => {
      tryToApp(t1);
    }, t);
  };

  const tryToApp = (t1) => {
    const t2 = Date.now();
    if (!t1 || t2 - t1 < t + 200) {
      isApp = false;
    }
  };

  if (url.indexOf("//play.google.com/store/apps/details?") > 0) {
    addIframe(url.split("//play.google.com/store/apps/details?")[1]);
  } else {
    window.location.href = url;
  }
};

// h5下载apk
const installApk = (src) => {
  const form = document.createElement("form");
  form.action = src;
  document.getElementsByTagName("body")[0].appendChild(form);
  form.submit();
  return false;
};

参考文档: