Appearance
HTML5+ WebView 接入
接入方式
本文介绍手动创建 HTML5+ WebView 的接入方式。页面中直接使用组件的方案见 <web-view> 组件接入。
本文使用 uni-app App-vue + HTML5+ WebView,面向 Android / iOS App。创建子 WebView 承载直播间,使用 URL 拦截接收 H5 消息,使用 evalJS 执行 H5 代码。
| 方向 | H5 侧 | uni-app / Native 侧 |
|---|---|---|
| H5 调 Native | htmlToFlutterEvent.postMessage 返回 Promise | overrideUrlLoading 接收消息,业务处理后 evalJS 回传结果 |
| Native 调 H5 | flutterToHtmlEvent 接收对象并返回结果 | evalJS 发起调用;H5 将执行结果通过同一通道送回 |
H5 业务入口及事件名称与 共用协议 一致。示例通过自定义 appbridge:// 通道、请求编号和 ack 完成消息传输。业务消息通过桥接通道发送,不拼接到直播间观看 URL。本方案适用于 App-vue,不适用于浏览器 H5、小程序或 App-nvue。
一、注入脚本示例
下载 bridgeRuntime.js,将其内容与下面的脚本按先后顺序放入同一个 static/uniAppBridge.js 文件。前者提供 Promise 和 callback 配对,下面这段实现 URL 传输。
每条消息先进入发送队列,Native 拦截后立即确认,再发送下一条,避免多次设置 location.href 导致消息相互覆盖。确认仅表示 Native 已收到消息,业务结果另行回传。
javascript
(function () {
if (window.AppBridgeScheme) return;
var queue = [];
var current = null;
function flush() {
if (current || !queue.length) return;
current = queue.shift();
var item = current;
item.timer = setTimeout(function () {
if (item.kind === 'request') {
window.AppBridgeAdapter.fail(item.id, 'Native 未确认接收消息');
}
current = null;
flush();
}, 2000);
window.location.href = 'appbridge://dispatch?message=' +
encodeURIComponent(JSON.stringify(item.message));
}
function enqueue(message) {
queue.push({ id: message.id, kind: message.kind, message: message });
flush();
}
window.AppBridgeScheme = {
ack: function (id) {
if (!current || current.id !== id) return;
clearTimeout(current.timer);
current = null;
flush();
},
sendResult: function (id, result) {
enqueue({ kind: 'h5Result', id: id, result: result });
}
};
window.AppBridgeAdapter.install(function (id, payload) {
enqueue({ kind: 'request', id: id, payload: payload });
});
enqueue({ kind: 'ready', id: 'ready_' + Date.now() });
})();ack 的 2 秒和共用脚本的 10 秒都是示例超时,按目标设备和业务调整。脚本作为 App 本地资源随包发布,不依赖直播间修改业务代码。
二、创建 WebView 并安装收发通道示例
下面是 App-vue 页面的 <script>。query.url 是上一页使用 encodeURIComponent 编码传入的观看 URL。账号打通时,请传入服务端刚生成的完整登录 URL。
第三节和第四节的业务方法需要补入同一个 methods 中,方法之间使用逗号分隔。
javascript
export default {
onLoad: function (query) {
this.watchUrl = decodeURIComponent(query.url || '');
this.liveView = null;
this.pageGeneration = 0;
this.bridgeReady = false;
this.h5Pending = new Map();
this.sequence = 0;
this.closing = false;
},
onReady: function () {
// #ifdef APP-PLUS
var self = this;
if (!/^https:\/\/live\.example\.com(?::443)?\//.test(this.watchUrl)) return;
this.previousUA = plus.navigator.getUserAgent();
this.platform = uni.getSystemInfoSync().platform === 'ios' ? 'iOS' : 'Android';
plus.navigator.setUserAgent(this.previousUA + ' channel/APP' + this.platform +
' version/1.0.0 package/com.example.live' +
' wxappid/example_wechat_appid scheme/examplelive');
var child = plus.webview.create('about:blank', 'live_' + Date.now(), {
top: '0px', bottom: '0px'
});
this.liveView = child;
// 拦截示例通道和所有非观看域名的导航;可信域名替换为自己的配置。
child.overrideUrlLoading({
mode: 'reject',
match: '^(?!https://live\\.example\\.com(?::443)?(?:/|$)).*'
}, function (event) {
if (event.url.indexOf('appbridge://dispatch?message=') === 0) {
self.receiveBridgeURL(event.url);
}
});
child.addEventListener('loading', function () {
self.bridgeReady = false;
self.pageGeneration++;
self.cancelH5Requests('页面已导航');
});
child.appendJsFile('_www/static/uniAppBridge.js');
this.$scope.$getAppWebview().append(child);
child.loadURL(this.watchUrl);
this.appLifecycleListener = function (state) {
self.callH5('appLifecycle', state).catch(function () {});
};
uni.$on('live-app-lifecycle', this.appLifecycleListener);
// #endif
},
onBackPress: function (options) {
// closePage 中的 uni.navigateBack 也会触发此钩子,必须放行。
if (options && options.from === 'navigateBack') return false;
if (this.closing) return true;
if (!this.liveView) return false;
var self = this;
this.liveView.canBack(function (event) {
if (event.canBack) self.liveView.back();
else self.closePage();
});
return true;
},
onUnload: function () {
this.cancelH5Requests('页面已关闭');
if (this.appLifecycleListener) uni.$off('live-app-lifecycle', this.appLifecycleListener);
if (this.liveView) this.liveView.close();
this.liveView = null;
// #ifdef APP-PLUS
if (this.previousUA) plus.navigator.setUserAgent(this.previousUA);
// #endif
},
methods: {
receiveBridgeURL: function (url) {
var self = this;
var message;
try {
message = JSON.parse(decodeURIComponent(url.split('?message=')[1]));
} catch (error) { return; }
var child = this.liveView;
var generation = this.pageGeneration;
if (!child || typeof message.id !== 'string') return;
child.evalJS('window.AppBridgeScheme.ack(' + JSON.stringify(message.id) + ');');
if (message.kind === 'ready') {
this.bridgeReady = true;
} else if (message.kind === 'h5Result') {
var pending = this.h5Pending.get(message.id);
if (!pending) return;
clearTimeout(pending.timer);
this.h5Pending.delete(message.id);
pending.resolve(message.result);
} else if (message.kind === 'request') {
this.handleNativeRequest(message.payload).then(function (result) {
if (self.liveView !== child || self.pageGeneration !== generation) return;
child.evalJS('window.AppBridgeAdapter.receive(' + JSON.stringify(message.id) +
',' + JSON.stringify(JSON.stringify(result)) + ');');
});
}
},
cancelH5Requests: function (reason) {
if (!this.h5Pending) return;
this.h5Pending.forEach(function (pending) {
clearTimeout(pending.timer);
pending.reject(new Error(reason));
});
this.h5Pending.clear();
},
closePage: async function () {
if (this.closing) return;
this.closing = true;
try { await this.callH5('pageLifecycle', 'dispose'); } catch (error) {}
uni.navigateBack(); // onUnload 释放子 WebView。
},
// 在此继续添加下文的 handleNativeRequest 和 callH5 方法。
}
};plus.navigator.setUserAgent 是应用级设置,此示例在关闭时恢复。已有多个 WebView 并行工作的 App 应统一管理 UA;需要严格按容器隔离配置时,可用 纯原生容器 封装组件来实现。
appendJsFile 在页面加载时注入本地脚本。App 收到 ready 后再主动调用 H5;网页完整导航后需重新完成桥接初始化。
三、H5 调 Native示例:查询应用信息、打开页面
1. H5 发起请求并读取结果示例
javascript
async function onGetAppInfoClick() {
if (!window.htmlToFlutterEvent) throw new Error('App 通道尚未就绪');
var responseText = await window.htmlToFlutterEvent.postMessage(JSON.stringify({
event: 'getAppInfo', data: {}
}));
var result = JSON.parse(responseText);
if (Number(result.code) !== 200) throw new Error(result.message);
console.log(result.data.appName, result.data.platform);
}
async function onOpenNativePageClick() {
if (!window.htmlToFlutterEvent) throw new Error('App 通道尚未就绪');
var responseText = await window.htmlToFlutterEvent.postMessage(JSON.stringify({
event: 'openNativePage',
data: { page: 'orderDetail', params: { orderId: 'order_12345' } }
}));
var result = JSON.parse(responseText);
if (Number(result.code) !== 200) throw new Error(result.message);
return result.data;
}2. uni-app 分发业务并回传结果示例
将该方法加入上一节的 methods。H5 调用后,receiveBridgeURL 会调用这里,取得结果后执行 AppBridgeAdapter.receive,完成 H5 正在等待的 Promise。
javascript
handleNativeRequest: async function (payload) {
var request;
try {
request = JSON.parse(payload);
if (!request || typeof request.event !== 'string') throw new Error('Invalid event');
} catch (error) {
return { event: '', timestamp: new Date().toISOString(),
code: 400, message: '消息格式错误', data: null };
}
var result = {
event: request.event, timestamp: new Date().toISOString(),
code: 200, message: '', data: null
};
if (request.event === 'getAppInfo') {
result.data = {
appName: '示例应用', packageName: 'com.example.live',
version: '1.0.0', buildNumber: '1', platform: this.platform,
scheme: 'examplelive://'
};
} else if (request.event === 'openNativePage') {
var data = request.data || {};
var params = data.params || {};
if (data.page !== 'orderDetail' || typeof params.orderId !== 'string' || !params.orderId.trim()) {
result.code = 400;
result.message = '页面或订单参数错误';
} else {
try {
// 此路由需在接入项目的 pages.json 中注册。
await new Promise(function (resolve, reject) {
uni.navigateTo({
url: '/pages/order/detail?orderId=' + encodeURIComponent(params.orderId),
success: resolve,
fail: reject
});
});
result.data = { page: 'orderDetail' };
} catch (error) {
result.code = 500;
result.message = '无法打开 App 页面';
}
}
} else {
result.code = 400;
result.message = '未知事件';
}
return result;
}结果中的 data 就是 H5 上面读取的应用信息。实际项目应从应用配置取得这些值,并在此补齐页面所需的其他 Native 事件。可选 callback 由共用脚本处理,业务层不再重复调用。
openNativePage 是自定义事件示例,使用统一的 data.page、data.params,并将 orderDetail 映射到 /pages/order/detail。接入项目应实现 pages/order/detail.vue、在 pages.json 中注册它,并在目标页的 onLoad(query) 中读取 query.orderId。200 表示已发起路由跳转,参数缺失返回 400,路由失败返回 500。完整字段见 共用协议。
handleNativeRequest 与 closePage、callH5 放在同一个 methods 对象内,彼此同级。
四、Native 调 H5示例:查询视频尺寸
1. uni-app 执行 H5 函数并等待结果示例
HTML5+ 的 evalJS 不直接返回 JavaScript 业务结果。因此这里在执行 H5 函数后,通过注入脚本的 sendResult 把结果送回,使用请求编号完成本地 Promise。
将此方法加入同一个 methods:
javascript
callH5: function (event, data) {
var self = this;
if (!this.liveView || !this.bridgeReady) {
return Promise.reject(new Error('H5 通道尚未就绪'));
}
var id = 'native_' + this.pageGeneration + '_' + (++this.sequence);
var message = { event: event, timestamp: new Date().toISOString(), data: data };
return new Promise(function (resolve, reject) {
var timer = setTimeout(function () {
self.h5Pending.delete(id);
reject(new Error('H5 返回结果超时'));
}, 3000);
self.h5Pending.set(id, { resolve: resolve, reject: reject, timer: timer });
var source = '(function(id,message){' +
'try {' +
'var result = typeof window.flutterToHtmlEvent === "function"' +
'? window.flutterToHtmlEvent(message)' +
': {code:400,message:"H5 处理函数不存在",data:null};' +
'window.AppBridgeScheme.sendResult(id,result);' +
'} catch(error) {' +
'window.AppBridgeScheme.sendResult(id,{code:500,message:"H5 执行失败",data:null});' +
'}' +
'})(' + JSON.stringify(id) + ',' + JSON.stringify(message) + ');';
self.liveView.evalJS(source);
});
}页面收到 ready 后即可在按钮等业务入口中调用:
javascript
// 放入 methods,或者在页面现有的业务方法中执行。
queryVideoSize: async function () {
var result = await this.callH5('getVideoInfo', {});
if (Number(result.code) !== 200) throw new Error(result.message);
console.log(result.data.width, result.data.height);
}2. H5 接收调用并返回对象示例
现有直播间已经提供 flutterToHtmlEvent。以下是自建测试 H5 的接收示例;连接真实直播间时,App 直接调用已有函数,不覆盖它。
javascript
window.flutterToHtmlEvent = function (message) {
var result = {
event: message.event, timestamp: Date.now(),
code: 200, message: '', data: null
};
if (message.event === 'getVideoInfo') {
var video = document.querySelector('video');
if (video) result.data = { width: video.videoWidth, height: video.videoHeight };
else { result.code = 401; result.message = '播放器尚未就绪'; }
} else if (message.event === 'appLifecycle') {
console.log('App 生命周期', message.data);
} else if (message.event === 'pageLifecycle') {
console.log('页面生命周期', message.data);
} else {
result.code = 400;
result.message = '未知事件';
}
return result;
};此示例按现有 H5 的同步返回对象设计。若自建 H5 处理函数返回 Promise,应在执行脚本中等待完成后再调用 sendResult。
五、App 前后台与关闭示例
在 App 的 App.vue 实际生命周期中发送通知;上面 WebView 页面已注册监听,会调用 H5 的 flutterToHtmlEvent:
javascript
export default {
onShow: function () { uni.$emit('live-app-lifecycle', 'resumed'); },
onHide: function () { uni.$emit('live-app-lifecycle', 'paused'); }
};系统返回键先检查 WebView 浏览历史,无历史则发送 pageLifecycle / dispose 后退出页面。完整导航时清理旧文档请求;App 页面卸载时移除生命周期监听并释放子 WebView。
uni.navigateBack() 同样触发 onBackPress,示例通过 options.from 放行程序返回,避免再次拦截关闭流程。iOS 侧滑返回不会触发该钩子;如果业务要求关闭前必须通知 H5,应另行管理侧滑手势并统一关闭入口,不能仅依赖这里的返回拦截。官方返回逻辑说明、页面生命周期说明
联调检查
分别验证 H5 查询应用信息及调用 openNativePage 打开订单详情页、uni-app 查询 H5 视频尺寸、前后台通知、返回关闭和页面刷新。重点检查 URL 拦截是否生效、消息确认是否收到、连续请求是否丢失,以及导航后旧结果是否被丢弃。