UniApp 加载 Web 页面完整解决方案
背景与需求
在 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 H5url = 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-PLUSuni.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 H5uni.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 代码,大大提升了开发效率和维护性。