本文是公众号文章直接贴过来的,发现图片都没自动上传,文章最后有个人公众号二维码(或者搜索关注:sanshuiqing123),关注个人公众号,查看历史文章。前端
上次文章介绍了Hybrid开发中经常使用到的Web和NA的通讯方案:JSBridge,经过比较以后,最终推荐安卓和iOS通用的scheme协议,能够保证APP内和外均可以使用,今天开始正式介绍整个Hybrid架构内容。就像本系列「开篇语」提到的,这里的Hybrid是「狭义的Hybrid」,而不是全部的NA套个webapp就是Hybrid。git
Hybrid技术体系是一套不少技术组成的完整知识架构。拿手百Hybrid方案,包括规范/约定、开发、联调、服务支撑等整个开发流程,而随着PWA之类「新」技术的产生又在考研技术架构面向将来的设计能力,因此手百Hybrid还在不断完善,注意是不断完善,不是推到重来!手百Hybrid技术总体架构以下:github
接下来系列会围绕本架构图中重要的部分依次展开,全面的介绍Hybrid开发知识,本文介绍模板本地化开发方案。web
所谓「模板本地化」,就是将Web页面内置到APP内,随版发布上线,而后经过云端接口实现更新,再直白一点:将H5网站的页面预先放到客户端发版,而后云端更新新版本。ajax
这个方案好处在于:redis
本地化模板提升页面打开速度,减小用户等待时间算法
模板可更新,版本控制更方即可控,收敛快chrome
Web页面和NA内置代码实现一套代码,减小开发成本数据库
上手成本小,开发就是实际开发Web(H5)页面,经过构建工具,生成Web页面和NA模板包不一样的代码json
标准H5代码,迁移成本小,经过Node和构建工具,能够作到H5版本先后同构,未来还能够不改代码的前提下适配PWA
本文提到的JSBridge调起协议是统一使用 hybrid://
开头,只是提供思路和介绍总体Hybrid模板包架构和用到的技术,不涉及到具体代码,可是文章保证干货和诚意都满满。
本文会介绍两种模板本地化方案,分别是:依赖客户端拦截器(proxy)的方案一;彻底无域名限制的方案二,两个方案的实现流程以下:

经过流程图来看,方案一比方案二多了拦截器。
方案一是「可感知」的,根据本地缓存的模板包域名(可下发、可彻底根据缓存的模板域名)进行过滤,若是本地有则读取本地缓存,若是本地没有则仍是访问线上(无损)。
方案二 是「预置型」的方案,只有发版时候规定的某些页面或者频道(插件)开通了Hybrid功能,才能使用Hybrid模板包缓存。
根据上面提到的两种方案,对应的模板包打包内容也不同。具体包内容目录结构以下所示:
方案一:拦截器 | 方案二:无拦截器 | |
---|---|---|
cookie | 带域名符合http规范 | 须要native支持 |
入口开放 | 方便,无需发版 | 须要发版 |
NA开发成本 | 拦截器开发 | 浏览器能力对齐 |
开发联调成本 | 较低 | 较高 |
webapp改形成本 | 方便 | 不方便 |
安全性 | 域名限制 | 须要独立控制 |
打包方案 | 按照网站目录打包 | 按照插件/频道打包 |
扩展性 | 依照PWA模式扩展能力 | - |
方案一相对来讲客户端开发量较大,并且拦截域名太多会有性能问题,可是优势也相对较多一些,具体须要本身权衡,再次强调,本系列本意是扫盲和普及Hybrid开发知识,尽可能全面的介绍Hybrid技术,不针对某种方案深刻展开,后续讨论内容也是如此。
模板本地化以后,客户端实际是经过 file://
协议访问本地模板,这样会致使跟域名相关的操做和方法须要客户端提供端能力支持,目前最重要的是一下几个:
http请求:若是在file协议下直接请求http[s]会引发跨域(Tips:跨域的几种状况有哪些?
cookie操做:域名没了,cookie怎么办?session也是cookie的实现
localstorage/sessionstorage:这俩是根据域名划分存储空间和读写的
这些浏览器的基本能力须要依靠客户端为页面提供端能力来补齐。浏览器根据域名作了限制的端能力,是遵照「同源策略」,因此在补齐端能力的时候,应该考虑进去「同源策略」,不能无节制的开通端能力
考虑到同源策略,每一个模板包会对应一个域名限制(系列文章中上一篇提到过JSBridge的安全鉴权问题),两种方案分别对应的同源策略解决方案是:
方案一拦截器:代理的域名就是要限制的域名范围
方案二无拦截器:在package.json/manifest.json中添加域名权限申请,好比 domain:xxx.baidu.com
根据本系列上篇JSBridge的最佳实践结合经常使用的请求方式,设计http请求的接口以下:
GET:hybrid://http/get?url=http://baidu.com&query={key1:value1,key2:value2}
POST:hybrid://http/post?url=http://baidu.com&data={key1:value1,key2:value2}
从协议上来看,post和get并无差别,可是hybrid的JSBridge协议设计是基于URL schema协议的,因此会有长度限制,对于简单的post操做使用这个协议就能够,可是对于post的data太大的状况下(不考虑上传文件等多媒体类POST操做,这方面能够经过后面文章提到的device API加强解决),须要提供
为了防止网站cookie被恶意修改,不少网站已经使用httponly的方式来操做cookie,这种状况下cookie实际上是没有必要经过前端使用js操做的,并且在 file://
协议下发送 http[s]://
API请求,http协议也会带上域名和路径下的cookie,因此cookie的操做我是不建议开端能力的。可是凡事都有可是。。。cookie操做仍是设计上吧:
hybrid://cookie/[get/set/delete]?name=value,expire=xxx;name2=value2,path=/
复制代码
浏览器缓存的设计参考web storage的key-value存储设计,而后增长过时时间,相似memcached这类内存缓存,由于value是string类型,因此不像redis那样支持多种存储类型格式。
hybrid://cache/save?key=xxx&value=xxx&callback=xxx[&expire=xxx]
复制代码hybrid://cache/get?key=xxx&callback=xxx
复制代码hybrid://cache/delete?key=xxx&callback=xxx复制代码
为了保证后续一套代码,在不一样的构建流程打出不同的模板包(好比H5版本和hybrid版本),构建过程再也不是简单的压缩资源,而是根据打包类型对组件进行有选择性的pick,这里咱们使用 FIS3
,根据不一样的 media
打出不同的包。例如:
fis.media('hybrid').match(…)复制代码fis.media('webapp').match(…)复制代码
详细用法请查看fis.baidu.com ,这里再也不展开。
咱们的H5代码和Hybrid的代码都在一块儿,彻底组件化以后,除了Hybrid的差别化的功能性组件,其余组件都是能够通用的。这些差别化的组件包括:
上一章节提到的Http、cache和cookie这类跟域名相关的代码,咱们分别封装了Hybrid内和H5版本,好比Http都叫Fetch,可是内部实现:hybrid是调用NA接口,H5是Fetch+ajax
浏览和导航类:好比跳转/打开新页面,NA内是打开一个新的webview,H5是 location
跳转或直接A标签
端能力和加强类组件:一样是大图浏览器,为了更好的交互,NA上作了加强,H5是纯web实现
这些差别性的代码都封装在不一样的组件内,经过设计模式中的「接口模式」对外暴漏同样的接口和使用方法。
这种设计,能够更好的提升业务同事学习和编写程序的成本,同时统一的组件化管理方便单元测试和联调,(划重点:等本系列联调部分会再重提这种设计的好处,那时候会发现这设计太赞了!)
这里安利集鹄大叔的jdists(有对应的fis插件):https://github.com/zswang/jdists
经过jdists能够经过注释的方式对代码进行区域化裁剪和联调功能,好比下面代码能够经过fis3的media来触发trigger,从而在不一样的条件下暴漏不一样的代码区域:
/*<debug>*/复制代码console.log(debug);复制代码/*</debug>*/复制代码/*<jdists trigger="hybrid">复制代码var fetch = require('na/fetch')复制代码</jdists>*/复制代码/*<jdists trigger="webapp">复制代码var fetch = require('web/fetch')复制代码</jdists>*/复制代码
除了代码pick以外,构建工具还要和模板包管理平台配合,增长额外的文件生成(好比生成版本号、签名、diff包)作到彻底自动化,而后包管理平台去拉取构建工具生成的zip包,上传到CDN平台,在数据库建立一条记录,关于模板包管理平台的内容,在下一篇文章详细介绍。
模板包设计好了以后,就须要考虑下发和更新的流程。除了考虑模板的更新,还须要考虑到容错机制,保证模板包升级不断出现问题或者某些极端状况下,服务可用。
模板更新时机是很讲究的,不一样的APP产品能够利用的时机可能不同,目前针对手百产品合适的时机有:
APP冷热启动
接口检测
频道异步检查,当打开某个Hybrid频道后台开始更新
页面主动检测
推送
综合来讲,无论怎样的更新时机,都逃不出两种更新的途径:
经过专门的NA更新接口上传本地版本号,server下发最新版本信息,彻底有NA完成模板包更新
经过页面JS接口主动检测进行更新
第一种途径,好比冷热启动、接口和频道异步都是经过上行请求将APP内单个或者多个频道的当前模板版本号上传给server,server根据模板维护cms推送过来的最新版本挨个频道依次比较版本号,若是版本号有更新,就下发对应的最新流量包地址和校验信息。
而页面主动检测包括两种:
在页面发起请求的API接口中带有当前的版本号,若是接口判断有更新,则下发对应的更新包信息(下载地址和校验信息),而后页面js经过NA接口通知客户端更新
页面js直接调用NA接口强制进行一次单独的检测,这样客户端会带着版本号走专门的接口只进行模板版本的更新检测,而后走NA的更新流程
当得知某个版本号存在不可修复的bug,须要紧急容错,能够经过server下发对应的command(app调起协议,也是一种schema)来指定在某个版本(模板版本、客户端版本)或者版本号范围内不调用Hybrid,而直接访问H5页面,达到快速止损。
举例说明:新闻列表是NA实现的,新闻的详情页是Hybrid的作的,因此Hybrid的上游是NA作的list,Hybrid详情页是经过NA的列表调起展示的。NA的list数据和调起协议(command)是由server下发的,若是server下发的是调起Hybrid页面,那么用户点击list打开Hybrid详情页;当Hybrid版本在某些状况下有问题(好比在某些客户端版本上有bug或者直接Hybrid某版本包就有bug),那么Server下发给NA调起详情页的Command就变成直接打开H5 webapp,而不是打开Hybrid,从而将有问题的Hybrid切换到线上H5,达到容错止损。
除了server端止损以外,咱们还设计了回滚机制,在包管理平台,能够将任意一个已经上线的版本包做为回滚包,而后生成新的版本号进行下发回滚。
客户端在安装更新包以前,先将老的版本包进行备份,而后开始安装,若是安装过程碰见问题,好比覆盖失败,或者空间不够等各类问题,就须要删除以前的安装操做,将备份包从新解压,保证整个安装过程是完整的,而不是安装一半。
针对模板包的收敛率率,除了更新时机以外,还应该从链路优化和包体积方面进行优化。
具体策略以下所列:
针对模板包作专门的通讯线路和协议优化
使用CDN就近存储
断点续传
分块下载
重试策略
减少包体积
增量更新包设计
增量包的设计通常有两种方式,能够根据不一样的场景和开发周期进行选择:
按文件diff进行增量下发
按二进制包进行增量下发
这种方式对于打包工具来讲很简单,并且客户端没有开发工做量。
客户端拿到模板更新包是总体覆盖安装的,覆盖安装的意思是:碰见新的文件就直接新增,碰见重名的文件就直接覆盖安装。这就有点像前端静态资源管理,能够打md5进行增量,能够同名覆盖。
咱们作法是当发版的时候,编译工具打出一个全量包,而后从版本库中找到最近的3次版本包,对文件进行遍历,找出diff,而后造成 Vn-Vn+1
的diff包,而这一切都是自动维护的。
我什么是最近3个版本包作diff,而不是所有?
由于若是所有随着发版愈来愈多,diff包会呈现指数增加,增长维护成本;并且模板收敛好了,两个以前老版本的量就很小了,没有必要为这些版本作增量包;再说全量包也不会大到哪里去嘛~
这种方式是将模板zip包,根据bsdiff差分算法打出不一样的patch,而后将下发给客户端,客户端再加上上一个版本的包生成新的全量包。
两种方式各有利弊,实际应用要权衡成本和收益。
模板包在下发的过程当中,会碰见被篡改的问题,并且包若是不完整,也会对Hybrid模板包的完整性和功能构成威胁。
首先整个模板包的更新接口和模板包zip的CDN都是使用HTTPS。另外在包安全性方面咱们分别作了两个校验:
模板完整性检测
模板合法性校验
结合启动更新流程(客户端和server交互部分)来说下:

在server升级接口会返回两个最重要的字段: md5
和 signature
。
md5用于校验zip的完整性,保证zip是完整的,可解压的;
signature是签名,签名算法是结合私钥、请求参数、版本号、客户端特征值和模板包的md5等组合计算的,签名算法是保证了包的安全性;这样即便包被劫持替换,不知道签名算法也不会经过校验。

知识是相通的,要学会触类旁通,文章为了通俗易懂,不少技术介绍根据前端常见的场景作了类比。只要有心,涉猎范围够广,你会发现:
写过chrome扩展的对于模板本地化方案是否是似曾相识?
若是以为和开发chrome扩展相似,能不能利用chrome的调试功能进行调试?
而模板本地化方案又跟PWA是否是能够通用?
PWA解决的痛点是什么?和本文的模板本地化方案怎么快速切换?
这些问题都值得深刻思考,且听后续文章慢慢道来~