一個 Hybrid SDK 設計與實現

隨着移動浪潮的興起,各種 App 層出不窮,極速發展的業務拓展提升了團隊對開發效率的要求,這個時候純粹使用 Native 開發技術成本難免會更高一點。而 H5 的低成本、高效率、跨平臺等特性馬上被利用起來了,形成一種新的開發模式:Hybrid App

作爲一種混合開發的模式,Hybrid App 底層依賴於 Native 提供的容器(Webview),上層使用各種前端技術完成業務開發(現在三足鼎立的 Vue、React、Angular),底層透明化、上層多樣化。這種場景非常有利於前端介入,非常適合業務的快速迭代。於是 Hybrid 火了。

大道理誰都懂,但是按照我知道的情況,還是有非常多的人和公司在 Hybrid 這一塊並沒有做的很好,所以我將我的經驗做一個總結,希望可以幫助廣大開發者的技術選型有所幫助。

◆ Hybrid 的一個現狀

可能早期都是 PC 端的網頁開發,隨着移動互聯網的發展,iOS、Android 智能手機的普及,非常多的業務和場景都從 PC 端轉移到移動端。開始有前端開發者爲移動端開發網頁。這樣子早期資源打包到 Native App 中會造成應用包體積的增大。越來越多的業務開始用 H5 嘗試,這樣子難免會需要一個需要訪問 Native 功能的地方,這樣子可能早期就是懂點前端技術的 Native 開發者自己封裝或者暴露 Native 能力給 JS 端,等業務較多的時候者樣子很明顯不現實,就需要專門的 Hybrid 團隊做這個事情;量大了,就需要規矩,就需要規範。

總結:

  1. Hybrid 開發效率高、跨平臺、低成本

  2. Hybrid 從業務上講,沒有版本問題,有 Bug 可以及時修復

Hybrid 在大量應用的時候就需要一定的規範,那麼本文將討論一個 Hybrid 的設計知識。

◆ Native 與前端分工

在做 Hybird 架構設計之前我們需要分清 Native 與前端的界限。首先 Native 提供的是宿主環境,要合理利用 Native 提供的能力,要實現通用的 Hybrid 架構,站在大前端的視覺,我覺得需要考慮以下核心設計問題。

◆ 交互設計

Hybrid 架構設計的第一要考慮的問題就是如何設計前端與 Native 的交互,如果這塊設計不好會對後續的開發、前端框架的維護造成深遠影響。並且這種影響是不可逆、積重難返。所以前期需要前端與 Native 好好配合、提供通用的接口。比如

  1. Native UI 組件、Header 組件、消息類組件

  2. 通訊錄、系統、設備信息讀取接口

  3. H5 與 Native 的互相跳轉。比如 H5 如何跳轉到一個 Native 頁面,H5 如何新開 Webview 並做動畫跳轉到另一個 H5 頁面

◆ 賬號信息設計

賬號系統是重要且無法避免的,Native 需要設計良好安全的身份驗證機制,保證這塊對業務開發者足夠透明,打通賬戶體系

◆ Hybrid 開發調試

功能設計、編碼完並不是真正結束,Native 與前端需要商量出一套可開發調試的模型,不然很多業務開發的工作難以繼續。

iOS 調試技巧

Android 調試技巧:

◆ Hybrid 交互設計

Hybrid 交互無非是 Native 調用 H5 頁面 JS 方法,或者 H5 頁面通過 JS 調 Native 提供的接口。2 者通信的橋樑是 Webview。
業界主流的通信方法:1. 橋接對象(時機問題,不太主張這種方式);2. 自定義 Url scheme

App 自身定義了 url scheme,將自定義的 url 註冊到調度中心,例如
weixin:// 可以打開微信。

關於 Url scheme 如果不太清楚可以看看 這篇文章

◆ JS to Native

Native 在每個版本都會提供一些 Api,前端會有一個對應的框架團隊對其封裝,釋放業務接口。舉例

SDGHybrid.http.get()  // 向業務服務器拿數據
SDGHybrid.http.post() // 向業務服務器提交數據
SDGHybrid.http.sign() // 計算簽名
SDGHybrid.http.getUA()  // 獲取UserAgent
SDGHybridReady(function(arg){
  SDGHybrid.http.post({
    url: arg.baseurl + '/feedback',
    params:{
      title: '點菜很慢',
      content: '服務差'
    },
    success: (data) => {
      renderUI(data);
    },
    fail: (err) => {
      console.log(err);
    }
  })
})

前端框架定義了一個全局變量 SDGHybrid 作爲 Native 與前端交互的橋樑,前端可以通過這個對象獲得訪問 Native 的能力

◆ Api 交互

調用 Native Api 接口的方式和使用傳統的 Ajax 調用服務器,或者 Native 的網絡請求提供的接口相似

所以我們需要封裝的就是模擬創建一個類似 Ajax 模型的 Native 請求。

◆ 格式約定

交互的第一步是設計數據格式。這裏分爲請求數據格式與響應數據格式,參考 Ajax 模型:

$.ajax({
  type: "GET",
  url: "test.json",
  data: {username:$("#username").val(), content:$("#content").val()},
  dataType: "json",
  success: function(data){
    renderUI(data);           
  }
});
$.ajax(options) => XMLHTTPRequest
type(默認值:GET),HTTP請求方法(GET|POST|DELETE|...)
url(默認值:當前url),請求的url地址
data(默認值:'') 請求中的數據如果是字符串則不變,如果爲Object,則需要轉換爲String,含有中文則會encodeURI

所以 Hybrid 中的請求模型爲:

requestHybrid({
  // H5 請求由 Native 完成
  tagname: 'NativeRequest',
  // 請求參數
  param: requestObject,
  // 結果的回調
  callback: function (data) {
    renderUI(data);
  }
});

這個方法會形成一個 URL,比如:

SDGHybrid://NativeRequest?t=1545840397616&callback=Hybrid_1545840397616&param=%7B%22url%22%3A%22https%3A%2F%2Fwww.datacubr.com%2FApi%2FSearchInfo%2FgetLawsInfo%22%2C%22params%22%3A%7B%22key%22%3A%22%22%2C%22page%22%3A1%2C%22encryption%22%3A1%7D%2C%22Hybrid_Request_Method%22%3A0%7D

Native 的 webview 環境可以監控內部任何的資源請求,判斷如果是 SDGHybrid 則分發事件,處理結束可能會攜帶參數,參數需要先 urldecode 然後將結果數據通過 Webview 獲取 window 對象中的 callback(Hybrid_時間戳)

數據返回的格式和普通的接口返回格式類似

{
  errno: 1,
  message: 'App版本過低,請升級App版本',
  data: {}
}

這裏注意:真實數據在 data 節點中。如果 errno 不爲 0,則需要提示 message。

簡易版本代碼實現。

 tmpFn = params.callback;
        params.callback = t;
        window.SDGHybrid[t] = function (data) {
            tmpFn(data);
            delete window.SDGHybrid[t];
        }
    }
    loadURL(_getHybridUrl(params));
};
//獲取版本信息,約定APP的navigator.userAgent版本包含版本信息:scheme/xx.xx.xx
var getHybridInfo = function () {
    var platform_version = {};
    var na = navigator.userAgent;
    var info = na.match(/scheme\/\d\.\d\.\d/);
    if (info && info[0]) {
      info = info[0].split('/');
      if (info && info.length == 2) {
        platform_version.platform = info[0];
        platform_version.version = info[1];
      }
    }
    return platform_version;
};

Native 對於 H5 來說有個 Webview 容器,框架 && 底層不太關心 H5 的業務實現,所以真實業務中 Native 調用 H5 場景較少。

上面的網絡訪問 Native 代碼(iOS 爲例)

typedef NS_ENUM(NSInteger){
    Hybrid_Request_Method_Post = 0,
    Hybrid_Request_Method_Get = 1} Hybrid_Request_Method;@interface RequestModel : NSObject@property (nonatomic, strong) NSString *url;@property (nonatomic, assign) Hybrid_Request_Method Hybrid_Request_Method;@property (nonatomic, strong) NSDictionary *params;@end@interface HybridRequest : NSObject+ (void)requestWithNative:(RequestModel *)requestModel hybridRequestSuccess:(void (^)(id responseObject))success hybridRequestfail:(void (^)(void))fail;
+ (void)requestWithNative:(RequestModel *)requestModel hybridRequestSuccess:(void (^)(id responseObject))success hybridRequestfail:(void (^)(void))fail{    //處理請求不全的情況
    NSAssert(requestModel || success || fail, @"Something goes wrong");    
    NSString *url = requestModel.url;    NSDictionary *params = requestModel.params;    if (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Get) {
        [AFNetPackage getJSONWithUrl:url parameters:params success:^(id responseObject) {
            success(responseObject);
        } fail:^{
            fail();
        }];
    }    else if (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Post) {
        [AFNetPackage postJSONWithUrl:url parameters:params success:^(id responseObject) {
            success(responseObject);
        } fail:^{
            fail();
        }];
    }
}

◆ 常用交互 Api

良好的交互設計是第一步,在真實業務開發中有一些 Api 一定會由應用場景。

◆ 跳轉

是 Hybrid 必用的 Api 之一,對前端來說有以下情況:

//H5跳Native頁面
//=>SDGHybrid://forward?t=1446297487682¶m=%7B%22topage%22%3A%22home%22%2C%22type%22%3A%22h2n%22%2C%22data2%22%3A2%7D
requestHybrid({
   tagname: 'forward',
   param: {
     // 要去到的頁面
     topage: 'home',
     // 跳轉方式,H5跳Native
     type: 'native',
     // 其它參數
     data2: 2
   }
});

H5 頁面要去 Native 某個頁面

//=>SDGHybrid://forward?t=1446297653344¶m=%7B%22topage%22%253A%22Goods%252Fdetail%20%20%22%252C%22type%22%253A%22h2n%22%252C%22id%22%253A20151031%7D
requestHybrid({
  tagname: 'forward',
  param: {
    // 要去到的頁面
    topage: 'Goods/detail',
    // 跳轉方式,H5跳Native
    type: 'native',
    // 其它參數
    id: 20151031
  }
});

H5 新開 Webview 的方式去跳轉 H5

requestHybrid({
  tagname: 'forward',
  param: {
    // 要去到的頁面,首先找到goods頻道,然後定位到detail模塊
    topage: 'goods/detail  ',
    //跳轉方式,H5新開Webview跳轉,最後裝載H5頁面
    type: 'webview',
    //其它參數
    id: 20151031
  }
});

back  與 forward 一致,可能會有 animatetype 參數決定頁面切換的時候的動畫效果。真實使用的時候可能會全局封裝方法去忽略 tagname 細節。

◆ Header 組件的設計

Native 每次改動都比較 “慢”,所以類似 Header 就很需要。

  1. 主流容器都是這麼做的,比如微信、手機百度、攜程

  2. 沒有 Header 一旦出現網絡錯誤或者白屏,App 將陷入假死狀態

PS:Native 打開 H5,如果 300ms 沒有響應則需要 loading 組件,避免白屏
因爲 H5 App 本身就有 Header 組件,站在前端框架層來說,需要確保業務代碼是一致的,所有的差異需要在框架層做到透明化,簡單來說 Header 的設計需要遵循:

一般來說 Header 組件需要完成以下功能:

  1. Header 左側與右側可配置,顯示爲文字或者圖標(這裏要求 Header 實現主流圖標,並且也可由業務控制圖標),並需要控制其點擊回調

  2. Header 的 title 可設置爲單標題或者主標題、子標題類型,並且可配置 lefticon 與 righticon(icon 居中)

  3. 滿足一些特殊配置,比如標籤類 Header

所以,站在前端業務方來說,Header 的使用方式爲(其中 tagname 是不允許重複的):

//Native以及前端框架會對特殊tagname的標識做默認回調,如果未註冊callback,或者點擊回調callback無返回則執行默認方法
 // back前端默認執行History.back,如果不可後退則回到指定URL,Native如果檢測到不可後退則返回Naive大首頁
 // home前端默認返回指定URL,Native默認返回大首頁
  this.header.set({
      left: [
        {
          //如果出現value字段,則默認不使用icon
          tagname: 'back',
          value: '回退',
          //如果設置了lefticon或者righticon,則顯示icon
          //native會提供常用圖標icon映射,如果找不到,便會去當前業務頻道專用目錄獲取圖標
          lefticon: 'back',
          callback: function () { }
        }
     ],
     right: [
      {
        //默認icon爲tagname,這裏爲icon
        tagname: 'search',
        callback: function () { }
      },
      //自定義圖標
      {
        tagname: 'me',
        //會去hotel頻道存儲靜態header圖標資源目錄搜尋該圖標,沒有便使用默認圖標
        icon: 'hotel/me.png',
        callback: function () { }
      }
    ],
    title: 'title',
        //顯示主標題,子標題的場景
    title: ['title', 'subtitle'], 
    //定製化title
    title: {
      value: 'title',
      //標題右邊圖標
      righticon: 'down', //也可以設置lefticon
      //標題類型,默認爲空,設置的話需要特殊處理
      //type: 'tabs',
      //點擊標題時的回調,默認爲空
      callback: function () { }
    }
});

因爲 Header 左邊一般來說只有一個按鈕,所以其對象可以使用這種形式:

this.header.set({
  back: function () { },
    title: ''
});
//語法糖=>
this.header.set({
    left: [{
        tagname: 'back',
        callback: function(){}
    }],
  title: '',
});

爲完成 Native 端的實現,這裏會新增兩個接口,向 Native 註冊事件,以及註銷事件:

var registerHybridCallback = function (ns, name, callback) {
  if(!window.Hybrid[ns]) window.Hybrid[ns] = {};
  window.Hybrid[ns][name] = callback;
};
var unRegisterHybridCallback = function (ns) {
  if(!window.Hybrid[ns]) return;
  delete window.Hybrid[ns];
};

Native Header 組件實現:

define([], function () {
    'use strict';
    return _.inherit({
        propertys: function () {
            this.left = [];
            this.right = [];
            this.title = {};
            this.view = null;
            this.hybridEventFlag = 'Header_Event';
        },
        //全部更新
        set: function (opts) {
            if (!opts) return;
            var left = [];
            var right = [];
            var title = {};
            var tmp = {};
            //語法糖適配
            if (opts.back) {
                tmp = { tagname: 'back' };
                if (typeof opts.back == 'string') tmp.value = opts.back;
                else if (typeof opts.back == 'function') tmp.callback = opts.back;
                else if (typeof opts.back == 'object') _.extend(tmp, opts.back);
                left.push(tmp);
            } else {
                if (opts.left) left = opts.left;
            }
            //右邊按鈕必須保持數據一致性
            if (typeof opts.right == 'object' && opts.right.length) right = opts.right
            if (typeof opts.title == 'string') {
                title.title = opts.title;
            } else if (_.isArray(opts.title) && opts.title.length > 1) {
                title.title = opts.title[0];
                title.subtitle = opts.title[1];
            } else if (typeof opts.title == 'object') {
                _.extend(title, opts.title);
            }
            this.left = left;
            this.right = right;
            this.title = title;
            this.view = opts.view;
            this.registerEvents();
            _.requestHybrid({
                tagname: 'updateheader',
                param: {
                    left: this.left,
                    right: this.right,
                    title: this.title
                }
            });
        },
        //註冊事件,將事件存於本地
        registerEvents: function () {
            _.unRegisterHybridCallback(this.hybridEventFlag);
            this._addEvent(this.left);
            this._addEvent(this.right);
            this._addEvent(this.title);
        },
        _addEvent: function (data) {
            if (!_.isArray(data)) data = [data];
            var i, len, tmp, fn, tagname;
            var t = 'header_' + (new Date().getTime());
            for (i = 0, len = data.length; i < len; i++) {
                tmp = data[i];
                tagname = tmp.tagname || '';
                if (tmp.callback) {
                    fn = $.proxy(tmp.callback, this.view);
                    tmp.callback = t;
                    _.registerHeaderCallback(this.hybridEventFlag, t + '_' + tagname, fn);
                }
            }
        },
        //顯示header
        show: function () {
            _.requestHybrid({
                tagname: 'showheader'
            });
        },
        //隱藏header
        hide: function () {
            _.requestHybrid({
                tagname: 'hideheader',
                param: {
                    animate: true
                }
            });
        },
        //只更新title,不重置事件,不對header其它地方造成變化,僅僅最簡單的header能如此操作
        update: function (title) {
            _.requestHybrid({
                tagname: 'updateheadertitle',
                param: {
                    title: 'aaaaa'
                }
            });
        },
        initialize: function () {
            this.propertys();
        }
    });
});

◆ 請求類

雖然 get 類請求可以用 jsonp 方式繞過跨域問題,但是 post 請求是一個攔路虎。爲了安全性問題服務器會設置 cors 僅僅針對幾個域名,Hybrid 內嵌靜態資源可能是通過本地 file 的方式讀取,所以 cors 就行不通了。另外一個問題是防止爬蟲獲取數據,由於 Native 針對網絡做了安全性設置(鑑權、防抓包等),所以 H5 的網絡請求由 Native 完成。可能有些人說 H5 的網絡請求讓 Native 走就安全了嗎?我可以繼續爬取你的 Dom 節點啊。這個是針對反爬蟲的手段一。想知道更多的反爬蟲策略可以看看我這篇文章 Web 反爬蟲方案

這個使用場景和 Header 組件一致,前端框架層必須做到對業務透明化,業務事實上不必關心這個網絡請求到底是由 Native 還是瀏覽器發出。

HybridGet = function (url, param, callback) {
};
HybridPost = function (url, param, callback) {
};

真實的業務場景,會將之封裝到數據請求模塊,在底層做適配,在 H5 站點下使用 ajax 請求,在 Native 內嵌時使用代理發出,與 Native 的約定爲

requestHybrid({
  tagname: 'NativeRequest',
  param: {
    url: arg.Api + "SearchInfo/getLawsInfo",
    params: requestparams,
    Hybrid_Request_Method: 0,
    encryption: 1
  },
  callback: function (data) {
    renderUI(data);
  }
});

◆ 常用 NativeUI 組件

一般情況 Native 通常會提供常用的 UI,比如 加載層 loading、消息框 toast

var HybridUI = {};
HybridUI.showLoading();
//=>
requestHybrid({
    tagname: 'showLoading'
});
HybridUI.showToast({
    title: '111',
    //幾秒後自動關閉提示框,-1需要點擊纔會關閉
    hidesec: 3,
    //彈出層關閉時的回調
    callback: function () { }
});
//=>
requestHybrid({
    tagname: 'showToast',
    param: {
        title: '111',
        hidesec: 3,
        callback: function () { }
    }
});

Native UI 與前端 UI 不容易打通,所以在真實業務開發過程中,一般只會使用幾個關鍵的 Native UI。

◆ 賬號系統的設計

Webview 中跑的網頁,賬號登錄與否由是否攜帶密鑰 cookie 決定(不能保證密鑰的有效性)。因爲 Native 不關注業務實現,所以每次載入都有可能是登錄成功跳轉回來的結果,所以每次載入都需要關注密鑰 cookie 變化,以做到登錄態數據的一致性。

/*
    無論成功與否皆會關閉登錄框
    參數包括:
    success 登錄成功的回調
     error 登錄失敗的回調
    url 如果沒有設置success,或者success執行後沒有返回true,則默認跳往此url
*/
HybridUI.Login = function (opts) {
    //...
};
//=>
requestHybrid({
    tagname: 'login',
    param: {
       success: function () { },
       error: function () { },
       url: '...'
    }
});
//與登錄接口一致,參數一致
HybridUI.logout = function () {
    //...
};

在設計 Hybrid 層的時候,接口要做到對於處於 Hybrid 環境中的代碼樂意通過接口獲取 Native 端存儲的用戶賬號信息;對於處於傳統的網頁環境,可以通過接口獲取線上的賬號信息,然後將非敏感的信息存儲到 LocalStorage 中,然後每次頁面加載從 LocalStorage 讀取數據到內存中(比如 Vue.js 框架中的 Vuex,React.js 中的 Redux)

◆ Hybrid 資源管理

Hybrid 的資源需要 增量更新 需要拆分方便,所以一個 Hybrid 資源結構類似於下面的樣子

假設有 2 個業務線:商城、購物車

WebApp
│- Mall
│- Cart
│  index.html //業務入口html資源,如果不是單頁應用會有多個入口
│  │  main.js //業務所有js資源打包
│  │
│  └─static //靜態樣式資源
│      ├─css 
│      ├─hybrid //存儲業務定製化類Native Header圖標
│      └─images
├─libs
│      libs.js //框架所有js資源打包
│
└─static
   ├─css
   └─images

◆ 增量更新

每次業務開發完畢後都需要在打包分發平臺進行部署上線,之後會生成一個版本號。

FQwXa7

當 Native App 啓動的時候會從服務端請求一個接口,接口的返回一個 json 串,內容是 App 所包含的各個 H5 業務線的版本號和 md5 信息。

拿到 json 後和 App 本地保存的版本信息作比較,發現變動了則去請求相應的接口,接口返回 md5 對應的文件。Native 拿到後完成解壓替換。

全部替換完畢後將這次接口請求到的資源版本號信息保存替換到 Native 本地。

因爲是每個資源有版本號,所以如果線上的某個版本存在問題,那麼可以根據相應的穩定的版本號回滾到穩定的版本。

◆ 一些零散的解決方案

  1. 靜態直出

“直出” 這個概念對前端同學來說,並不陌生。爲了優化首屏體驗,大部分主流的頁面都會在服務器端拉取首屏數據後通過 NodeJs 進行渲染,然後生成一個包含了首屏數據的 Html 文件,這樣子展示首屏的時候,就可以解決內容轉菊花的問題了。
當然這種頁面 “直出” 的方式也會帶來一個問題,服務器需要拉取首屏數據,意味着服務端處理耗時增加。
不過因爲現在 Html 都會發布到 CDN 上,WebView 直接從 CDN 上面獲取,這塊耗時沒有對用戶造成影響。
手 Q 裏面有一套自動化的構建系統 Vnues,當產品經理修改數據發佈後,可以一鍵啓動構建任務,Vnues 系統就會自動同步最新的代碼和數據,然後生成新的含首屏 Html,併發布到 CDN 上面去。

我們可以做一個類似的事情,自動同步最新的代碼和數據,然後生成新的含首屏 Html,併發布到 CDN 上面去

  1. 離線預推

頁面發佈到 CDN 上面去後,那麼 WebView 需要發起網絡請求去拉取。當用戶在弱網絡或者網速比較差的環境下,這個加載時間會很長。於是我們通過離線預推的方式,把頁面的資源提前拉取到本地,當用戶加載資源的時候,相當於從本地加載,即使沒有網絡,也能展示首屏頁面。這個也就是大家熟悉的離線包。
手 Q 使用 7Z 生成離線包, 同時離線包服務器將新的離線包跟業務對應的歷史離線包進行 BsDiff 做二進制差分,生成增量包,進一步降低下載離線包時的帶寬成本,下載所消耗的流量從一個完整的離線包(253KB)降低爲一個增量包(3KB)。

  1. 攔截加載

事實上,在高度定製的 wap 頁面場景下,我們對於 webview 中可能出現的頁面類型會進行嚴格控制。可以通過內容的控制,避免 wap 頁中出現外部頁面的跳轉,也可以通過 webview 的對應代理方法,禁掉我們不希望出現的跳轉類型,或者同時使用,雙重保護來確保當前 webview 容器中只會出現我們定製過的內容。既然 wap 頁的類型是有限的,自然想到,同類型頁面大都由前端採用模板生成,頁面所使用的 html、css、js 的資源很可能是同一份,或者是有限的幾份,把它們直接隨客戶端打包在本地也就變得可行。加載對應的 url 時,直接 load 本地的資源。
對於 webview 中的網絡請求,其實也可以交由客戶端接管,比如在你所採用的 Hybrid 框架中,爲前端註冊一個發起網絡請求的接口。wap 頁中的所有網絡請求,都通過這個接口來發送。這樣客戶端可以做的事情就非常多了,舉個例子,NSURLProtocol 無法攔截 WKWebview 發起的網絡請求,採用 Hybrid 方式交由客戶端來發送,便可以實現對應的攔截。
基於上面的方案,我們的 wap 頁的完整展示流程是這樣:客戶端在 webview 中加載某個 url,判斷符合規則,load 本地的模板 html,該頁面的內部實現是通過客戶端提供的網絡請求接口,發起獲取具體頁面內容的網絡請求,獲得填充的數據從而完成展示。

NSURLProtocol 能夠讓你去重新定義蘋果的 URL 加載系統 (URL Loading System) 的行爲,URL Loading System 裏有許多類用於處理 URL 請求,比如 NSURL,NSURLRequest,NSURLConnection 和 NSURLSession 等。當 URL Loading System 使用 NSURLRequest 去獲取資源的時候,它會創建一個 NSURLProtocol 子類的實例,你不應該直接實例化一個 NSURLProtocol,NSURLProtocol 看起來像是一個協議,但其實這是一個類,而且必須使用該類的子類,並且需要被註冊。                                     

  1. WKWebView 網絡請求攔截
    方法一(Native 側):
    原生 WKWebView 在獨立於 app 進程之外的進程中執行網絡請求,請求數據不經過主進程,因此在 WKWebView 上直接使用 NSURLProtocol 是無法攔截請求的。

但是由於 mPaas 的離線包機制強依賴網絡攔截,所以基於此,mPaaS 利用了 WKWebview 的隱藏 api,去註冊攔截網絡請求去滿足離線包的業務場景需求,參考代碼如下:

[WKBrowsingContextController registerSchemeForCustomProtocol:@"https"]

但是因爲出於性能的原因,WKWebView 的網絡請求在給主進程傳遞數據的時候會把請求的 body 去掉,導致攔截後請求的 body 參數丟失。

在離線包場景,由於頁面的資源不需要 body 數據,所以離線包可以正常使用不受影響。但是在 H5 頁面內的其他 post 請求會丟失 data 參數。

爲了解決 post 參數丟失的問題,mPaas 通過在 js 注入代碼,hook 了 js 上下文裏的 XMLHTTPRequest 對象解決。

通過在 JS 層把方法內容組裝好,然後通過 WKWebView 的 messageHandler 機制把內容傳到主進程,把對應 HTTPBody 然後存起來,隨後通知 JS 端繼續這個請求,網絡請求到主進程後,在將 post 請求對應的 HttpBody 添加上,這樣就完成了一次 post 請求的處理。整體流程可以參考如下:

通過上面的機制,既滿足了離線包的資源攔截訴求,也解決了 post 請求 body 丟失的問題。

但是在一些場景還是存在一些問題,需要開發者進行適配。

方法二(JS 側):
通過 AJAX 請求的 hook 方式,將網絡請求的信息代理到客戶端本地。能拿到 WKWebView 裏面的 post 請求信息,剩下的就不是問題啦。
AJAX hook 的實現可以看這個 Repo.

來源:https://segmentfault.com/a/1190000040250804

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Glahum7po-8vDe2o4mFmrg