一个 Hybrid SDK 设计与实现

发表于 3年以前  | 总阅读数:232 次

随着移动浪潮的兴起,各种 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调试技巧 https://www.jianshu.com/p/f430caa81fa8

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 本地。

因为是每个资源有版本号,所以如果线上的某个版本存在问题,那么可以根据相应的稳定的版本号回滚到稳定的版本。

一些零散的解决方案


静态直出

“直出”这个概念对前端同学来说,并不陌生。为了优化首屏体验,大部分主流的页面都会在服务器端拉取首屏数据后通过 NodeJs 进行渲染,然后生成一个包含了首屏数据的 Html 文件,这样子展示首屏的时候,就可以解决内容转菊花的问题了。

当然这种页面“直出”的方式也会带来一个问题,服务器需要拉取首屏数据,意味着服务端处理耗时增加。

不过因为现在 Html 都会发布到 CDN 上,WebView 直接从 CDN 上面获取,这块耗时没有对用户造成影响。

手 Q 里面有一套自动化的构建系统 Vnues,当产品经理修改数据发布后,可以一键启动构建任务,Vnues 系统就会自动同步最新的代码和数据,然后生成新的含首屏 Html,并发布到 CDN 上面去。

我们可以做一个类似的事情,自动同步最新的代码和数据,然后生成新的含首屏 Html,并发布到 CDN 上面去

离线预推

页面发布到 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]

拦截加载

事实上,在高度定制的 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看起来像是一个协议,但其实这是一个类,而且必须使用该类的子类,并且需要被注册。

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.


本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/FwoodWwvBej3enq3vfqrOw

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录