本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金
原生和 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-
- 注入 API:通过 WebView 提供的接口,向 JS 的 window 中注入对象或方法,让 JS 调用时,直接执行相应的 Native 代码逻辑
-
- 拦截 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上挂载对象和方法
- 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;
};
参考文档: