UniApp 加载 Web 页面完整解决方案_uniwebview
背景与需求
在 UniApp 开发过程中,我们经常需要加载 H5 页面来展示复杂的业务内容,比如审批流程、表单填写、数据展示等。传统方案是使用原生插件来实现 WebView 功能,但这种方式存在以下问题:
- 依赖原生插件:需要维护 Android 和 iOS 两套原生代码
- 通信复杂:H5 页面与 UniApp 的数据交互实现困难
- 功能受限:文件上传、页面跳转等功能需要额外开发
- 维护成本高:版本更新时需要同步更新原生插件
本文将介绍一套完整的 UniApp WebView 解决方案,实现 H5 页面与 UniApp 的无缝集成。
核心功能需求
我们的目标是实现以下功能:
- ✅ 基础加载:在 UniApp 中加载任意 H5 页面
- ✅ 双向通信:H5 页面能调用 UniApp 功能,UniApp 能向 H5 页面传递数据
- ✅ 页面跳转:H5 页面中的房源编号、客源编号能直接跳转到对应详情页
- ✅ 文件上传:支持图片选择和文件选择功能
- ✅ 业务集成:支持表单提交、数据回调等业务场景
解决方案演进
方案一:自定义注入 Bridge(失败)
思路:通过 evalJS
向 WebView 注入自定义的通信桥接对象。
// 尝试注入自定义 Bridgewebview.evalJS(` window.uniAppBridge = { jumpToPropertyDetail: function(code) { /* ... */ } };`);
问题:
- 在 Android 平台上注入不稳定
- 时机难以控制,容易失败
- 兼容性差
方案二:使用 UniApp 官方 SDK(成功)
思路:使用 UniApp 官方提供的 WebView 通信 SDK。
优势:
- 官方支持,稳定可靠
- 标准化的通信方式
- 完整的 API 支持
完整实现方案
1. 创建 WebView 页面组件
创建 pages/common/webview-page-simple.vue
:
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); } }}.webview-container { width: 100%; height: 100vh;}
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
:
业务页面模板 /* 样式代码... */ 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; } 初始化中... 业务审批页面
房源编号:FY20240101 - 某某小区3室2厅 点击选择文件或图片
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); };
使用方法
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。
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. 页面结构规范
页面标题 // 必须:监听环境就绪 document.addEventListener(\'UniAppJSBridgeReady\', function() { console.log(\'通信已就绪\'); });
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 无法加载
解决:
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();
2. 通信失败
问题:发送消息后没有响应
检查步骤:
- 确认 SDK 已正确加载
- 确认监听了
UniAppJSBridgeReady
事件 - 确认消息格式正确
- 检查 UniApp 端的消息处理逻辑
3. 文件上传问题
问题:选择文件后没有反应
解决:
// 确保在 APP 环境下使用// #ifdef APP-PLUSuni.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 代码,大大提升了开发效率和维护性。