关于hybrid-app:一个-Hybrid-SDK-设计与实现

45次阅读

共计 14915 个字符,预计需要花费 38 分钟才能阅读完成。

随着挪动浪潮的衰亡,各种 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 的设计常识。

  • Hybrid、Native、前端各自的工作是什么
  • Hybrid 交互接口如何设计
  • Hybrid 的 Header 如何设计
  • 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 调试技巧:

  • App 中开启 Webview 调试(WebView.setWebContentsDebuggingEnabled(true); )
  • chrome 浏览器输出 chrome://inspect/#devices 拜访能够调试的 webview 列表
  • 须要翻墙的环境

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。

繁难版本代码实现。

// 通用的 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) {
    // 生成随机函数
    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 的业务实现,所以实在业务中 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 之一,对前端来说有以下状况:

  • 页面内跳转,与 Hybrid 无关
  • H5 跳转 Native 界面
  • H5 新开 Webview 跳转 H5 页面,个别动画切换页面
    如果应用动画,依照业务来说分为后退、后退。forward & backword,规定如下,首先是 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',
     // 其它参数
     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',
    // 其它参数
    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 的设计须要遵循:

  • H5 Header 组件与 Native 提供的 Header 组件应用调用层接口统一
  • 前端框架层依据环境判断抉择应该应用 H5 的 Header 组件抑或 Native 的 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 变动,以做到登录态数据的一致性。

  • 应用 Native 代理做申请接口,如果没有登录则 Native 层唤起登录页
  • 直连形式应用 ajax 申请接口,如果没登录则在底层唤起登录页(H5)
/*
    无论胜利与否皆会敞开登录框
    参数包含: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

增量更新

每次业务开发结束后都须要在打包散发平台进行部署上线,之后会生成一个版本号。

Channel Version md5
Mall 1.0.1 12233000ww
Cart 1.1.2 28211122wt2

当 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)。

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

  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.

正文完
 0