👉目录
1 前言
2 通信方案
3 离线包方案
4 容器基础能力
5 开发调试
6 稳定性与安全
7 番外篇
8 总结
// 正常网页跳转地址
const url = 'https://qq.com/xxx?param=xxx'
// 约定跳转 url
const fakeUrl = 'scheme://getUserInfo/action?param=xx&callbackid=xx'
// 1. A 标签发起一次
<a href="scheme://getUserInfo/action?param=xx&callbackid=xx">用户信息</a>
// 2. 在JS中创建一个iframe,然后动态插入到 DOM 中
$('body').append('<iframe src="scheme://getUserInfo/action?param=xx&callbackid=xx"></iframe>');
// 3. location.href 跳转
location.href = 'scheme://getUserInfo/action?param=xx&callbackid=xx'
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 1 根据url,判断是否是所需要的拦截的调用 判断协议/域名
if (是){
// 2 取出路径,确认要发起的native调用的指令是什么
// 3 取出参数,拿到JS传过来的数据
// 4 根据指令调用对应的native方法,传递数据
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
//1 根据url,判断是否是所需要的拦截的调用 判断协议/域名
if (是){
// 2 取出路径,确认要发起的native调用的指令是什么
// 3 取出参数,拿到JS传过来的数据
// 4 根据指令调用对应的native方法,传递数据
// 确认拦截,拒绝WebView继续发起请求
decisionHandler(WKNavigationActionPolicyCancel);
} else {
decisionHandler(WKNavigationActionPolicyAllow);
}
return YES;
}
location.href = 'scheme://getUserInfo/action?param=111&callbackid=xx'
location.href = 'scheme://getUserInfo/action?param=222&callbackid=xx'
const data = {
module: 'base',
action:'getUserInfo',
params:'xxxx',
callbackId:'xxxx',
};
const jsonData = JSON.stringify([data]);
// 发起调用,可以同步获取调用结果
const ret = prompt(jsonData);
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
//1 根据传来的字符串反解出数据,判断是否是所需要的拦截而非常规H5弹框
if (是){
// 2 取出指令参数,确认要发起的native调用的指令是什么
// 3 取出数据参数,拿到JS传过来的数据
// 4 根据指令调用对应的native方法,传递数据
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler{
// 1 根据传来的字符串反解出数据,判断是否是所需要的拦截而非常规H5弹框
if (是){
// 2 取出指令参数,确认要发起的native调用的指令是什么
// 3 取出数据参数,拿到JS传过来的数据
// 4 根据指令调用对应的native方法,传递数据
// 直接返回JS空字符串
completionHandler(@"");
}else{
//直接返回JS空字符串
completionHandler(@"");
}
}
//准备要传给native的数据,包括指令,数据,回调等
const data = {
module: 'base',
action:'getUserInfo',
params:'xxxx',
callbackId:'xxxx',
};
//直接使用这个客户端注入的函数
nativeObject.getUserInfo(data);
// 通过addJavascriptInterface()将Java对象映射到JS对象
//参数1:Javascript对象名
//参数2:Java对象名
mWebView.addJavascriptInterface(new AndroidtoJs(), "nativeObject");
nativeObject.getUserInfo("js调用了android中的getUserInfo方法");
//准备要传给native的数据,包括指令,数据,回调等
const data = {
module: 'base',
action:'getUserInfo',
params:'xxxx',
callbackId:'xxxx',
};
//传递给客户端,不支持同步获取结果
window.webkit.messageHandlers.nativeObject.postMessage(data)
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
//1 解读JS传过来的JSValue data数据
NSDictionary *msgBody = message.body;
//2 取出指令参数,确认要发起的native调用的指令是什么
//3 取出数据参数,拿到JS传过来的数据
//4 根据指令调用对应的native方法,传递数据
}
function calljs(data){
console.log(JSON.parse(data))
//1 识别客户端传来的数据
//2 对数据进行分析,从而调用或执行其他逻辑
}
//不展开了,data是一个字典,把字典序列化
NSString *paramsString = [self _serializeMessageData:data];
NSString* javascriptCommand = [NSString stringWithFormat:@"calljs('%@');", paramsString];
//要求必须在主线程执行JS
if ([[NSThread currentThread] isMainThread]) {
[self.webView evaluateJavaScript:javascriptCommand completionHandler:nil];
} else {
__strong typeof(self)strongSelf = self;
dispatch_sync(dispatch_get_main_queue(), ^{
[strongSelf.webView evaluateJavaScript:javascriptCommand completionHandler:nil];
});
}
calljs('{data:xxx,data2:xxx}');
mWebView.loadUrl("javascript:calljs(\'{data:xxx,data2:xxx}\')");
const invokeMap = new Map();
let invokeId = 0;
class BridgeNameSpace {
/**
* 调用Native功能
* @param eventName - 事件名称
* @param params - 通讯数据
* @param callback - 回调函数
*/
invoke = (eventName, params, callback) => {
invokeId += 1;
invokeMap.set(invokeId, callback);
if (isAndroid) {
window.BridgeNameSpace.invokeHandler(eventName, params, invokeId);
} else {
window.webkit.messageHandlers.invokeHandler.postMessage({
event: eventName,
params,
callbackId: invokeId,
});
}
};
/**
* 调用Native功能
* @param eventName - 事件名称
* @param params - 通讯数据
* @param callback - 回调函数
*/
invokeSync(eventName, params, callback) {
invokeId += 1;
invokeMap.set(invokeId, callback);
if (isAndroid) {
window.BridgeNameSpace.invokeHandler(eventName, params, invokeId);
} else { // 将消息体直接JSON字符串化,调用 Prompt(),并且可以直接拿到返回值
const result = prompt(JSON.stringify(params));
return result;
}
}
/**
* Native将invoke结果返回给js的回调句柄
* @param id - callbackId
* @param params - 通讯数据
*/
invokeCallbackHandler = (id, params) => {
const fn = invokeMap.get(id);
if (typeof fn === 'function') {
fn(params);
}
invokeMap.delete(id);
};
getSystemInfo(callback) {
const promsie = new Promise((resolve, reject) => {
this.invoke('getSystemInfo', {}, (res) => {
if (res.status === 'success') {
resolve(res);
} else {
reject(res);
}
});
});
if (callback) {
return promsie.then(callback).catch(callback);
}
return promsie;
}
}
window.BridgeNameSpace = new BridgeNameSpace();
BridgeNameSpace.getSystemInfo()
.then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
});
BridgeNameSpace.getSystemInfo((res) => {
console.log(res);
});
const publishMap = {};
class BridgeNameSpace {
/**
* 订阅 Native 事件
* @param eventName - 事件名
* @param callback - 回调函数
*/
subscribe = (eventName, callback) => {
if (!publishMap[eventName]) {
publishMap[eventName] = [];
}
const oldEvents = publishMap[eventName];
publishMap[eventName] = oldEvents.concat(callback);
};
/**
* Native将publish结果返回给js的回调句柄
* @param eventName - 事件名
* @param params - 调用参数
*/
subscribeCallbackHandler = (eventName, params) => {
const cbs = publishMap[eventName] || [];
if (cbs.length) {
cbs.forEach((cb) => cb(params));
}
};
/**
* ⻚⾯可⻅通知
*/
onPageVisible(callback) {
this.subscribe(
'onPageVisible',
callback,
);
}
}
BridgeNameSpace.onPageInvisible(() => {});
const notifyMap = new Map();
class BridgeNameSpace {
/**
* 混合式框架向Native发送通知 notify
* @param eventName - 事件名,命名空间为当前包
* @param params - 参数对象,由通知业务自己定义
* @param callback - 回调函数,回调是否通知成功
*/
notify = (eventName, params, callback) => {
this.invoke('notify', { event: eventName, params }, callback);
};
/**
* webview 事件处理函数,可与notify配合使用
* 事件订阅方法,可对本应用及跨应用事件进行订阅
* @param {String} eventName
* @param {Function} callback
*/
subscribeNotify = (eventName, callback) => {
this.invoke('subscribeNotification', { event: eventName }, (res) => {
if (res.status === 'success') {
notifyMap.set(eventName, callback);
} else {
callback(res);
}
});
};
/**
* Native将notify结果返回给js的回调句柄
* @param eventName - 事件名
* @param params - 调用参数
*/
notifyCallbackHandler = (eventName, params) => {
const fn = notifyMap.get(eventName);
if ('function' === typeof fn) {
fn(params);
} else {
notifyMap.delete(eventName);
}
};
}
// Webview A 订阅
BridgeNameSpace.subscribeNotify(
'QSOverlayPlayerBackClick',
(res) => {
console.log(res);
}
);
// Webview B 通知
BridgeNameSpace.notify(
'QSOverlayPlayerBackClick',
{ test: 'a' },
(res) => {
if (res.status === 'success') { console.log('通知成功');
}
}
);
build
├── index.html
└── static
├── css
│ ├── main.f855e6bc.css
├── js
│ ├── 787.d4aba7ab.chunk.js
│ ├── main.8381e2a9.js
└── img
└── arrow.80454996.svg
zip
└── page-frame.html
├── config.json
├── css
│ ├── main.f855e6bc.css
├── js
│ ├── 787.d4aba7ab.chunk.js
│ ├── main.8381e2a9.js
└── img
└── arrow.80454996.svg
{
"global": {
"showNavigationBar": false,
"themes": {
"black": {
"backgroundColor": "#0a0c0e"
},
"white": {
"backgroundColor": "#FFFFFF"
}
}
},
"pages": {
"index": {
"showNavigationBar": false
},
"detail": {
"showNavigationBar": true,
"themes": {} }
}
}
[{
name: 'https://domain-one.com/path/page-frame.html',
test: function(options) {
const {
path
options; =
return /NewsTZBD/i.test(path);
},
config: {
global: {
showNavigationBar: false,
themes: {
panda: {
backgroundColor: "#f5f6fa",
},
black: {
backgroundColor: "#12161f",
},
blue: {
backgroundColor: "#f5f6fa",
}
},
},
pages: {
index: {
showNavigationBar: false,
},
},
},
{
name: 'https://domain-two.com/path/page-frame.html',
config: {},
}]
class BridgeNameSpace {
/**
* @params{Object} params 传递数据 { url, p_showNav}
* params.url
*/
navigateTo(params) {
this.invoke('navigateTo', params, () => {
//
})
}
}
const url = 'https://domain.com/path/index.html?pid=xxx#/index';
BridgeNameSpace.navigateTo({
p_url: url,
p_showNav: true,
});
class BridgeNameSpace {
/**
* 显示toast
* @param {String} position 弹出位置,center(中间),top(顶部)
* @param {String} text 要提示的⽂字
*/
showToast(position, text, callback) {
this.invoke('showToast', { position, text }, callback);
},
/**
* loading view控制 loadingBar
* @param {String} action: show/hide, 控制显示/隐藏
*/
loadingBar(action, callback) {
this.invoke('loadingBar', { action }, callback);
}
}
class BridgeNameSpace {
setHeaderConfig(config, callback) {
this.invoke('setHeaderConfig', {
title: config.title,
subTitle: config.subTitle,
right: [{
actionName: 'font',
}, {
actionName: 'share',
// 可传入图标,没有使用系统默认的
icon: '',
}]
}, callback);
},
/**
* 监听按钮点击事件
*/
onHeaderButtonClick(callback) {
this.on('onHeaderButtonClick', callback);
}
}
class BridgeNameSpace {
/**
* 启用下拉刷新(默认关闭),前端仍然可以决定是否使用 Native 刷新控件
* @param {Boolean} enabled 下拉刷新开启标识
* @param callback
*/
enablePullDownRefresh(enabled, callback) {
this.invoke('enablePullDownRefresh', { enabled }, callback);
},
/**
* 下拉刷新,通过 API 调用即可触发,和手动刷新一致
* @function startPullDownRefresh
*/
startPullDownRefresh(callback) {
this.invoke('startPullDownRefresh', {}, callback);
}
/**
* 下拉刷新完成调用,将收起下拉刷新条
*/
stopPullDownRefresh(callback) {
this.invoke('stopPullDownRefresh', {}, callback);
},
/**
* 下拉刷新触发通知
* @param {Function} callback 回调函数
*/
onPullDownRefresh(callback) {
this.on('onPullDownRefresh', callback);
}
}
本篇文章的完成,离不开前人经验的总结,甚至有部分代码是直接参考,以下是主要参考链接:
移动 H5 首屏秒开优化方案探讨:https://blog.cnbang.net/tech/3477/
70%以上业务由H5开发,手机QQ Hybrid 的架构如何优化演进?:https://mp.weixin.qq.com/s/evzDnTsHrAr2b9jcevwBzA
📢📢欢迎加入腾讯云开发者社群,享前沿资讯、大咖干货,找兴趣搭子,交同城好友,更有鹅厂招聘机会、限量周边好礼等你来~
(长按图片立即扫码)