hybrid的机制学习

对于hybrid真正的接触和深入了解,是从来到这家公司开始的,之前虽然接触过,但是那个时候使用的都是各种sdk,面试通过后,在来之前,前端团队负责人就跟我说,来之前可以先自己了解一下hybrid。
什么是hybrid开发
所谓hybrid,顾名思义就是‘混合模式开发’,简单的解释就是前端h5结合native的原声能力,结合各自的优势和长处,打造一个超h5,类原生的前端应用。
说到h5开发和native开发,这里还是搜集总结了各自的优缺点
Native/Web/Hybrid的对比
hybrid的机制学习
文章图片
Native/Web/Hybrid的对比 从这里可以明显看出来,native的优势呢:

  • 优秀的原生的操作体验
  • 性能好
  • 权限高
native的优势又往往受到设计和产品的青睐。那就开始产生出结合两者优势的技术方案。
那h5的具备的优势非常明显:
  • 无需走发版
  • 无版本问题
  • 一套代码多端运行
就这几点优势,也是为什么众多开发者和企业对其趋之若鹜。
接触hybrid以后就知道了,以前做的jssdk的开发、小程序的开发,以及后续接触的weex开发,其实都是hybrid的开发,只是结合的形式和开发的方式有所区别罢了,其实具体的核心部分都是一样,都是利用h5与native的通讯,让客户端和前端各司其责,然后有效的、高效率的结合。
小程序和weex
目前正火的小程序,其实处于h5和native中间的一个产物,webview作为渲染容器,结合小程序提供的高性能的原生组件,以及小程序官方提供的很便捷的开发api,开发者可以很快的开发一个类原生的在微信中使用的‘app’,当然还有不得不提的,小程序的发展迭代速度,社区的维护、文档的更新、健全的发布及版本控制机制,以其很多hybrid开发者梦寐以求的pc端的开发调试工具(还开放了远程调试哦)。
weex就比小程序更加的彻底,全部都会转换成原生(想想都很激动),前端按照h5开发的页面最终都呈现成原生。三端统一这一个跨时代的技术,一直在跌跌撞撞中缓慢前行,之前有了解过且'hello world'过的有React Native、PhoneGap、Cordova但是都很快就放弃了,因为这些都需要‘多栖程序员’才来驾驭。目前我们的项目中就是存在典型的两种技术栈,h5+hybrid以及weex+hybrid,对于两种技术栈的抉择上,即使weex目前仍存在很多问题,但就weex不会有‘安卓字体上下居中会偏上’的问题,我就会优先选它!
hybrid的实现方式
最终希望达到的效果就是js和native能自由通讯,目前主流的两种方式是:
  • url schema的声明式调用,如 hybird://changeTitle?title=修改标题&id=1
  • 典型的‘发布订阅’的函数式调用,通过客户端内置在webview中的bridge对象,通过调用bridge的send方法把js中的信息传给native,native通过访问webview的window对象,或者bridge对象,从而触发回调。
schema 对于url schema的这种形式,有点类似打点的处理方式,需要
  • 与native约定好数据格式
  • native拦截h5的请求
  • 分析处理
  • 回调
比如这里的,会拦截所有hybrid://的前端请求,openURL为前端调用的方法,后面即为处理的参数和回调id,这种方式一般就把回调函数挂载在window对象下。
那前端是如何发起请求呢?通常的有
  1. window.location.href跳转的形式
  2. iframe的src方式
但是location的形式存在一个问题,多次修改href的值,在Native层只能接收到最后一次请求,前面的请求都会被忽略掉。所用通常会使用iframe的形式。
window.callbacks[1] = function() { // ... } var url = 'hybird://changeTitle?title=修改标题&id=1'; var iframe = document.createElement('iframe'); iframe.style.width = '1px'; iframe.style.height = '1px'; iframe.style.display = 'none'; iframe.src = https://www.it610.com/article/url; document.body.appendChild(iframe); setTimeout(function() { iframe.remove(); }, 100);

bridge bridge的使用也是需要与native约定数据格式和函数,数据格式好理解,就类似与后端约定接口的数据格式一样(这里约定的是第一个参数是请求数据,第二个是回调处理函数)。
bridge的实现原理可以简单用下面这段代码表示
bridge.callbacks[1] = function() {// 先将回调执行函数挂载在bridge下 //deal in changeTitle callback delete bridge.callbacks[1]; } bridge.send({ target: 'changeTitle', data: { title: '修改标题' }, id:1, // 告诉navtive 回调的函数挂载的id })

send就是bridge提供的方法,实现js到native到通讯(安卓的WebViewJavascriptBridge对应的是sendMessage,ios的webkit.WebViewJavascriptBridge对应的是postMessage)。
但是实际的开发过程中,往往不希望这样去自己定义id,而是希望通过下面这种方式来调用。我们扩展一下send函数,我们还接收一个回调函数作为参数,这里会在执行最后的send函数之前,会生成唯一的id(自增或通过时间戳等方式)作为取回调函数的钥匙,并将回调函数挂载在bridge对象下。
bridge.send({ target: 'changeTitle', data: { title: '修改标题' }, },() => { // deal in changeTitle callback })

这里就会有一个生成唯一的id的过程
bridge.send = function (message,callback) { message.id = new Date().getTime(); bridge.callbacks[id] = callback; //挂载成功后,在执行真正的send if(isAndroid){ window.WebViewJavascriptBridge.sendMessage(message); } else { window.webkit.messageHandlers.WebViewJavascriptBridge.postMessage(message); } };

然后我们在window对象下定义一个doInFinish的函数,doInFinish是native到js的通讯过程,调用的时候传入唯一的回调id,这个时候就去取bridge.callbacks下对应id的函数然后执行,这里需要注意的是,为了防止对象冗余,所以在执行后会进行销毁。这里也就解释了我们注册了右上角的分享按钮,分享一次后需重新注册一次,因为调用一次回调函数就被销毁了。
function doInFinish(id,result) { const callback = bridge.callbacks[id]; callback(result); delete bridge.callbacks[id]; };

以上就是hybird的基本原理了,两种形式在业务中都有使用。
url schema用于‘声明式‘调用,通常用于页面的跳转链接。如指定打开带有特殊属性(禁用下拉刷新、隐藏分享按钮)的webview容器,或指定打开weex容器等,就可以在链接上带上固定的参数,如https://xxxxx/index.html?hybrid_info={'disableRefresh': true,'hideShare': true}
bridge这种’函数式‘调用,适合在代码中使用,底层做好封装,js就可以执行函数那样去调用native的函数了。
以下是以changeTitle为例,用简图说明下bridge的调用过程。
hybrid的机制学习
文章图片
前端调用过程简图 令我困惑的config注册过程
在了解以上的基础原理介绍以后,使用函数式调用使用hybrid的时候,直接使用即可,但是看了公司的源码会发现,在hybrid的调用之前,有一个约定的config操作,这里称之为注册的过程。
就拿changeTitle为例,按照上面的原理分析,按照bridge里分析的直接send就可以了,但是实际上是先config,之后在进行changeTitle的调用。
// 这里抽象逻辑后的代码结构 bridge.send({ target: 'config', data: { api: 'changeTitle' }, },() => {// 注册成功 bridge.send({ target: 'changeTitle', data: { title: '修改标题' }, },() => { // deal in changeTitle callback }) })

这里也是纠结了一下,觉得注册过程略显冗余,经过分析和探讨,这块之所以会有注册的策略,主要的原因有以下几点
  1. 防止调用的hybrid方法不存在出错
js调用之后处于’黑盒’(无法跟踪定位问题)状态,注册之后,调用出错至少能排除’native还没有该方法’这种情况。由于h5的灵活和跨平台性,无法确定和限制h5最终运行的环境,而且很多公司会有好几个app,各app之间的差异性(版本、私有的实现等)会导致js的环境不可控,通过提前注册的形式来明确的知道当前所有调用的方法Native是否已经注册并支持。
  1. 应对后续有可能接入第三方的应用的可能性
类似微信的jssdk,不同的主体对于微信的native开放出来的sdk使用权限不一样,通过用appid注册的形式,来确定sdk的使用权限。
这里就出现了一个问题,hybrid每次调用都要注册么,这样岂不是很冗余而且影响性能么?微信的jssdk是下面使用的
wx.config({// 仅执行一次 debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: '', // 必填,公众号的唯一标识 timestamp: , // 必填,生成签名的时间戳 nonceStr: '', // 必填,生成签名的随机串 signature: '',// 必填,签名 jsApiList: ['chooseImage'] // 必填,需要使用的JS接口列表 }); wx.chooseImage({ count: 1, // 默认9 sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有 sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有 success: function (res) { var localIds = res.localIds; // 返回选定照片的本地ID列表,localId可以作为img标签的src属性显示图片 } });

会发现也是会分两个步骤,先是进行config,成功之后就可以直接调用使用了。
【hybrid的机制学习】那我们的自己的hybrid的原理其实也是一样,我们的基础使用方法也类似hybrid('changeTitle').changeTitle({title: '修改标题'},() => {}),其中同样也是包含了两个过程,先注册然后进行调用。
最后说一下之所以能这样链式调用,是因为这里注册完以后,会以此为函数名挂载在hybrid对象下,且返回当前对象。
下面这里例子就是先在对象a下注册一个方法名为b的函数,然后直接调用b函数,下次调用b函数的时候就无需走注册的过程。
let a = {} a = (name) => { // ... config finish if (typeof a[name] !== 'function'){ // 仅挂载一次 a[name] = (params) => { //deal with params console.log(params) } } return a //需返回当前对象 } a('b').b('test')// 'test'

    推荐阅读