Hybrid App 实战之 JSBridge 通信
原生和 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
iframe.src:url 长度有限制。iOS 采用 Ajax 发送同域请求的方式,并将参数放到 head 或 body 里,解决长度隐患,但 WKWebView 不支持这种方式
location.href:连续调用 Native,很容易丢失一些调用;创建请求,耗时会长
拦截 URL SCHEME:Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作
注入 API:通过 WebView 提供的接口,向 JS 的 window 中注入对象或方法,让 JS 调用时,直接执行相应的 Native 代码逻辑
项目实战
项目前端基于 NuxtJS 服务端框架,实现方案就是外层是一个 Android 原生壳子,内嵌 H5 页面,当然 H5 页面也可单独运行。其中 JSBridge
为挂载在 window 上的桥接对象
注入 API:在window上挂载对象和方法
window.JSBridge:可判断是否为 app 环境
挂载了 getAppInfo()函数,用于传递 app 信息,例如版本号、渠道等等
本地存储权限判断(checkPermission, requestPermission),具体可看 getPermission()
也可挂载一些其他的方法变量和方法
拦截 URL SCHEME
initTitle:初始化头部的命令,具体可看 setHeaderConfig()
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; };
作者:时光足迹
链接:https://juejin.cn/post/7021451193285083150