<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一個 Hybrid SDK 設計與實現(xiàn)

          共 18915字,需瀏覽 38分鐘

           ·

          2021-07-12 11:40



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

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

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


          Hybrid 的一個現(xiàn)狀


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

          總結:

          1. Hybrid 開發(fā)效率高、跨平臺、低成本

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

          Hybrid 在大量應用的時候就需要一定的規(guī)范,那么本文將討論一個 Hybrid 的設計知識。
          - Hybrid 、Native、前端各自的工作是什么
          - Hybrid 交互接口如何設計
          - Hybrid 的 Header 如何設計
          - Hybrid 的如何設計目錄結構以及增量機制如何實現(xiàn)
          - 資源緩存策略,白屏問題...


          Native 與前端分工


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

          交互設計

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

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

          2. 通訊錄、系統(tǒng)、設備信息讀取接口

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

          賬號信息設計

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

          Hybrid 開發(fā)調試

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

          iOS調試技巧 https://www.jianshu.com/p/f430caa81fa8

          Android 調試技巧:

          • App 中開啟 Webview 調試(WebView.setWebContentsDebuggingEnabled(true); )

          • chrome 瀏覽器輸入 chrome://inspect/#devices 訪問可以調試的 webview 列表

          • 需要翻墻的環(huán)境



          Hybrid 交互設計


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

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

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

          JS to Native

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

          SDGHybrid.http.get()  // 向業(yè)務服務器拿數(shù)據(jù)
          SDGHybrid.http.post() // 向業(yè)務服務器提交數(shù)據(jù)
          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 接口的方式和使用傳統(tǒng)的 Ajax 調用服務器,或者 Native 的網絡請求提供的接口相似

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

          格式約定

          交互的第一步是設計數(shù)據(jù)格式。這里分為請求數(shù)據(jù)格式與響應數(shù)據(jù)格式,參考 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(默認值:'') 請求中的數(shù)據(jù)如果是字符串則不變,如果為Object,則需要轉換為String,含有中文則會encodeURI

          所以 Hybrid 中的請求模型為:

          requestHybrid({
          // H5 請求由 Native 完成
          tagname: 'NativeRequest',
          // 請求參數(shù)
          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 環(huán)境可以監(jiān)控內部任何的資源請求,判斷如果是 SDGHybrid 則分發(fā)事件,處理結束可能會攜帶參數(shù),參數(shù)需要先 urldecode 然后將結果數(shù)據(jù)通過 Webview 獲取 window 對象中的 callback(Hybrid_時間戳)

          數(shù)據(jù)返回的格式和普通的接口返回格式類似

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

          這里注意:真實數(shù)據(jù)在 data 節(jié)點中。如果 errno 不為0,則需要提示 message。

          簡易版本代碼實現(xiàn)。

          //通用的 Hybrid call Native
          window.SDGbrHybrid = window.SDGbrHybrid || {};
          var loadURL = function (url) {
          var iframe = document.createElement('iframe');
          iframe.style.display = "none";
          iframe.style.width = '1px';
          iframe.style.height = '1px';
          iframe.src = url;
          document.body.appendChild(iframe);
          setTimeout(function () {
          iframe.remove();
          }, 100);
          };

          var _getHybridUrl = function (params) {
          var paramStr = '', url = 'SDGHybrid://';
          url += params.tagname + "?t=" + new Date().getTime();
          if (params.callback) {
          url += "&callback=" + params.callback;
          delete params.callback;
          }

          if (params.param) {
          paramStr = typeof params.param == "object" ? JSON.stringify(params.param) : params.param;
          url += "&param=" + encodeURIComponent(paramStr);
          }
          return url;
          };


          var requestHybrid = function (params) {
          //生成隨機函數(shù)
          var tt = (new Date().getTime());
          var t = "Hybrid_" + tt;
          var tmpFn;

          if (params.callback) {
          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 的業(yè)務實現(xiàn),所以真實業(yè)務中 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


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

          跳轉

          跳轉是 Hybrid 必用的 Api 之一,對前端來說有以下情況:
          - 頁面內跳轉,與 Hybrid 無關
          - H5 跳轉 Native 界面
          - H5 新開 Webview 跳轉 H5 頁面,一般動畫切換頁面
          如果使用動畫,按照業(yè)務來說分為前進、后退。forward & backword,規(guī)定如下,首先是 H5 跳 Native 某個頁面

           //H5跳Native頁面
          //=>SDGHybrid://forward?t=1446297487682&param=%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',
          // 其它參數(shù)
          data2: 2
          }
          });

          H5 頁面要去 Native 某個頁面

          //=>SDGHybrid://forward?t=1446297653344&param=%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',
          // 其它參數(shù)
          id: 20151031
          }
          });

          H5 新開 Webview 的方式去跳轉 H5

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

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


          Header 組件的設計


          Native 每次改動都比較“慢”,所以類似 Header 就很需要。
          1. 主流容器都是這么做的,比如微信、手機百度、攜程
          2. 沒有 Header 一旦出現(xiàn)網絡錯誤或者白屏,App 將陷入假死狀態(tài)

          PS:Native 打開 H5,如果 300ms 沒有響應則需要 loading 組件,避免白屏
          因為 H5 App 本身就有 Header 組件,站在前端框架層來說,需要確保業(yè)務代碼是一致的,所有的差異需要在框架層做到透明化,簡單來說 Header 的設計需要遵循:
          - H5 Header 組件與 Native 提供的 Header 組件使用調用層接口一致
          - 前端框架層根據(jù)環(huán)境判斷選擇應該使用 H5 的 Header 組件抑或 Native 的 Header 組件

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

          1. Header 左側與右側可配置,顯示為文字或者圖標(這里要求 Header 實現(xiàn)主流圖標,并且也可由業(yè)務控制圖標),并需要控制其點擊回調

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

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

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

           //Native以及前端框架會對特殊tagname的標識做默認回調,如果未注冊callback,或者點擊回調callback無返回則執(zhí)行默認方法
          // back前端默認執(zhí)行History.back,如果不可后退則回到指定URL,Native如果檢測到不可后退則返回Naive大首頁
          // home前端默認返回指定URL,Native默認返回大首頁
          this.header.set({
          left: [
          {
          //如果出現(xiàn)value字段,則默認不使用icon
          tagname: 'back',
          value: '回退',
          //如果設置了lefticon或者righticon,則顯示icon
          //native會提供常用圖標icon映射,如果找不到,便會去當前業(yè)務頻道專用目錄獲取圖標
          lefticon: 'back',
          callback: function () { }
          }
          ],
          right: [
          {
          //默認icon為tagname,這里為icon
          tagname: 'search',
          callback: function () { }
          },
          //自定義圖標
          {
          tagname: 'me',
          //會去hotel頻道存儲靜態(tài)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 端的實現(xiàn),這里會新增兩個接口,向 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 組件實現(xiàn):

          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;
          }

          //右邊按鈕必須保持數(shù)據(jù)一致性
          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 內嵌靜態(tài)資源可能是通過本地 file 的方式讀取,所以 cors 就行不通了。另外一個問題是防止爬蟲獲取數(shù)據(jù),由于 Native 針對網絡做了安全性設置(鑒權、防抓包等),所以 H5 的網絡請求由 Native 完成??赡苡行┤苏f H5 的網絡請求讓 Native 走就安全了嗎?我可以繼續(xù)爬取你的 Dom 節(jié)點啊。這個是針對反爬蟲的手段一。想知道更多的反爬蟲策略可以看看我這篇文章 Web反爬蟲方案

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

          HybridGet = function (url, param, callback) {

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

          };

          真實的業(yè)務場景,會將之封裝到數(shù)據(jù)請求模塊,在底層做適配,在H5站點下使用ajax請求,在Native內嵌時使用代理發(fā)出,與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不容易打通,所以在真實業(yè)務開發(fā)過程中,一般只會使用幾個關鍵的Native UI。


          賬號系統(tǒng)的設計


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

          • 使用 Native 代理做請求接口,如果沒有登錄則 Native 層喚起登錄頁

          • 直連方式使用 ajax 請求接口,如果沒登錄則在底層喚起登錄頁(H5)

          /*
          無論成功與否皆會關閉登錄框
          參數(shù)包括:
          success 登錄成功的回調
          error 登錄失敗的回調
          url 如果沒有設置success,或者success執(zhí)行后沒有返回true,則默認跳往此url
          */

          HybridUI.Login = function (opts) {
          //...
          };
          //=>
          requestHybrid({
          tagname: 'login',
          param: {
          success: function () { },
          error: function () { },
          url: '...'
          }
          });
          //與登錄接口一致,參數(shù)一致
          HybridUI.logout = function () {
          //...
          };

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


          Hybrid 資源管理


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

          假設有2個業(yè)務線:商城、購物車

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

          └─static
          ├─css
          └─images


          增量更新


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

          ChannelVersionmd5
          Mall1.0.112233000ww
          Cart1.1.228211122wt2

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

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

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

          因為是每個資源有版本號,所以如果線上的某個版本存在問題,那么可以根據(jù)相應的穩(wěn)定的版本號回滾到穩(wěn)定的版本。


          一些零散的解決方案


          靜態(tài)直出

          “直出”這個概念對前端同學來說,并不陌生。為了優(yōu)化首屏體驗,大部分主流的頁面都會在服務器端拉取首屏數(shù)據(jù)后通過 NodeJs 進行渲染,然后生成一個包含了首屏數(shù)據(jù)的 Html 文件,這樣子展示首屏的時候,就可以解決內容轉菊花的問題了。

          當然這種頁面“直出”的方式也會帶來一個問題,服務器需要拉取首屏數(shù)據(jù),意味著服務端處理耗時增加。

          不過因為現(xiàn)在 Html 都會發(fā)布到 CDN 上,WebView 直接從 CDN 上面獲取,這塊耗時沒有對用戶造成影響。

          手 Q 里面有一套自動化的構建系統(tǒng) Vnues,當產品經理修改數(shù)據(jù)發(fā)布后,可以一鍵啟動構建任務,Vnues 系統(tǒng)就會自動同步最新的代碼和數(shù)據(jù),然后生成新的含首屏 Html,并發(fā)布到 CDN 上面去。

          我們可以做一個類似的事情,自動同步最新的代碼和數(shù)據(jù),然后生成新的含首屏 Html,并發(fā)布到 CDN 上面去

          離線預推

          頁面發(fā)布到 CDN 上面去后,那么 WebView 需要發(fā)起網絡請求去拉取。當用戶在弱網絡或者網速比較差的環(huán)境下,這個加載時間會很長。于是我們通過離線預推的方式,把頁面的資源提前拉取到本地,當用戶加載資源的時候,相當于從本地加載,即使沒有網絡,也能展示首屏頁面。這個也就是大家熟悉的離線包。

          手 Q 使用 7Z 生成離線包, 同時離線包服務器將新的離線包跟業(yè)務對應的歷史離線包進行 BsDiff 做二進制差分,生成增量包,進一步降低下載離線包時的帶寬成本,下載所消耗的流量從一個完整的離線包(253KB)降低為一個增量包(3KB)。

          https://mp.weixin.qq.com/s?__biz=MzUxMzcxMzE5Ng==&mid=2247488218&idx=1&sn=21afe07eb642162111ee210e4a040db2&chksm=f951a799ce262e8f6c1f5bb85e84c2db49ae4ca0acb6df40d9c172fc0baaba58937cf9f0afe4&scene=27#wechat_redirect

          攔截加載

          事實上,在高度定制的 wap 頁面場景下,我們對于 webview 中可能出現(xiàn)的頁面類型會進行嚴格控制??梢酝ㄟ^內容的控制,避免 wap 頁中出現(xiàn)外部頁面的跳轉,也可以通過 webview 的對應代理方法,禁掉我們不希望出現(xiàn)的跳轉類型,或者同時使用,雙重保護來確保當前 webview 容器中只會出現(xiàn)我們定制過的內容。既然 wap 頁的類型是有限的,自然想到,同類型頁面大都由前端采用模板生成,頁面所使用的 html、css、js 的資源很可能是同一份,或者是有限的幾份,把它們直接隨客戶端打包在本地也就變得可行。加載對應的 url 時,直接 load 本地的資源。

          對于 webview 中的網絡請求,其實也可以交由客戶端接管,比如在你所采用的 Hybrid 框架中,為前端注冊一個發(fā)起網絡請求的接口。wap 頁中的所有網絡請求,都通過這個接口來發(fā)送。這樣客戶端可以做的事情就非常多了,舉個例子,NSURLProtocol 無法攔截 WKWebview 發(fā)起的網絡請求,采用 Hybrid 方式交由客戶端來發(fā)送,便可以實現(xiàn)對應的攔截。

          基于上面的方案,我們的 wap 頁的完整展示流程是這樣:客戶端在 webview 中加載某個 url,判斷符合規(guī)則,load 本地的模板 html,該頁面的內部實現(xiàn)是通過客戶端提供的網絡請求接口,發(fā)起獲取具體頁面內容的網絡請求,獲得填充的數(shù)據(jù)從而完成展示。

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

          WKWebView 網絡請求攔截

          方法一(Native 側):

          原生 WKWebView 在獨立于 app 進程之外的進程中執(zhí)行網絡請求,請求數(shù)據(jù)不經過主進程,因此在 WKWebView 上直接使用 NSURLProtocol 是無法攔截請求的。

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

          [WKBrowsingContextController registerSchemeForCustomProtocol:@"https"]

          但是因為出于性能的原因,WKWebView 的網絡請求在給主進程傳遞數(shù)據(jù)的時候會把請求的 body 去掉,導致攔截后請求的 body 參數(shù)丟失。

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

          為了解決 post 參數(shù)丟失的問題,mPaas 通過在 js 注入代碼,hook 了 js 上下文里的 XMLHTTPRequest 對象解決。

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

          通過上面的機制,既滿足了離線包的資源攔截訴求,也解決了 post 請求 body 丟失的問題。但是在一些場景還是存在一些問題,需要開發(fā)者進行適配。

          方法二(JS 側):

          通過 AJAX 請求的 hook 方式,將網絡請求的信息代理到客戶端本地。能拿到 WKWebView 里面的 post 請求信息,剩下的就不是問題啦。

          AJAX hook 的實現(xiàn)可以看這個 Repo.



          推薦閱讀

          ?  從 Flutter 和前端角度出發(fā),聊聊單線程模型下如何保證 UI 流暢性
          ?  iOS逆向 -- 逆向初探
          ?  WebKit 源碼調試與分析


          分享,收藏,點贊,在看四連,就差您了 ??????





          瀏覽 47
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩AV无码乱伦 | 在线日韩黄色 | 国产在线天堂 | www.国产豆花精品区 | 欧美日韩视频在线观看一区 |