Mike

4 Dec, 2019

WebView 與 JavaScript 的互相交互

一些開發 javaScript 與 webview 的經驗分享。

我們在開發APP的時候很常第一時間想到Android 或是 ios 透過原生的方式進行開發,只是原生開發效能雖然好,但是在更新或是上版往往有許多問題需要解決,所以很常會透過webview的方式嵌入web來呈現頁面,也就是透過網頁的方式來開發APP的頁面。

那再來問題就是要怎麼 call 原生Android 或 ios 的方法,其實不管是Android 或 ios都是把他們寫好的方法給丟到 webview 裡 global 的物件內,然後再去執行,畢竟 webview 就是瀏覽器,舉例來說如果Android有一個 getUserToken的方法,我們要 call 它只要像這樣

window.android['getUserToken']();
// or
window.android.getUserToken();

那在 ios 上面的話是需要另外掛載 plugins,有很多的做法,在這邊的話我同事是用 WKWebViewJavascriptBridge 這套 plugins 來處理,所以我的 code 就會以這個 plugins 為範例。

WKWebViewJavascriptBridge在使用要先在code裡面加入這段

function iosCallback(callback) {
  if (window.WebViewJavascriptBridge) {
    return callback(WebViewJavascriptBridge);
  }
  if (window.WVJBCallbacks) {
    return window.WVJBCallbacks.push(callback);
  }
  window.WVJBCallbacks = [callback];
  var WVJBIframe = document.createElement('iframe');
  WVJBIframe.style.display = 'none';
  WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
  document.documentElement.appendChild(WVJBIframe);
  setTimeout(function () {
    document.documentElement.removeChild(WVJBIframe) 
  }, 0)
}

這是WKWebViewJavascriptBridge的預載函式,透過這個 function 裡面的方法就可以去跟 ios 的 webview去做互動,像這樣

const contents = JSON.stringify(content);
iosCallback(e => e.callHandler('getUserToken', contents, res=>{
   console.log(res);
}))

這邊要特別注意,調用 webview 方法的時候在ios上面是非同步的,所以如果我們要整合 android 跟 ios 的話,就會有一個是同步一個是非同步的狀況發生,那要怎麼樣處理呢?

我們可以透過把 android 跟 ios 的方法透過 Promise 給封裝起來,然後才好在我們的 web 裡面是調用,我們來看看怎麼封裝。

import device from 'current-device';
function iosCallback(callback) {
    if (window.WebViewJavascriptBridge) {
        return callback(WebViewJavascriptBridge);
    }
    if (window.WVJBCallbacks) {
        return window.WVJBCallbacks.push(callback);
    }
    window.WVJBCallbacks = [callback];
    var WVJBIframe = document.createElement('iframe');
    WVJBIframe.style.display = 'none';
    WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
    document.documentElement.appendChild(WVJBIframe);
    setTimeout(function () { document.documentElement.removeChild(WVJBIframe) }, 0)
}

export default function (event, content={}) {
    const contents = JSON.stringify(content)
    if(device.ios()) {
        return new Promise((resolve, reject) => {
            iosCallback(e =>  e.callHandler(event, contents, res=>{
                resolve(res);
            }))
        });
    } else if(device.android()){
        return new Promise((resolve, reject) => {
            let fn = window.android[event](contents);
            resolve(fn);
        });
    }else{
        const newPromise = new Promise((resolve, reject) => {
            switch (event) {
                case "getDomain":
                    resolve(process.env.DOMAIN);
                    break;
                case "getUserToken":
                    resolve(process.env.TOKEN);
                    break;
                default:
                    resolve(`===> default`);
                    break;
            }
        })
        return newPromise;
    }
}
  1. 首先在這邊我透過 current-device 來更精準判斷所使用的裝置。
  2. 參數的部分這邊透過 JSON.stringfy(content)來把要傳給 App 的參數轉成 string,因為 android 跟 ios 沒有 javaScript 的 Object,所以只好吃 string 然後再去解析。
  3. 然後再帶入 event,透過 resolve 把 App 回傳的參數給傳出去。
  4. 為了將回傳出去的資料都給 Promise 包起來,所以 android 的方法也都給包裝 Promise。
  5. 最後因為開發的時候會再 local 開發,會拿不到APP的內容,所以我多包了一層傳入測試的資料讓我可以本地開放使用。

這樣的話就可以再主程式裡面調用這個方法

import appWebviewFn from "./lib/appWebviewFn.js";
appWebviewFn("getUserToken");

因為它會傳是一個 Promise 我們可以這樣

import appWebviewFn from "./lib/appWebviewFn.js";

const getData = async () => {
  const domain = await appWebviewFn("getDomain");
  const token = await appWebviewFn("getUserToken");
  /*
     拿到 domain 跟 token 之後的處理需求
  */
}

getData();

搭配 async/await 使用,可以確保拿到你的資料,你的code 也不用一直 .then 的方式來寫 callBack,這樣會變得非常方便!