UniApp 加载 Web 页面完整解决方案

发布于:2025-07-03 ⋅ 阅读:(19) ⋅ 点赞:(0)

背景与需求

在 UniApp 开发过程中,我们经常需要加载 H5 页面来展示复杂的业务内容,比如审批流程、表单填写、数据展示等。传统方案是使用原生插件来实现 WebView 功能,但这种方式存在以下问题:

  1. 依赖原生插件:需要维护 Android 和 iOS 两套原生代码
  2. 通信复杂:H5 页面与 UniApp 的数据交互实现困难
  3. 功能受限:文件上传、页面跳转等功能需要额外开发
  4. 维护成本高:版本更新时需要同步更新原生插件

本文将介绍一套完整的 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. 通信失败

问题:发送消息后没有响应
检查步骤

  1. 确认 SDK 已正确加载
  2. 确认监听了 UniAppJSBridgeReady 事件
  3. 确认消息格式正确
  4. 检查 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,我们可以实现:

  1. 稳定的通信:基于官方 SDK,兼容性好
  2. 丰富的功能:支持文件上传、页面跳转、数据回调等
  3. 简单的维护:无需维护原生插件代码
  4. 良好的体验:接近原生应用的使用体验

这套解决方案已在实际项目中验证,能够满足大部分 H5 页面集成需求。我们成功地将复杂的原生 WebView 实现简化为纯 UniApp 代码,大大提升了开发效率和维护性。