姬長信(Redy)

Awesome Hybrid – 1

# Hybrid 设计与实现 随着移动浪潮的兴起/uff0c各种 App 层出不穷/uff0c极速发展的业务拓展提升了团队对开发效率的要求/uff0c这个时候纯粹使用 Native 开发技术成本难免会更高一点。而 H5 的低成本、高效率、跨平台等特性马上被利用起来了/uff0c形成一种新的开发模式/uff1a Hybrid App 作为一种混合开发的模式/uff0cHybrid App 底层依赖于 Native 提供的容器/uff08Webview/uff09/uff0c上层使用各种前端技术完成业务开发/uff08现在三足鼎立的 Vue、React、Angular/uff09/uff0c底层透明化、上层多样化。这种场景非常有利于前端介入/uff0c非常适合业务的快速迭代。于是 Hybrid 火了。 大道理谁都懂/uff0c但是按照我知道的情况/uff0c还是有非常多的人和公司在 Hybrid 这一块并没有做的很好/uff0c所以我将我的经验做一个总结/uff0c希望可以帮助广大开发者的技术选型有所帮助 ## Hybrid 的一个现状 可能早期都是 PC 端的网页开发/uff0c随着移动互联网的发展/uff0ciOS、Android 智能手机的普及/uff0c非常多的业务和场景都从 PC 端转移到移动端。开始有前端开发者为移动端开发网页。这样子早期资源打包到 Native App 中会造成应用包体积的增大。越来越多的业务开始用 H5 尝试/uff0c这样子难免会需要一个需要访问 Native 功能的地方/uff0c这样子可能早期就是懂点前端技术的 Native 开发者自己封装或者暴露 Native 能力给 JS 端/uff0c等业务较多的时候者样子很明显不现实/uff0c就需要专门的 Hybrid 团队做这个事情/uff1b量大了/uff0c就需要规矩/uff0c就需要规范。 总结/uff1a 1. Hybrid 开发效率高、跨平台、低成本 2. Hybrid 从业务上讲/uff0c没有版本问题/uff0c有 Bug 可以及时修复 Hybrid 在大量应用的时候就需要一定的规范/uff0c那么本文将讨论一个 Hybrid 的设计知识。 - Hybrid 、Native、前端各自的工作是什么 - Hybrid 交互接口如何设计 - Hybrid 的 Header 如何设计 - Hybrid 的如何设计目录结构以及增量机制如何实现 - 资源缓存策略/uff0c白屏问题... ## Native 与前端分工 在做 Hybird 架构设计之前我们需要分清 Native 与前端的界限。首先 Native 提供的是宿主环境/uff0c要合理利用 Native 提供的能力/uff0c要实现通用的 Hybrid 架构/uff0c站在大前端的视觉/uff0c我觉得需要考虑以下核心设计问题。 ### 交互设计 Hybrid 架构设计的第一要考虑的问题就是如何设计前端与 Native 的交互/uff0c如果这块设计不好会对后续的开发、前端框架的维护造成深远影响。并且这种影响是不可逆、积重难返。所以前期需要前端与 Native 好好配合、提供通用的接口。比如 1. Native UI 组件、Header 组件、消息类组件 2. 通讯录、系统、设备信息读取接口 3. H5 与 Native 的互相跳转。比如 H5 如何跳转到一个 Native 页面/uff0cH5 如何新开 Webview 并做动画跳转到另一个 H5 页面 ### 账号信息设计 账号系统是重要且无法避免的/uff0cNative 需要设计良好安全的身份验证机制/uff0c保证这块对业务开发者足够透明/uff0c打通账户体系 ### Hybrid 开发调试 功能设计、编码完并不是真正结束/uff0cNative 与前端需要商量出一套可开发调试的模型/uff0c不然很多业务开发的工作难以继续。 [iOS调试技巧](https://www.jianshu.com/p/f430caa81fa8) Android 调试技巧/uff1a 1. App 中开启 Webview 调试(WebView.setWebContentsDebuggingEnabled(true); ) 2. chrome 浏览器输入 chrome://inspect/#devices 访问可以调试的 webview 列表 3. 需要翻墙的环境 ![结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HybridStructure.jpg) ## Hybrid 交互设计 Hybrid 交互无非是 Native 调用 H5 页面JS 方法/uff0c或者 H5 页面通过 JS 调 Native 提供的接口。2者通信的桥梁是 Webview。 业界主流的通信方法/uff1a1.桥接对象/uff08时机问题/uff0c不太主张这种方式/uff09/uff1b2.自定义 Url scheme ![通信设计](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Native-JS-Communication.png) App 自身定义了 url scheme/uff0c将自定义的 url 注册到调度中心/uff0c例如 weixin:// 可以打开微信。 关于 Url scheme 如果不太清楚可以看看 [这篇文章](https://www.jianshu.com/p/253479ccc83a) ### JS to Native Native 在每个版本都会提供一些 Api/uff0c前端会有一个对应的框架团队对其封装/uff0c释放业务接口。举例 ``` 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 与前端交互的桥梁/uff0c前端可以通过这个对象获得访问 Native 的能力 ### Api 交互 调用 Native Api 接口的方式和使用传统的 Ajax 调用服务器/uff0c或者 Native 的网络请求提供的接口相似 ![Api交互](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HybridApi.jpg) 所以我们需要封装的就是模拟创建一个类似 Ajax 模型的 Native 请求。 ![通信示例](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Hybrid-Ajax.jpg) ### 格式约定 交互的第一步是设计数据格式。这里分为请求数据格式与响应数据格式/uff0c参考 Ajax 模型/uff1a ``` $.ajax({ type: "GET", url: "test.json", data: {username:$("#username").val(), content:$("#content").val()}, dataType: "json", success: function(data){ renderUI(data); } }); ``` ``` $.ajax(options) => XMLHTTPRequest type(默认值/uff1aGET)/uff0cHTTP请求方法/uff08GET|POST|DELETE|.../uff09 url(默认值/uff1a当前url)/uff0c请求的url地址 data(默认值/uff1a'') 请求中的数据如果是字符串则不变/uff0c如果为Object/uff0c则需要转换为String/uff0c含有中文则会encodeURI ``` 所以 Hybrid 中的请求模型为/uff1a ``` requestHybrid({ // H5 请求由 Native 完成 tagname: 'NativeRequest', // 请求参数 param: requestObject, // 结果的回调 callback: function (data) { renderUI(data); } }); ``` 这个方法会形成一个 URL/uff0c比如/uff1a 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 环境可以监控内部任何的资源请求/uff0c判断如果是 SDGHybrid 则分发事件/uff0c处理结束可能会携带参数/uff0c参数需要先 urldecode 然后将结果数据通过 Webview 获取 window 对象中的 callback/uff08Hybrid_时间戳/uff09 数据返回的格式和普通的接口返回格式类似 ``` { errno: 1, message: 'App版本过低/uff0c请升级App版本', data: {} } ``` 这里注意/uff1a真实数据在 data 节点中。如果 errno 不为0/uff0c则需要提示 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)); }; //获取版本信息/uff0c约定APP的navigator.userAgent版本包含版本信息/uff1ascheme/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 容器/uff0c框架&&底层不太关心 H5 的业务实现/uff0c所以真实业务中 Native 调用 H5 场景较少。 上面的网络访问 Native 代码/uff08iOS为例/uff09 ``` 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 良好的交互设计是第一步/uff0c在真实业务开发中有一些 Api 一定会由应用场景。 ### 跳转 跳转是 Hybrid 必用的 Api 之一/uff0c对前端来说有以下情况/uff1a - 页面内跳转/uff0c与 Hybrid 无关 - H5 跳转 Native 界面 - H5 新开 Webview 跳转 H5 页面/uff0c一般动画切换页面 如果使用动画/uff0c按照业务来说分为前进、后退。forward & backword/uff0c规定如下/uff0c首先是 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', // 跳转方式/uff0cH5跳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', // 跳转方式/uff0cH5跳Native type: 'native', // 其它参数 id: 20151031 } }); ``` H5 新开 Webview 的方式去跳转 H5 ``` requestHybrid({ tagname: 'forward', param: { // 要去到的页面/uff0c首先找到goods频道/uff0c然后定位到detail模块 topage: 'goods/detail ', //跳转方式/uff0cH5新开Webview跳转/uff0c最后装载H5页面 type: 'webview', //其它参数 id: 20151031 } }); ``` back 与 forward 一致/uff0c可能会有 animatetype 参数决定页面切换的时候的动画效果。真实使用的时候可能会全局封装方法去忽略 tagname 细节。 ## Header 组件的设计 Native 每次改动都比较/u201c慢/u201d/uff0c所以类似 Header 就很需要。 1. 主流容器都是这么做的/uff0c比如微信、手机百度、携程 2. 没有 Header 一旦出现网络错误或者白屏/uff0cApp 将陷入假死状态 PS/uff1a Native 打开 H5/uff0c如果 300ms 没有响应则需要 loading 组件/uff0c避免白屏 因为 H5 App 本身就有 Header 组件/uff0c站在前端框架层来说/uff0c需要确保业务代码是一致的/uff0c所有的差异需要在框架层做到透明化/uff0c简单来说 Header 的设计需要遵循/uff1a - H5 Header 组件与 Native 提供的 Header 组件使用调用层接口一致 - 前端框架层根据环境判断选择应该使用 H5 的 Header 组件抑或 Native 的 Header 组件 一般来说 Header 组件需要完成以下功能/uff1a 1. Header 左侧与右侧可配置/uff0c显示为文字或者图标/uff08这里要求 Header 实现主流图标/uff0c并且也可由业务控制图标/uff09/uff0c并需要控制其点击回调 2. Header 的 title 可设置为单标题或者主标题、子标题类型/uff0c并且可配置 lefticon 与 righticon/uff08icon居中/uff09 3. 满足一些特殊配置/uff0c比如标签类 Header 所以/uff0c站在前端业务方来说/uff0cHeader 的使用方式为/uff08其中 tagname 是不允许重复的/uff09/uff1a ``` //Native以及前端框架会对特殊tagname的标识做默认回调/uff0c如果未注册callback/uff0c或者点击回调callback无返回则执行默认方法 // back前端默认执行History.back/uff0c如果不可后退则回到指定URL/uff0cNative如果检测到不可后退则返回Naive大首页 // home前端默认返回指定URL/uff0cNative默认返回大首页 this.header.set({ left: [ { //如果出现value字段/uff0c则默认不使用icon tagname: 'back', value: '回退', //如果设置了lefticon或者righticon/uff0c则显示icon //native会提供常用图标icon映射/uff0c如果找不到/uff0c便会去当前业务频道专用目录获取图标 lefticon: 'back', callback: function () { } } ], right: [ { //默认icon为tagname/uff0c这里为icon tagname: 'search', callback: function () { } }, //自定义图标 { tagname: 'me', //会去hotel频道存储静态header图标资源目录搜寻该图标/uff0c没有便使用默认图标 icon: 'hotel/me.png', callback: function () { } } ], title: 'title', //显示主标题/uff0c子标题的场景 title: ['title', 'subtitle'], //定制化title title: { value: 'title', //标题右边图标 righticon: 'down', //也可以设置lefticon //标题类型/uff0c默认为空/uff0c设置的话需要特殊处理 //type: 'tabs', //点击标题时的回调/uff0c默认为空 callback: function () { } } }); ``` 因为 Header 左边一般来说只有一个按钮/uff0c所以其对象可以使用这种形式/uff1a ``` this.header.set({ back: function () { }, title: '' }); //语法糖=> this.header.set({ left: [{ tagname: 'back', callback: function(){} }], title: '', }); ``` 为完成 Native 端的实现/uff0c这里会新增两个接口/uff0c向 Native 注册事件/uff0c以及注销事件/uff1a ``` 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 组件实现/uff1a ``` 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 } }); }, //注册事件/uff0c将事件存于本地 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 Web网络请求由Native完成](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DataViaNative.png) 这个使用场景和 Header 组件一致/uff0c前端框架层必须做到对业务透明化/uff0c业务事实上不必关心这个网络请求到底是由 Native 还是浏览器发出。 ``` HybridGet = function (url, param, callback) { }; HybridPost = function (url, param, callback) { }; ``` 真实的业务场景/uff0c会将之封装到数据请求模块/uff0c在底层做适配/uff0c在H5站点下使用ajax请求/uff0c在Native内嵌时使用代理发出/uff0c与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/uff0c比如 加载层loading、消息框toast ``` var HybridUI = {}; HybridUI.showLoading(); //=> requestHybrid({ tagname: 'showLoading' }); HybridUI.showToast({ title: '111', //几秒后自动关闭提示框/uff0c-1需要点击才会关闭 hidesec: 3, //弹出层关闭时的回调 callback: function () { } }); //=> requestHybrid({ tagname: 'showToast', param: { title: '111', hidesec: 3, callback: function () { } } }); ``` Native UI与前端UI不容易打通/uff0c所以在真实业务开发过程中/uff0c一般只会使用几个关键的Native UI。 ## 账号系统的设计 Webview 中跑的网页/uff0c账号登录与否由是否携带密钥 cookie 决定/uff08不能保证密钥的有效性/uff09。因为 Native 不关注业务实现/uff0c所以每次载入都有可能是登录成功跳转回来的结果/uff0c所以每次载入都需要关注密钥 cookie 变化/uff0c以做到登录态数据的一致性。 - 使用 Native 代理做请求接口/uff0c如果没有登录则 Native 层唤起登录页 - 直连方式使用 ajax 请求接口/uff0c如果没登录则在底层唤起登录页/uff08H5/uff09 ```javascript /* 无论成功与否皆会关闭登录框 参数包括/uff1a success 登录成功的回调 error 登录失败的回调 url 如果没有设置success/uff0c或者success执行后没有返回true/uff0c则默认跳往此url */ HybridUI.Login = function (opts) { //... }; //=> requestHybrid({ tagname: 'login', param: { success: function () { }, error: function () { }, url: '...' } }); //与登录接口一致/uff0c参数一致 HybridUI.logout = function () { //... }; ``` 在设计 Hybrid 层的时候/uff0c接口要做到对于处于 Hybrid 环境中的代码乐意通过接口获取 Native 端存储的用户账号信息/uff1b对于处于传统的网页环境/uff0c可以通过接口获取线上的账号信息/uff0c然后将非敏感的信息存储到 LocalStorage 中/uff0c然后每次页面加载从 LocalStorage 读取数据到内存中/uff08比如 Vue.js 框架中的 Vuex/uff0cReact.js 中的 Redux/uff09 ## Hybrid 资源管理 Hybrid 的资源需要 `增量更新` 需要拆分方便/uff0c所以一个 Hybrid 资源结构类似于下面的样子 ![Hybrid资源结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-07-08-Hybrid-ResourceStructure.png) 假设有2个业务线/uff1a商城、购物车 ```tex WebApp /u2502- Mall /u2502- Cart /u2502 index.html //业务入口html资源/uff0c如果不是单页应用会有多个入口 /u2502 /u2502 main.js //业务所有js资源打包 /u2502 /u2502 /u2502 /u2514/u2500static //静态样式资源 /u2502 /u251c/u2500css /u2502 /u251c/u2500hybrid //存储业务定制化类Native Header图标 /u2502 /u2514/u2500images /u251c/u2500libs /u2502 libs.js //框架所有js资源打包 /u2502 /u2514/u2500static /u251c/u2500css /u2514/u2500images ``` ## 增量更新 每次业务开发完毕后都需要在打包分发平台进行部署上线/uff0c之后会生成一个版本号。 | Channel | Version | md5 | | ------- | ------- | ----------- | | Mall | 1.0.1 | 12233000ww | | Cart | 1.1.2 | 28211122wt2 | 当 Native App 启动的时候会从服务端请求一个接口/uff0c接口的返回一个 json 串/uff0c内容是 App 所包含的各个 H5 业务线的版本号和 md5 信息。 拿到 json 后和 App 本地保存的版本信息作比较/uff0c发现变动了则去请求相应的接口/uff0c接口返回 md5 对应的文件。Native 拿到后完成解压替换。 全部替换完毕后将这次接口请求到的资源版本号信息保存替换到 Native 本地。 因为是每个资源有版本号/uff0c所以如果线上的某个版本存在问题/uff0c那么可以根据相应的稳定的版本号回滚到稳定的版本。 ## 一些零散的解决方案 1. 静态直出 /u201c直出/u201d这个概念对前端同学来说/uff0c并不陌生。为了优化首屏体验/uff0c大部分主流的页面都会在服务器端拉取首屏数据后通过 NodeJs 进行渲染/uff0c然后生成一个包含了首屏数据的 Html 文件/uff0c这样子展示首屏的时候/uff0c就可以解决内容转菊花的问题了。 当然这种页面/u201c直出/u201d的方式也会带来一个问题/uff0c服务器需要拉取首屏数据/uff0c意味着服务端处理耗时增加。 不过因为现在 Html 都会发布到 CDN 上/uff0cWebView 直接从 CDN 上面获取/uff0c这块耗时没有对用户造成影响。 手 Q 里面有一套自动化的构建系统 Vnues/uff0c当产品经理修改数据发布后/uff0c可以一键启动构建任务/uff0cVnues 系统就会自动同步最新的代码和数据/uff0c然后生成新的含首屏 Html/uff0c并发布到 CDN 上面去。 我们可以做一个类似的事情/uff0c自动同步最新的代码和数据/uff0c然后生成新的含首屏 Html/uff0c并发布到 CDN 上面去 2. 离线预推 页面发布到 CDN 上面去后/uff0c那么 WebView 需要发起网络请求去拉取。当用户在弱网络或者网速比较差的环境下/uff0c这个加载时间会很长。于是我们通过离线预推的方式/uff0c把页面的资源提前拉取到本地/uff0c当用户加载资源的时候/uff0c相当于从本地加载/uff0c即使没有网络/uff0c也能展示首屏页面。这个也就是大家熟悉的离线包。 手 Q 使用 7Z 生成离线包, 同时离线包服务器将新的离线包跟业务对应的历史离线包进行 BsDiff 做二进制差分/uff0c生成增量包/uff0c进一步降低下载离线包时的带宽成本/uff0c下载所消耗的流量从一个完整的离线包/uff08253KB/uff09降低为一个增量包/uff083KB/uff09。 https://mp.weixin.qq.com/s?__biz=MzUxMzcxMzE5Ng==&mid=2247488218&idx=1&sn=21afe07eb642162111ee210e4a040db2&chksm=f951a799ce262e8f6c1f5bb85e84c2db49ae4ca0acb6df40d9c172fc0baaba58937cf9f0afe4&scene=27#wechat_redirect 3. 拦截加载 事实上/uff0c在高度定制的 wap 页面场景下/uff0c我们对于 webview 中可能出现的页面类型会进行严格控制。可以通过内容的控制/uff0c避免 wap 页中出现外部页面的跳转/uff0c也可以通过 webview 的对应代理方法/uff0c禁掉我们不希望出现的跳转类型/uff0c或者同时使用/uff0c双重保护来确保当前 webview 容器中只会出现我们定制过的内容。既然 wap 页的类型是有限的/uff0c自然想到/uff0c同类型页面大都由前端采用模板生成/uff0c页面所使用的 html、css、js 的资源很可能是同一份/uff0c或者是有限的几份/uff0c把它们直接随客户端打包在本地也就变得可行。加载对应的 url 时/uff0c直接 load 本地的资源。 对于 webview 中的网络请求/uff0c其实也可以交由客户端接管/uff0c比如在你所采用的 Hybrid 框架中/uff0c为前端注册一个发起网络请求的接口。wap 页中的所有网络请求/uff0c都通过这个接口来发送。这样客户端可以做的事情就非常多了/uff0c举个例子/uff0cNSURLProtocol 无法拦截 WKWebview 发起的网络请求/uff0c采用 Hybrid 方式交由客户端来发送/uff0c便可以实现对应的拦截。 基于上面的方案/uff0c我们的 wap 页的完整展示流程是这样/uff1a客户端在 webview 中加载某个 url/uff0c判断符合规则/uff0cload 本地的模板 html/uff0c该页面的内部实现是通过客户端提供的网络请求接口/uff0c发起获取具体页面内容的网络请求/uff0c获得填充的数据从而完成展示。 NSURLProtocol能够让你去重新定义苹果的URL加载系统(URL Loading System)的行为/uff0cURL Loading System里有许多类用于处理URL请求/uff0c比如NSURL/uff0cNSURLRequest/uff0cNSURLConnection和NSURLSession等。当URL Loading System使用NSURLRequest去获取资源的时候/uff0c它会创建一个NSURLProtocol子类的实例/uff0c你不应该直接实例化一个NSURLProtocol/uff0cNSURLProtocol看起来像是一个协议/uff0c但其实这是一个类/uff0c而且必须使用该类的子类/uff0c并且需要被注册。