背景与需求
在 UniApp 开发过程中,我们经常需要加载 H5 页面来展示复杂的业务内容,比如审批流程、表单填写、数据展示等。传统方案是使用原生插件来实现 WebView 功能,但这种方式存在以下问题:
- 依赖原生插件:需要维护 Android 和 iOS 两套原生代码
- 通信复杂:H5 页面与 UniApp 的数据交互实现困难
- 功能受限:文件上传、页面跳转等功能需要额外开发
- 维护成本高:版本更新时需要同步更新原生插件
本文将介绍一套完整的 UniApp WebView 解决方案,实现 H5 页面与 UniApp 的无缝集成。
核心功能需求
我们的目标是实现以下功能:
- ✅ 基础加载:在 UniApp 中加载任意 H5 页面
- ✅ 双向通信:H5 页面能调用 UniApp 功能,UniApp 能向 H5 页面传递数据
- ✅ 页面跳转:H5 页面中的房源编号、客源编号能直接跳转到对应详情页
- ✅ 文件上传:支持图片选择和文件选择功能
- ✅ 业务集成:支持表单提交、数据回调等业务场景
解决方案演进
方案一:自定义注入 Bridge(失败)
思路:通过 evalJS
向 WebView 注入自定义的通信桥接对象。
// 尝试注入自定义 Bridge
webview.evalJS(`
window.uniAppBridge = {
jumpToPropertyDetail: function(code) { /* ... */ }
};
`);
问题:
- 在 Android 平台上注入不稳定
- 时机难以控制,容易失败
- 兼容性差
方案二:使用 UniApp 官方 SDK(成功)
思路:使用 UniApp 官方提供的 WebView 通信 SDK。
<!-- 引入官方 SDK -->
<script src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script>
优势:
- 官方支持,稳定可靠
- 标准化的通信方式
- 完整的 API 支持
完整实现方案
1. 创建 WebView 页面组件
创建 pages/common/webview-page-simple.vue
:
<template>
<view class="webview-container">
<web-view
:src="webUrl"
@message="handleMessage"
></web-view>
</view>
</template>
<script>
import CookieUtils from '@/util/cookie.js';
export default {
data() {
return {
webUrl: ''
}
},
onLoad(options) {
if (options.url) {
let url = decodeURIComponent(options.url);
// 处理本地文件路径
if (url.startsWith('/')) {
// #ifdef H5
url = window.location.origin + url;
// #endif
// #ifdef APP-PLUS
// 直接使用相对路径,UniApp 会自动处理
url = url;
// #endif
}
// 添加参数
const cookie = CookieUtils.getCookie();
const separator = url.includes('?') ? '&' : '?';
this.webUrl = `${url}${separator}cookie=${encodeURIComponent(cookie)}&platform=uniapp&device=${uni.getSystemInfoSync().platform}`;
console.log('加载URL:', this.webUrl);
}
// 设置标题
if (options.title) {
uni.setNavigationBarTitle({
title: decodeURIComponent(options.title)
});
}
},
methods: {
handleMessage(e) {
console.log('收到消息:', e.detail.data);
const data = e.detail.data;
const messages = Array.isArray(data) ? data : [data];
messages.forEach(message => {
this.processMessage(message);
});
},
processMessage(message) {
switch (message.action || message.type) {
case 'navigateTo':
this.handleNavigate(message);
break;
case 'jumpToPropertyDetail':
this.jumpToPropertyDetail(message.data || message);
break;
case 'jumpToInquiryDetail':
this.jumpToInquiryDetail(message.data || message);
break;
case 'chooseImage':
this.handleChooseImage(message);
break;
case 'chooseFile':
this.handleChooseFile(message);
break;
case 'submitApproval':
this.handleSubmitApproval(message);
break;
case 'back':
uni.navigateBack();
break;
default:
console.log('未知消息类型:', message);
}
},
jumpToPropertyDetail(data) {
const propertyCode = data.propertyCode || data.code || data.PropertyCode;
if (propertyCode) {
uni.navigateTo({
url: `/pages/house/detail-page?PropertyCode=${propertyCode}&isWeb=true`
});
}
},
jumpToInquiryDetail(data) {
const inquiryCode = data.inquiryCode || data.code || data.InquiryCode;
if (inquiryCode) {
uni.navigateTo({
url: `/pages/passenger/passenger-detail?InquiryCode=${inquiryCode}&isWeb=true`
});
}
},
handleChooseImage(message) {
uni.chooseImage({
count: message.count || 9,
success: (res) => {
console.log('选择图片成功:', res);
uni.showToast({
title: `已选择 ${res.tempFilePaths.length} 张图片`,
icon: 'success'
});
},
fail: (err) => {
console.error('选择图片失败:', err);
uni.showToast({
title: '选择图片失败',
icon: 'none'
});
}
});
},
handleChooseFile(message) {
// #ifdef APP-PLUS
uni.chooseFile({
count: message.count || 1,
extension: message.extensions || ['*'],
success: (res) => {
console.log('选择文件成功:', res);
uni.showToast({
title: `已选择 ${res.tempFiles.length} 个文件`,
icon: 'success'
});
},
fail: (err) => {
console.error('选择文件失败:', err);
uni.showToast({
title: '选择文件失败',
icon: 'none'
});
}
});
// #endif
// #ifdef H5
uni.showModal({
title: '提示',
content: 'H5环境暂不支持文件选择,请使用图片选择功能',
showCancel: false
});
// #endif
},
handleSubmitApproval(message) {
console.log('处理审批提交:', message);
const data = message.data || {};
uni.showToast({
title: '审批已提交',
icon: 'success'
});
setTimeout(() => {
uni.showModal({
title: '提交完成',
content: '审批已成功提交,是否返回?',
success: (res) => {
if (res.confirm) {
uni.navigateBack();
}
}
});
}, 1500);
}
}
}
</script>
<style scoped>
.webview-container {
width: 100%;
height: 100vh;
}
</style>
2. 注册页面路由
在 pages.json
中添加:
{
"path": "pages/common/webview-page-simple",
"style": {
"navigationBarTitleText": "加载中...",
"navigationBarTextStyle": "black"
}
}
3. 封装调用方法
在 util/native_plug_util.js
中添加:
/**
* 跳转到 UniApp 的简化 web-view 页面(推荐使用)
* @param {Object} url h5页面地址
* @param {Object} title 页面标题
* @param {Object} options 额外选项
*/
jumpToUniWebViewSimple(url, title, options = {}) {
// 对 URL 进行编码,避免参数丢失
const encodedUrl = encodeURIComponent(url);
let navigateUrl = `/pages/common/webview-page-simple?url=${encodedUrl}`;
// 如果有标题,也进行编码
if (title) {
const encodedTitle = encodeURIComponent(title);
navigateUrl += `&title=${encodedTitle}`;
}
// 添加额外参数
if (options.useSDK !== false) {
navigateUrl += '&useSDK=true';
}
// 跳转到 UniApp 的简化 web-view 页面
uni.navigateTo({
url: navigateUrl,
success: () => {
console.log('成功跳转到 WebView 页面:', url);
},
fail: (err) => {
console.error('跳转到 web-view 页面失败:', err);
uni.showToast({
title: '页面跳转失败',
icon: 'none'
});
}
});
},
4. 创建业务 H5 页面模板
创建 static/html/business_template.html
:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>业务页面模板</title>
<!-- 引入 UniApp WebView SDK -->
<script type="text/javascript" src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script>
<style>
/* 样式代码... */
body {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.btn {
display: inline-block;
padding: 12px 24px;
margin: 5px;
background: #007aff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.property-link {
color: #007aff;
text-decoration: underline;
cursor: pointer;
font-weight: bold;
}
</style>
</head>
<body>
<!-- 状态指示器 -->
<div id="status" class="status-indicator">初始化中...</div>
<div class="container">
<div class="header">
<h1>业务审批页面</h1>
</div>
<div class="content">
<!-- 房源/客源信息展示 -->
<div class="form-group">
<label>关联房源:</label>
<div>
<span class="property-link" onclick="jumpToProperty('FY20240101')">
房源编号:FY20240101 - 某某小区3室2厅
</span>
</div>
</div>
<!-- 表单字段 -->
<div class="form-group">
<label for="title">审批标题:</label>
<input type="text" id="title" placeholder="请输入审批标题">
</div>
<div class="form-group">
<label for="content">审批内容:</label>
<textarea id="content" placeholder="请输入审批内容..."></textarea>
</div>
<!-- 文件上传区域 -->
<div class="form-group">
<label>附件上传:</label>
<div class="file-upload" onclick="uploadFiles()">
<p>点击选择文件或图片</p>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="footer">
<button class="btn" onclick="submitApproval()">提交审批</button>
<button class="btn" onclick="goBack()">返回</button>
</div>
</div>
<script>
let isUniAppReady = false;
// 更新状态指示器
function updateStatus(ready) {
const statusEl = document.getElementById('status');
if (ready) {
statusEl.textContent = '✓ 通信就绪';
statusEl.style.background = '#d4edda';
statusEl.style.color = '#155724';
isUniAppReady = true;
} else {
statusEl.textContent = '✗ 通信未就绪';
statusEl.style.background = '#f8d7da';
statusEl.style.color = '#721c24';
}
}
// 监听 UniApp 环境准备就绪
document.addEventListener('UniAppJSBridgeReady', function() {
console.log('UniApp 通信已就绪');
updateStatus(true);
});
// 跳转到房源详情
function jumpToProperty(propertyCode) {
if (!isUniAppReady) {
alert('通信未就绪,请稍后再试');
return;
}
console.log('跳转到房源详情:', propertyCode);
uni.postMessage({
data: {
type: 'jumpToPropertyDetail',
data: {
propertyCode: propertyCode
}
}
});
}
// 上传文件
function uploadFiles() {
if (!isUniAppReady) {
alert('通信未就绪,请稍后再试');
return;
}
const choice = confirm('选择"确定"上传图片,选择"取消"上传文件');
if (choice) {
// 通过 UniApp 选择图片
uni.postMessage({
data: {
action: 'chooseImage',
count: 9
}
});
} else {
// 选择文件
uni.postMessage({
data: {
action: 'chooseFile',
count: 5,
extensions: ['.pdf', '.doc', '.docx']
}
});
}
}
// 提交审批
function submitApproval() {
const title = document.getElementById('title').value;
const content = document.getElementById('content').value;
if (!title || !content) {
alert('请填写完整的审批信息');
return;
}
const approvalData = {
title: title,
content: content,
timestamp: new Date().toISOString()
};
console.log('提交审批数据:', approvalData);
if (isUniAppReady) {
uni.postMessage({
data: {
action: 'submitApproval',
data: approvalData
}
});
} else {
alert('通信未就绪,请稍后再试');
}
}
// 返回上一页
function goBack() {
if (isUniAppReady && uni.navigateBack) {
uni.navigateBack();
} else if (isUniAppReady) {
uni.postMessage({
data: { type: 'back' }
});
} else {
window.history.back();
}
}
// 页面加载完成
window.onload = function() {
console.log('业务页面加载完成');
// 检查环境
setTimeout(function() {
if (window.uni) {
updateStatus(true);
} else {
updateStatus(false);
}
}, 1000);
};
</script>
</body>
</html>
使用方法
1. 在 UniApp 中调用
import nativePlugUtil from '@/util/native_plug_util.js';
// 加载远程页面
nativePlugUtil.jumpToUniWebViewSimple('https://your-domain.com/approval.html', '审批页面');
// 加载本地页面
nativePlugUtil.jumpToUniWebViewSimple('/static/html/business_template.html', '业务页面');
2. H5 页面与 UniApp 通信
页面跳转
// 跳转到房源详情
uni.postMessage({
data: {
type: 'jumpToPropertyDetail',
data: { propertyCode: 'FY123456' }
}
});
// 跳转到客源详情
uni.postMessage({
data: {
type: 'jumpToInquiryDetail',
data: { inquiryCode: 'KY123456' }
}
});
文件操作
// 选择图片
uni.postMessage({
data: {
action: 'chooseImage',
count: 9
}
});
// 选择文件
uni.postMessage({
data: {
action: 'chooseFile',
extensions: ['.pdf', '.doc', '.docx']
}
});
业务操作
// 提交数据
uni.postMessage({
data: {
action: 'submitApproval',
data: {
title: '审批标题',
content: '审批内容'
}
}
});
核心技术要点
1. SDK 引入
关键:必须在 H5 页面中引入 UniApp 官方 SDK。
<script src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script>
2. 环境检测
// 监听环境就绪
document.addEventListener('UniAppJSBridgeReady', function() {
console.log('通信已就绪');
// 此时可以安全使用 uni.postMessage
});
3. 消息通信
H5 → UniApp:
uni.postMessage({
data: {
type: 'messageType',
data: { /* 数据 */ }
}
});
UniApp → H5:
// 在 handleMessage 方法中处理
handleMessage(e) {
const message = e.detail.data;
// 处理消息
}
4. 错误处理
function safeCall(callback) {
if (!isUniAppReady) {
alert('通信未就绪,请稍后再试');
return;
}
callback();
}
最佳实践
1. 页面结构规范
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>页面标题</title>
<!-- 必须:引入 SDK -->
<script src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script>
</head>
<body>
<!-- 页面内容 -->
<script>
// 必须:监听环境就绪
document.addEventListener('UniAppJSBridgeReady', function() {
console.log('通信已就绪');
});
</script>
</body>
</html>
2. 状态管理
let isUniAppReady = false;
function updateStatus(ready) {
isUniAppReady = ready;
// 更新 UI 状态指示器
}
function safeExecute(fn) {
if (isUniAppReady) {
fn();
} else {
console.warn('UniApp 通信未就绪');
}
}
3. 错误处理
// 统一的错误处理
function handleError(error, context) {
console.error(`${context} 出错:`, error);
if (isUniAppReady) {
uni.postMessage({
data: {
type: 'error',
error: error.message,
context: context
}
});
} else {
alert(`操作失败: ${error.message}`);
}
}
常见问题与解决方案
1. SDK 加载失败
问题:网络问题导致 SDK 无法加载
解决:
<!-- 使用多个 CDN 源 -->
<script>
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
const sdkSources = [
'https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js',
'https://unpkg.com/@dcloudio/uni-webview-js@0.0.3/index.js'
];
async function loadUniSDK() {
for (const src of sdkSources) {
try {
await loadScript(src);
console.log('SDK 加载成功');
break;
} catch (e) {
console.warn('SDK 加载失败,尝试下一个源', src);
}
}
}
loadUniSDK();
</script>
2. 通信失败
问题:发送消息后没有响应
检查步骤:
- 确认 SDK 已正确加载
- 确认监听了
UniAppJSBridgeReady
事件 - 确认消息格式正确
- 检查 UniApp 端的消息处理逻辑
3. 文件上传问题
问题:选择文件后没有反应
解决:
// 确保在 APP 环境下使用
// #ifdef APP-PLUS
uni.chooseFile({
// 文件选择逻辑
});
// #endif
// #ifdef H5
// H5 环境下提供替代方案
uni.showModal({
title: '提示',
content: 'H5环境请使用图片上传功能'
});
// #endif
4. 页面跳转问题
问题:点击链接无法跳转
解决:
// 确保传递正确的参数格式
uni.postMessage({
data: {
type: 'jumpToPropertyDetail', // 确保类型正确
data: {
propertyCode: code // 确保字段名正确
}
}
});
总结
通过使用 UniApp 官方 WebView SDK,我们可以实现:
- 稳定的通信:基于官方 SDK,兼容性好
- 丰富的功能:支持文件上传、页面跳转、数据回调等
- 简单的维护:无需维护原生插件代码
- 良好的体验:接近原生应用的使用体验
这套解决方案已在实际项目中验证,能够满足大部分 H5 页面集成需求。我们成功地将复杂的原生 WebView 实现简化为纯 UniApp 代码,大大提升了开发效率和维护性。