Skip to content

Webview 前端與 App 的互動開發指南

在混合式應用開發中,Webview 是連接網頁和原生 App 的重要橋樑,本文將介紹如何實現兩者之間的互動。

前言

現代應用開發中,使用 Webview 來呈現網頁內容是很常見的做法,這讓我們能夠結合網頁技術的靈活性和原生 App 的功能性。本文將分享在實際開發中,如何實現前端網頁與 App 之間的有效溝通。

Webview 基本概念

什麼是 Webview

  • 原生 App 中的瀏覽器容器
  • 可以載入並顯示網頁內容
  • 支援 JavaScript 與原生 App 的互動

常見使用場景

  • 混合式應用開發
  • 動態更新內容
  • 複用現有網頁功能

判斷裝置系統

在 Webview 開發中,經常需要判斷當前運行的系統環境來執行不同的邏輯。以下是幾種常用的判斷方法:

1. 使用 navigator.userAgent

javascript
// 基本判斷方法
const isIOS = navigator.userAgent.match(/(ios|iphone|ipad|ipod|macintosh)/ig);
const isAndroid = navigator.userAgent.match(/(android|adr)/ig);

// 更完整的判斷方式
function getDeviceType() {
  const ua = navigator.userAgent;
  
  if (ua.match(/(ios|iphone|ipad|ipod|macintosh)/ig)) {
    return 'iOS';
  } else if (ua.match(/(android|adr)/ig)) {
    return 'Android';
  } else if (/Windows Phone/i.test(ua)) {
    return 'Windows Phone';
  } else {
    return 'Unknown';
  }
}

// 使用範例
const deviceType = getDeviceType();
console.log('當前裝置:', deviceType);

2. 使用平台特性判斷

javascript
// 更可靠的判斷方式,結合多個特徵
const deviceDetect = {
  isIOS() {
    return navigator.userAgent.match(/(ios|iphone|ipad|ipod|macintosh)/ig)
    // iPad on iOS 13 detection
    || (navigator.userAgent.includes("Mac") && "ontouchend" in document);
  },
  
  isAndroid() {
    return navigator.userAgent.match(/(android|adr)/ig);
  },

  isMobile() {
    return this.isIOS() || this.isAndroid();
  }
};

3. 判斷 Webview 環境

javascript
// 判斷是否在 Webview 中運行
const isInWebview = () => {
  const userAgent = navigator.userAgent.toLowerCase();
  
  const rules = [
    'webview',
    'wv', // Android webview
    '(iphone|ipod|ipad|ios|macintosh)(?!.*safari/)', // iOS webview
    'electron'
  ];
  
  return rules.some(rule => userAgent.includes(rule));
};

// 綜合判斷
const environment = {
  isWebview: isInWebview(),
  isIOS: deviceDetect.isIOS(),
  isAndroid: deviceDetect.isAndroid(),
  isMobile: deviceDetect.isMobile()
};

// 使用範例
if (environment.isWebview && environment.isIOS) {
  // 在 iOS Webview 中的處理邏輯
} else if (environment.isWebview && environment.isAndroid) {
  // 在 Android Webview 中的處理邏輯
} else {
  // 在普通瀏覽器中的處理邏輯
}

4. 注意事項

  1. UserAgent 的限制

    • UserAgent 可能被修改或偽裝
    • 不同裝置和系統版本可能有差異
    • 建議結合多個判斷方式確保準確性
  2. 版本特性判斷

    javascript
    // 獲取 iOS 版本
    function getIOSVersion() {
      const v = (navigator.appVersion).match(/OS (\d+)_(\d+)_?(\d+)?/);
      return v ? [parseInt(v[1], 10), parseInt(v[2], 10), parseInt(v[3] || 0, 10)] : [];
    }
    
    // 獲取 Android 版本(更新版本)
    function getAndroidVersion() {
      const ua = navigator.userAgent.toLowerCase();
      const match = ua.match(/android\s?([0-9\.]*)/);
      return match ? parseFloat(match[1]) : 0;
    }
  3. 備用

    javascript
    // 安全的特性檢測
    function safelyDetectDevice() {
      try {
        const ua = navigator.userAgent;
        return {
          isWebview: isInWebview(),
          platform: getDeviceType(),
          isIOS: ua.match(/(ios|iphone|ipad|ipod|macintosh)/ig) !== null,
          isAndroid: ua.match(/(android|adr)/ig) !== null,
          isMobile: true,
          version: ua.match(/(ios|iphone|ipad|ipod|macintosh)/ig) 
            ? getIOSVersion() 
            : getAndroidVersion()
        };
      } catch (error) {
        console.warn('裝置檢測失敗:', error);
        // 返回預設值
        return {
          isWebview: false,
          platform: 'unknown',
          isIOS: false,
          isAndroid: false,
          isMobile: false,
          version: null
        };
      }
    }

使用這些判斷方法時,建議根據專案需求選擇合適的組合,並做好錯誤處理和備用方案。同時,要注意不同系統版本可能存在的差異,定期更新和維護判斷邏輯。

前端傳值到 App

基礎傳值方式

javascript
// 整合裝置判斷
const appBridge = {
  // 裝置判斷
  device: {
    isIOS: navigator.userAgent.match(/(ios|iphone|ipad|ipod|macintosh)/ig),
    isAndroid: navigator.userAgent.match(/(android|adr)/ig),
    isWebview: () => {
      const userAgent = navigator.userAgent.toLowerCase();
      const rules = [
        'webview',
        'wv',
        '(iphone|ipod|ipad|ios|macintosh)(?!.*safari/)',
        'electron'
      ];
      return rules.some(rule => userAgent.includes(rule));
    }
  },

  // 傳值給 App
  callApp(functionName, data = {}) {
    try {
      if (!this.device.isWebview()) {
        console.warn('非 Webview 環境');
        return false;
      }

      if (this.device.isIOS) {
        window.webkit.messageHandlers[functionName].postMessage(data); // 傳值給 iOS 方式
        return true;
      } else if (this.device.isAndroid) {
        if (typeof window.Android[functionName] === 'function') {
          window.Android[functionName](JSON.stringify(data)); // 傳值給 Andriod 方式
          return true;
        } else {
          console.warn(`Android 未實現 ${functionName} 方法`);
          return false;
        }
      }
      return false;
    } catch (error) {
      console.error('調用 App 方法失敗:', error);
      return false;
    }
  }
};

// 使用範例
const result = appBridge.callApp('appMethod', {
  type: 'share',
  data: {
    title: '分享標題',
    content: '分享內容'
  }
});

常用功能封裝

javascript
// 常用功能封裝
const appFeatures = {
  // 分享功能
  share(shareData) {
    return appBridge.callApp('share', shareData);
  },

  // 獲取裝置資訊
  getDeviceInfo() {
    return appBridge.callApp('getDeviceInfo');
  },

  // 返回按鈕處理
  goBack() {
    return appBridge.callApp('goBack');
  },

  // 開啟相機
  openCamera(options = {}) {
    return appBridge.callApp('openCamera', options);
  },

  // 開啟相簿
  openGallery(options = {}) {
    return appBridge.callApp('openGallery', options);
  },

  // 儲存圖片
  saveImage(imageData) {
    return appBridge.callApp('saveImage', { image: imageData });
  }
};

// 使用範例
// 1. 分享
appFeatures.share({
  title: '分享標題',
  content: '分享內容',
  url: 'https://example.com',
  image: 'https://example.com/image.jpg'
});

// 2. 開啟相機
appFeatures.openCamera({
  maxWidth: 1024,
  maxHeight: 1024,
  quality: 0.8
});

// 3. 返回上一頁
appFeatures.goBack();

錯誤處理與備用方案

javascript
const enhancedAppBridge = {
  ...appBridge,
  
  // 新增一個增強版的 callApp 方法,多了錯誤處理和備用功能
  callApp(functionName, data = {}, fallback = null) {
    try {
      // 嘗試使用原本的 appBridge.callApp 方法
      const result = appBridge.callApp(functionName, data);
      
      // 如果呼叫失敗(result 為 false)且有提供備用方案
      if (!result && typeof fallback === 'function') {
        console.log(`執行備用方案: ${functionName}`);
        return fallback(data);  // 執行備用方案
      }
      return result;
    } catch (error) {
      // 如果執行過程中發生錯誤
      console.error(`${functionName} 執行失敗:`, error);
      if (typeof fallback === 'function') {
        return fallback(data);  // 執行備用方案
      }
      return false;
    }
  }
};

// 使用範例
enhancedAppBridge.callApp(
  'share',
  {
    title: '分享標題',
    content: '分享內容'
  },
  // 備用方案:當 App 方法呼叫失敗時的備用方案
  (data) => {
    // 嘗試使用網頁原生的分享功能
    if (navigator.share) {
      return navigator.share({
        title: data.title,
        text: data.content,
        url: window.location.href
      });
    }
    // 如果連網頁原生分享都不支援,就顯示提示訊息
    alert('請手動複製連結分享');
    return false;
  }
);

前端接收 App 的值

基礎接收方式

javascript
// 定義全域的接收函數
window.appCallback = {
  // 一般回調函數
  handleAppMessage(data) {
    try {
      const parsedData = typeof data === 'string' ? JSON.parse(data) : data;
      console.log('收到 App 傳來的資料:', parsedData);
      
      // 根據不同類型處理資料
      switch (parsedData.type) {
        case 'deviceInfo':
          // 處理裝置資訊
          handleDeviceInfo(parsedData.data);
          break;
        case 'location':
          // 處理位置資訊
          handleLocation(parsedData.data);
          break;
        default:
          console.log('未知的資料類型:', parsedData.type);
      }
    } catch (error) {
      console.error('處理 App 資料時發生錯誤:', error);
    }
  },

  // 處理相機/相簿回傳的圖片
  handleImageCallback(imageData) {
    try {
      const image = typeof imageData === 'string' ? JSON.parse(imageData) : imageData;
      // 處理圖片資料
      console.log('收到圖片資料:', image);
    } catch (error) {
      console.error('處理圖片資料時發生錯誤:', error);
    }
  },

  // 處理掃描 QR Code 結果
  handleQRCodeResult(result) {
    try {
      const qrData = typeof result === 'string' ? JSON.parse(result) : result;
      // 處理 QR Code 資料
      console.log('掃描結果:', qrData);
    } catch (error) {
      console.error('處理 QR Code 資料時發生錯誤:', error);
    }
  }
};

// 處理特定類型資料的函數
function handleDeviceInfo(deviceInfo) {
  console.log('裝置資訊:', deviceInfo);
  // 例如:更新 UI 顯示裝置資訊
}

function handleLocation(location) {
  console.log('位置資訊:', location);
  // 例如:在地圖上顯示位置
}

Promise 封裝

javascript
// 使用 Promise 封裝 App 回調
const appPromise = {
  // 等待 App 回應的 Promise 包裝函數
  waitForAppResponse(callbackName, timeout = 5000) {
    return new Promise((resolve, reject) => {
      // 設定超時處理
      const timeoutId = setTimeout(() => {
        reject(new Error('等待 App 回應超時'));
      }, timeout);

      // 設定回調函數
      window.appCallback[callbackName] = (data) => {
        clearTimeout(timeoutId);
        resolve(data);
      };
    });
  },

  // 使用範例:獲取裝置資訊
  async getDeviceInfo() {
    try {
      // 呼叫 App 方法
      appBridge.callApp('getDeviceInfo');
      // 等待 App 回應
      const deviceInfo = await this.waitForAppResponse('handleDeviceInfo');
      return deviceInfo;
    } catch (error) {
      console.error('獲取裝置資訊失敗:', error);
      return null;
    }
  },

  // 使用範例:掃描 QR Code
  async scanQRCode() {
    try {
      appBridge.callApp('scanQRCode');
      const result = await this.waitForAppResponse('handleQRCodeResult');
      return result;
    } catch (error) {
      console.error('掃描 QR Code 失敗:', error);
      return null;
    }
  }
};

// 使用範例
async function initDeviceInfo() {
  const deviceInfo = await appPromise.getDeviceInfo();
  if (deviceInfo) {
    console.log('成功獲取裝置資訊:', deviceInfo);
  }
}

async function handleScan() {
  const qrResult = await appPromise.scanQRCode();
  if (qrResult) {
    console.log('掃描結果:', qrResult);
  }
}

實際使用範例

javascript
// 1. 一般回調使用方式
window.appCallback.handleAppMessage({
  type: 'deviceInfo',
  data: {
    platform: 'iOS',
    version: '14.0',
    deviceId: 'XXXXX'
  }
});

// 2. Promise 使用方式
async function getUserLocation() {
  try {
    appBridge.callApp('getLocation');
    const location = await appPromise.waitForAppResponse('handleLocation');
    console.log('使用者位置:', location);
    return location;
  } catch (error) {
    console.error('獲取位置失敗:', error);
    return null;
  }
}

// 3. 圖片處理範例
window.appCallback.handleImageCallback({
  base64: 'data:image/jpeg;base64,/9j/4AAQSkZJRg...',
  width: 1024,
  height: 768,
  size: 1024000
});

開發注意事項

1. 兼容性處理

  • iOS 和 Android 的差異

    • iOS 使用 window.webkit.messageHandlers 傳值
    • Android 使用 window.Android 傳值
    • 需要統一封裝處理不同平台的呼叫方式
  • 系統版本差異

    • 不同 iOS 版本的 WebKit 功能支援度不同
    • Android WebView 版本可能因設備而異
    • 建議設定最低支援版本,並提供 fallback 方案
  • 瀏覽器相容性

    • 處理一般瀏覽器訪問的情況

2. 安全性考慮

  • 來源驗證

  • 資料加密

    • 敏感資料傳輸時使用加密
    • 避免明文傳輸用戶資訊
    • 使用 HTTPS 協議
  • 注入攻擊防範

    • 過濾 JavaScript 注入
    • 驗證 App 傳來的資料
    • 使用 Content Security Policy (CSP)

3. 文件

  • API 文件

    • 詳細的方法說明
    • 參數類型定義
    • 回調格式說明
  • 版本控制

    • 清晰的版本號管理
    • 向下兼容性說明
    • 更新日誌維護

這些注意事項能幫助開發者:

  • 增強應用穩定性
  • 改善使用者體驗
  • 便於後期維護

結語

在現代混合式應用開發中,Webview 已經成為連接網頁和原生 App 的重要橋樑。

  1. 有效判斷執行環境

    • 識別裝置類型
    • 處理不同平台差異
    • 提供適當的備用方案
  2. 建立穩固的互動機制

    • 統一的傳值介面
    • 錯誤處理
    • Promise 化的非同步操作

在實際開發中,建議:

  • 與 App 開發團隊保持良好溝通
  • 建立完整的 API 文件
  • 定期更新和維護程式碼