Appearance
<web-view> 组件接入
接入方式
在 uni-app 页面模板中声明 <web-view>,由组件创建、挂载并管理 WebView。手动创建容器的方案保留在 HTML5+ WebView 接入。两篇使用相同的 H5 业务入口和事件格式,区别在于容器创建方式和消息传输层。
本文面向 Android / iOS App-vue 的 .vue 页面:
| 方向 | H5 侧 | App 侧 |
|---|---|---|
| H5 调 App | htmlToFlutterEvent.postMessage,通过注入脚本适配为 uni.webView.postMessage | 组件的 @message 接收,再通过子 WebView 的 evalJS 回传结果 |
| App 调 H5 | flutterToHtmlEvent 接收对象并返回结果 | 子 WebView 的 evalJS 调用,再通过组件消息带回执行结果 |
App-vue 中可从页面的 $getAppWebview().children() 取得组件创建的子 WebView。App-nvue 使用 @onPostMessage 和组件 ref 上的 evalJS;App-vue 使用本篇的 @message 和子 WebView 实例。组件消息的数据位于 event.detail.data 数组中,App 端支持实时投递。DCloud 组件文档
一、本地桥接脚本示例
在接入方 uni-app 项目的 static 目录准备三个文件:
| 文件 | 来源与用途 |
|---|---|
| uni.webview.1.5.8.js | 从 DCloud 官方 SDK 地址 下载实际 JavaScript 文件,提供网页调用 uni 接口的能力 |
| bridgeRuntime.js | 下载 公共适配脚本,补齐 Promise、请求配对及可选 callback |
| uniComponentBridge.js | 使用下面的代码,将公共入口适配到组件消息 |
uni.webView.postMessage 本身不返回本次业务的 Promise 结果,因此还需通过 evalJS 将响应送回公共适配脚本。业务 H5 继续调用原有的 htmlToFlutterEvent.postMessage。
uniComponentBridge.js:
javascript
(function () {
if (window.UniComponentBridge) return;
var documentId = 'doc_' + Date.now() + '_' + Math.random().toString(36).slice(2);
var installed = false;
function send(message) {
message.source = 'live-component';
message.documentId = documentId;
window.uni.webView.postMessage({ data: message });
}
function install() {
if (installed || !window.UniAppJSBridge || !window.uni ||
!window.uni.webView || typeof window.uni.webView.postMessage !== 'function') return;
installed = true;
window.UniComponentBridge = {
documentId: documentId,
sendResult: function (id, result) {
send({ kind: 'h5Result', id: id, result: result });
}
};
window.AppBridgeAdapter.install(function (id, payload) {
send({ kind: 'request', id: id, payload: payload });
});
send({ kind: 'ready' });
}
document.addEventListener('UniAppJSBridgeReady', install);
install(); // SDK 已经就绪时直接安装,兼容脚本晚于就绪事件执行。
})();source、documentId、kind、id 用于适配层的消息与文档匹配,不作为直播间业务参数传入。SDK 就绪后才安装通道,App 收到 ready 后才主动调用 H5。
二、在页面直接声明组件示例
以下是一份 App-vue 页面示例。页面只包含一个 <web-view>,因此可以使用 children()[0];存在多个子 WebView 时,需要自行按目标实例定位。
先让组件加载 about:blank,取得实例、安装脚本和监听后,再更新 src 加载观看 URL。这样首次实际观看请求前就已设置 UA。账号打通和观看 URL 的获取见 接入总览。
vue
<template>
<view>
<web-view
v-if="showWebview"
:src="src"
:update-title="false"
@message="onWebMessage"
></web-view>
</view>
</template>
<script>
export default {
data: function () {
return { showWebview: false, src: 'about:blank' };
},
onLoad: function (query) {
this.watchUrl = decodeURIComponent(query.url || '');
this.nativeView = null;
this.documentId = null;
this.h5Pending = new Map();
this.sequence = 0;
this.closed = false;
this.closing = false;
// #ifdef APP-PLUS
if (!/^https:\/\/live\.example\.com(?::443)?\//.test(this.watchUrl)) return;
this.platform = uni.getSystemInfoSync().platform === 'ios' ? 'iOS' : 'Android';
this.previousUA = plus.navigator.getUserAgent();
plus.navigator.setUserAgent(this.previousUA + ' channel/APP' + this.platform +
' version/1.0.0 package/com.example.live' +
' wxappid/example_wechat_appid scheme/examplelive');
this.showWebview = true;
// #endif
},
onReady: function () {
// #ifdef APP-PLUS
if (!this.showWebview) return;
var self = this;
var attempts = 0;
var parent = this.$scope.$getAppWebview();
function attach() {
if (self.closed) return;
var children = parent.children();
if (children.length !== 1) {
if (++attempts < 40) self.attachTimer = setTimeout(attach, 50);
else uni.showToast({ title: '未找到观看组件', icon: 'none' });
return;
}
var child = children[0];
self.nativeView = child;
self.loadingListener = function () {
self.documentId = null;
self.cancelH5Requests('H5 已导航');
};
child.addEventListener('loading', self.loadingListener);
child.overrideUrlLoading({
mode: 'reject',
match: '^(?!https://live\\.example\\.com(?::443)?(?:/|$)).*'
}, function () {});
child.appendJsFile('_www/static/uni.webview.1.5.8.js');
child.appendJsFile('_www/static/bridgeRuntime.js');
child.appendJsFile('_www/static/uniComponentBridge.js');
self.src = self.watchUrl; // 由组件加载地址,不手动 create、append 或 loadURL。
}
attach();
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.nativeView) return false;
var self = this;
this.nativeView.canBack(function (event) {
if (self.closed) return;
if (event.canBack) self.nativeView.back();
else self.closePage();
});
return true;
},
onUnload: function () {
this.closed = true;
clearTimeout(this.attachTimer);
this.cancelH5Requests('页面已关闭');
if (this.appLifecycleListener) uni.$off('live-app-lifecycle', this.appLifecycleListener);
if (this.nativeView && this.loadingListener) {
this.nativeView.removeEventListener('loading', this.loadingListener);
}
this.nativeView = null; // 容器由组件随页面销毁,不重复 close。
// #ifdef APP-PLUS
if (this.previousUA) plus.navigator.setUserAgent(this.previousUA);
// #endif
},
methods: {
onWebMessage: function (event) {
if (this.closed) return;
var self = this;
var messages = event.detail && event.detail.data;
if (!Array.isArray(messages)) return;
messages.forEach(function (message) {
if (!message || message.source !== 'live-component') return;
if (message.kind === 'ready') {
self.documentId = message.documentId;
return;
}
if (!self.documentId || message.documentId !== self.documentId) return;
if (message.kind === 'h5Result') {
var pending = self.h5Pending.get(message.id);
if (!pending) return;
clearTimeout(pending.timer);
self.h5Pending.delete(message.id);
pending.resolve(message.result);
} else if (message.kind === 'request') {
var child = self.nativeView;
var doc = message.documentId;
function reply(result) {
if (self.closed || self.nativeView !== child || self.documentId !== doc) return;
child.evalJS('if(window.UniComponentBridge && window.UniComponentBridge.documentId===' +
JSON.stringify(doc) + '){window.AppBridgeAdapter.receive(' + JSON.stringify(message.id) +
',' + JSON.stringify(JSON.stringify(result)) + ');}');
}
self.handleNativeRequest(message.payload).then(reply, function () {
reply({ code: 500, message: 'App 执行失败', data: null });
});
}
});
},
cancelH5Requests: function (reason) {
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();
},
// 将下文 handleNativeRequest、callH5 方法加入此处,方法之间用逗号分隔。
}
};
</script>App-vue 的组件 ref 不直接提供本篇使用的 evalJS;这里操作的是组件已经创建的原生实例。appendJsFile 会按添加顺序加载本地文件,并在导航到新文档时再次执行。HTML5+ WebView API
示例域名、应用版本、包名、微信 AppID 和 scheme 需要替换为自己的配置。setUserAgent 会影响之后创建的 WebView,不能据此更新已有子 WebView;因此示例在修改 UA 后才通过 v-if 创建组件。多容器项目应统一管理 UA,关闭时的恢复操作不代表已创建实例也已恢复。UA API 说明
Android / iOS App-vue 使用组件创建的子 WebView 执行脚本;uni.createWebviewContext 的平台支持范围见 上下文 API 兼容说明。
三、H5 调 App示例:请求、接收和结果回传
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;
}App 接收并回传示例
将该方法加入页面 methods。@message 对应的 onWebMessage 会调用此方法,再用 evalJS 执行 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 { 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;
}可选 callback 由公共适配脚本处理,App 不再重复调用。getUserInfo、分享等其他事件仍需在 App 分发方法中按实际业务实现。
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 对象内,彼此同级。
四、App 调 H5示例:执行并读取返回值
App 发起调用示例
将该方法加入同一个 methods。evalJS 不直接返回业务值,因此由注入层的 sendResult 通过组件消息送回,并按请求编号完成 App 的 Promise。
javascript
callH5: function (event, data) {
var self = this;
if (!this.nativeView || !this.documentId || this.closed) {
return Promise.reject(new Error('H5 通道尚未就绪'));
}
var id = 'native_' + (++this.sequence);
var doc = this.documentId;
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,doc,message){' +
'var bridge=window.UniComponentBridge;' +
'if(!bridge || bridge.documentId!==doc)return;' +
'function fail(){bridge.sendResult(id,{code:500,message:"H5 执行失败",data:null});}' +
'try{' +
'var result=typeof window.flutterToHtmlEvent==="function"' +
'?window.flutterToHtmlEvent(message)' +
':{code:400,message:"H5 处理函数不存在",data:null};' +
'Promise.resolve(result).then(function(value){bridge.sendResult(id,value);},fail);' +
'}catch(error){fail();}' +
'})(' + JSON.stringify(id) + ',' + JSON.stringify(doc) + ',' + JSON.stringify(message) + ');';
self.nativeView.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);
}H5 接收并返回示例
真实直播间已有 flutterToHtmlEvent,App 直接调用,不覆盖现有实现。以下用于自建测试 H5,接收函数与其他方案相同:
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' || message.event === 'pageLifecycle') {
console.log(message.event, message.data);
} else {
result.code = 400;
result.message = '未知事件';
}
return result;
};五、前后台、返回和组件销毁示例
App 的 App.vue 发出真实的前后台事件,上面的页面监听后调用 H5:
javascript
export default {
onShow: function () { uni.$emit('live-app-lifecycle', 'resumed'); },
onHide: function () { uni.$emit('live-app-lifecycle', 'paused'); }
};返回键优先处理网页历史,无历史时先通知 pageLifecycle / dispose 再退出路由。完整导航清理旧文档的查询,页面卸载移除监听;组件随页面释放原生容器。示例超时按实际业务调整,分享等需要用户操作的事件不能简单套用短超时。
uni.navigateBack() 触发的 onBackPress 必须放行,否则关闭流程可能被自身拦截。iOS 侧滑返回不会触发该钩子;需要在关闭前通知 H5 时,应另行管理手势并统一关闭入口。官方返回逻辑说明、页面生命周期说明
联调检查
- Android / iOS App 中确认页面由
<web-view>加载,未额外创建第二个容器。 - 验证首次观看请求的 UA 和参数,确认 SDK、本地脚本按顺序执行并收到
ready。 - H5 查询
getAppInfo,确认@message实时收到请求,H5 Promise 获得结果;调用openNativePage,验证订单参数、跳转响应及路由失败处理。 - App 查询
getVideoInfo,确认 H5 返回尺寸,多个并发请求正确配对。 - 刷新、完整导航、返回关闭及切换前后台后,确认无旧请求回传或残留监听。