源宝导读:Hybrid-APP技术不仅具有“Native APP的良好交互体验”同时也具备“Web APP跨平台开发的优势”。既然Hybrid-APP有这么多优势,那么究竟什么样的APP才算Hybrid App呢?本文将分享我们的技术研究成果。
狭义的Hybrid:
也是现在大家普遍认知的,Hybrid就是一种给 WebView 增加一些js通信可以调用原生API的方式。
广义Hybrid:
前端的开发思路与客户端原生的开发思路相结合。
通过原生的配合,把原本js or 前端开发做不到的事情做到了,用原生的方式增强了原本的前端技术能力。
WebView+Bridge、RN、weex、小程序。
我能否认为,只要是前端的开发思路与客户端原生的开发思路相结合,就认为他是一种Hybrid?
通过原生的配合,把原本js or 前端开发做不到的事情做到了,用原生的方式增强了原本的前端技术能力,是否就是一种 Hybrid?
无论是WebView+Bridge也好,RN类似的原生渲染框架也好,小程序也好,某种意义上讲,他们都算Hybrid?
接下来我们来看一下,一个Hybrid框架所需要具备的基本能力:
跨平台能力。
这也是Hybrid应用与原生应用相比最大的优点,一次编写随处运行。
灵活的业务模块扩展能力。
良好的调用原生功能的能力。
由于在APP中有些功能必须由原生端提供,所以还需要有良好的调用原生功能的能力。
快速更新迭代的能力。
使用原生技术开发的APP每次更新都需要上传应用商店审核,但是使用Hybrid技术开发的应用可以绕过应用商店实现热更新。
在Hybrid APP中最核心的技术就在于前端与客户端如何通信,接下来我们看一下,js与native之间是如何通信的。
假跳转的请求拦截
A标签跳转。
原地跳转。
iframe跳转。
弹窗拦截
alert()
prompt()
confirm()
JS上下文注入
苹果JavaScriptCore注入。
安卓addJavascriptInterface注入。
苹果scriptMessageHandler注入。
evaluatingJavaScript 直接注入执行JS代码
JS是一个脚本语言,任何一个JS引擎都可以在任意时机直接执行JS代码,我们可以把任何Native想要传递的消息/数据直接写进JS代码中。
loadUrl 浏览器用’javascript:’+JS代码做跳转地址
在浏览器中,可以直接用’javascript:xxxx’来简单的执行一些JS代码,这个方法只有安卓可以用,因为iOS必须先将url字符串生成Request再交给webview去load
WKUserScript WKWebView的 addUserScript 方法
WKWebView官方提供了一个Api,可以让WebView在加载页面的时候,自动执行注入一些预先准备好的JS
假跳转的请求拦截 就是由网页发出一条新的跳转请求,跳转的目的地是一个非法的压根就不存在的地址。
url地址由协议、域名、路径、参数这么几个部分构成,我们可以构建一条假的url:用协议与域名当做通信识别、用路径当做指令识别、用参数当做数据传递。
客户端会无差别拦截所有请求,真正的url地址应该照常放过,只有协议域名匹配的url地址才应该被客户端拦截,拦截下来的url不会导致webview继续跳转错误地址,因此无感知,相反拦截下来的url我们可以读取其中路径当做指令,读取其中参数当做数据,从而根据约定调用对应的native原生代码。
JS发起调用
JS其实有很多种方式发起假请求,跟发起一个新请求没啥两样,只要按协议约定 生成假请求地址,正常的发起跳转即可,任何一种方式都可以让客户端拦截住。
客户端拦截
安卓的拦截方式:shouldOverride UrlLoading。
UIWebView的拦截方式:webView: shouldStartLoadWithRequest :navigationType。
WKWebView的拦截方式:webView: decidePolicyForNavigationAction :decisionHandler。
JS发起调用
可以使用alert/confirm/prompt三种弹框,每种弹框都可以由JS发出一串字符串,用于展示在弹框之上,而此字符串恰巧就是可以用来传递数据,我们把所有要传递通讯的信息,都封装进入一个js对象,然后生成字典,最后序列化成json转成字符串。
客户端拦截
安卓的拦截方式:onJsPrompt。
UIWebView的拦截方式:不支持截获任何一种弹框。
WKWebView的拦截方式:webView: runJavaScriptText InputPanelWith Prompt :balbala。
客户端注入
UIWebView可以通过KVC的方法,直接拿到整个WebView当前所拥有的JS上下文documentView.webView.mainFrame.javaScriptContext。
拿到了JSContext,一切的使用方式就和直接操作JavaScriptCore没啥区别了,我们可以把任何遵循JSExport协议的对象直接注入JS,让JS能够直接控制和操作。
JS调用
在没经过客户端注入的时候,直接使用调用callNativeFunction()会报 callNativeFunction is not defined这个错误,说明此时JS上下全文全局,是没有这个函数的,调用无效。
在执行完客户端注入后,此时JS上下文全局对象下面,就拥有了这个callNativeFunction的函数对象,就可以正常调用,从而传递数据到Native。
客户端注入
安卓的WebView有一个接口addJavascriptInterface,可以在loadUrl之前提前准备一个对象,通过这个接口注入给JS上下文,从而让JS能够操作,这个操作方式很类似苹果UIWebview JavaScriptCore注入,整个机制也差别不大。
JS调用
在android端注入的对象同样也被挂载在JS上下文全局对象下面,直接访问即可调用。
客户端注入
苹果在开放WKWebView这个性能全方位碾压UIWebView的web组件后,也大幅更改了JS与Native交互的方式,提供了专有的交互APIscriptMessageHandler
需要注意的是如果当前WebView没用了,需要销毁,需要先移除这个对象注入,否则会造成内存泄漏,WebView和所在VC循环引用,无法销毁。
JS调用
这里不像前边两个注入一样,直接注入到JS上下文全局对象里,addScriptMessageHandler方法注入的对象被放到了,全局对象下一个Webkit对象下面。
并且调用方式和之前的两种方法也不同,前两种都可以让js任意操作所注入自定义对象的所有方法,而addScriptMessageHandler注入其实只给注入对象起了一个名字nativeObject,但这个对象的能力是不能任意指定的,只有一个函数postMessage。
前面也简单介绍了一下,JS是一个脚本语言,可以在无需编译的情况下,直接输入字符串JS代码,直接运行执行看结果,这也是为什么在Chrome里,在网页运行的时候打开控制台,可以输入各种JS指令的看结果的。
也就是说当Native想要调用JS的时候,可以由Native把需要数据与调用的JS函数,通过字符串拼接成JS代码,交给WebView进行执行。
Android/iOS-UIWebView /iOS-WKWebView,都支持这种方法,这是目前最广泛运用的方法。
安卓在4.4以前是不能用evaluatingJavaScript 这个方法的,因此之前安卓都用的是webview直接loadUrl,但是传入的url并不是一个链接,而是以”javascript:”开头的js代码,从而达到让webview执行js代码的作用。
对于iOS的WKWebView,除了evaluatingJavaScript,还有WKUserScript这个方式可以执行JS代码,他们之间是有区别的,这个虽然是一种通信方式,但并不能随时随地进行通信。
evaluatingJavaScript 是在客户端调用的时候js端会立刻执行代码。
WKUserScript 是预先准备好JS代码,当WKWebView加载Dom的时候,执行当条JS代码。
在了解了Hybrid app核心的通信方案之后,我们接下来看看目前公司使用最广泛的跨平台技术cordova的运行原理是怎么样的。
这里客户端以Android端为例分析,Android端默认的入口是mainActivity类,我们可以看到它其实继承CordovaActivity类,一切初始化条件是从loadUrl方法开始。
CordovaActivity内依赖一个WebView类,一个Preferences类,一个CordovaInterface接口,并同时初始化一些配置信息。WebView具体实现是由CordovaWebViewImpl类,CordovaInterface接口具体实现是由CordovaInterfaceImpl类实现。
CordovaWebViewImpl是核心类,里面会把一些插件能力初始化,用一个PluginManager进行管理,包含一个引擎类—CordovaWebViewEngine,这个引擎是通过反射的方式创建,自身初始化的时候把NativeToJsMessageQueue关联起来,里面包含着以Js字符串为主的双向链表,把每次从前端通过JS代码存储起来,然后通过绑定的桥接方式Pop出到相应的Native代码中去。
最终实现由SystemWebViewEngine类来对Android系统中WebView控件进行二次包装,这个类的初始化是在CordovaWebViewImpl类反射创建,相关插件和消息传递也是通过SystemWebViewEngine进行绑定。
Android端调用loadUrl后会启动webview加载前端代码,首先会加载运行cordova.js中的代码,在cordova.js中会运行cordova/init模块对cordova进行一个初始化,初始化中主要的核心操作就是:检查监听核心事件是否触发、平台初始化工作、加载插件js。
不同的平台中平台启动处理的模块会有一些差异,但是核心处理相差不大,在Android平台中主要进行了三个处理:初始化通信模块、处理物理按键的事件、在onCordovaReady事件被触发时通知原生端展示webview。
插件js加载处理中主要先会通过load方法加载cordova_plugins.js,获取到项目中用到的插件,然后通过injectScript方法加载插件js,可以看到整个加载过程都是通过添加script标签进行加载的,所以一旦插件数量很多对加载速度会有一定的影响。
这里只是加载插件的js代码,原生端的插件加载并不是在这里进行的。
最后来总结一下整个cordova的启动流程,主要做了三个大的事情:
原生端启动webview加载前端代码。
初始化插件。
建立通信通道。
启动cordova后,在项目运行的过程中当前端需要调用native的能力时,就需要与native端进行通信,Cordova通信流程中主要有这么几点:
Cordova通过在原生端与js端维护两个消息队列来处理消息回调。
Cordova在执行完exec()后,android会马上从消息队列中取出数据同步返回,但不一定就是该次请求的数据。
js端通过轮询获取原生端消息队列中的数据。
Hybrid 的宗旨就是,如果 WebView 本身做不到,或者做起来有很大限制或者性能不佳,那么可以让原生配合,一起做到。
因为Hybrid本是一个面向业务服务的东西,如果业务的野心足够大,WebView 容器的想象空间应该是在能力上与RN/小程序看齐的,WebView 在 Hybrid 的支持下,不单纯是使用Bridge 调用几个原生 API 的事。
我们完全可以拆解RN中的每个环节,把RN号称比 WebView 好的原生渲染/原生组件拆解融入 WebView,我也可以学习小程序保持 WXML/WXSS的开发方式,而非RN那样统一用JSX开发。
这种拆解不是将所有框架优点塞在一个大而全的框架中,各种优化方案的选择背后一定带来的是一些取舍。谁来决定取舍,业务决定,如果自己能深度把握这里面的设计思想,就不用在乎什么新的轮子新的框架,取其设计优点,融入自己的业务之中即可。
------ END ------
作者简介
李同学: 前端研发工程师,目前负责天际移动平台的相关研发工作。
也许您还想看
微前端架构在容器平台的应用
前端数据层落地实践
移动建模平台元数据存储架构演进
AI云店小程序演变之路
基于 Go 的微服务运行情况监控实践