Apache Cordova Android原理应用实例详解
莱昂晨 人气:0前言
从原理到应用复盘一下自己做过的所有项目,希望能让我自己过两年仍然能看懂现在写的代码哈哈。在项目里我只负责了Android的开发包括插件开发和集成,ios没摸到,有机会也得自己玩下,但是这篇文章不会接触。
技术选型
现在Hybrid混合开发框架很多,Apche Cordova/React Native/Flutter等等等等。然而公司需求是2018年6月提的,Flutter 1.0在半年之后才出生,所以当时团队TL比较了RN与Cordova后基于以下几点选择了Cordova。
- 学习成本低,项目周期比较紧,只有1个月就要发版本,团队内没有熟悉RN的成员,学习成本低开发时间短非常重要
- 技术成熟,文档多不容易踩坑(当然现在RN也很成熟,但是当时RN的人确实没有很多,生态不成熟)
我们的应用跑在一台定制的Android终端,性能不是很好,在最后某些js动画场景里,以及一些js-native的调用里卡的不行,最后优化了下,效果也算还不错。这里建议如果对性能要求很高的项目,一定慎重考虑Cordova这种webview方案
项目最后选择了React + Mirrorx + Cordova的方案(当时有Dva, 但是由于Dva没有英文文档,而我们的项目是需要美国分团队共同维护的,所以选择了Mirrorx)
技术原理
架构图
本质上其实就是往app里面塞一个webview,通过file协议, 本地加载index.html
那么这里会有三个问题
- 如何本地加载url对应的资源
- webview如何使用js调用app原生api(JSBridge)
- app原生api如何回调webview中的js
- 多个plugin的情况下,cordova是如何通过cordova.exec(...’pluginName’...)定位到相应的plugin的 以下带着问题一个个进行解释
1. 如何本地加载url对应的资源
我们项目中通过eject暴露Webapck配置, 配置打包路径直接打进www文件夹。 通过Android的webview.loadUrl方法通过file协议load本地index.html。在Google inject中也可以看到对应的url. 对应的cordova代码如下
public class CordovaViewTestActivity extends Activity implements CordovaInterface { CordovaWebView cwv; /* Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); cwv = (CordovaWebView) findViewById(R.id.tutorialView); cwv.loadUrl("file:///android_asset/www/index.html"); }
2. webview如何使用js调用app原生api
2.1 通过addJavascriptInterface 及 @JavascriptInterface实现
2.2 通过WebClientChrome中的三个方法onJsAlert, onJsConfirm, onJsPrompt实现
当chromium webkit内核的webview调用window.alert, window.confirm, window.prompt方法时,对应的WebClientChrome的那三个方法同样会被执行
在cordova中会首先判断有没有设置navtiveApi
var nativeApi = this._cordovaNative || require('cordova/android/promptbasednativeapi');
在Android部分
var msgs = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson); // If argsJson was received by Java as null, try again with the PROMPT bridge mode. // This happens in rare circumstances, such as when certain Unicode characters are passed over the bridge on a Galaxy S2. See CB-2666. if (jsToNativeBridgeMode == jsToNativeModes.JS_OBJECT && msgs === "@Null arguments.") { androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT); androidExec(success, fail, service, action, args); androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT); } else if (msgs) { messagesFromNative.push(msgs); // Always process async to avoid exceptions messing up stack. nextTick(processMessages); }
如果有nativeApi即设备支持@JavascriptInterface注解, 则走addJavascriptInterface,否则走onJsPrompt
3. app原生api如何回调webview中的js
js端生成callback function id,传给native, native需要回调js时,回调对应的callbackId给js
private String callbackId; // 在js端生成,保存在native端 private CordovaWebView webView; protected boolean finished; //这个callback是否结束,如果结束在js端(cordova.js)就会把这个callbackid从列表中删掉,否则这个callback将一直存在,也就是说明你可以用这个callbackContext一直和js保持通信
回调方式:
/** Uses webView.loadUrl("javascript:") to execute messages. */ public static class LoadUrlBridgeMode extends BridgeMode {
/** Uses webView.evaluateJavascript to execute messages. */ public static class EvalBridgeMode extends BridgeMode {
public static class OnlineEventsBridgeMode extends BridgeMode
4. 多个plugin的情况
多个plugin的情况下,cordova是如何通过cordova.exec(...’pluginName’...)定位到相应的plugin的
当Cordova框架启动时候,CordovaActivity类中的onCreate方法调用loadUrl方法。在第一次loadUrl方法时,就会去初始化PluginManager并加载plugin,PluginManager加载plugin,将plugin的Class名字保存到一个hashmap中,用service名字作为key值。当JS端通过JavascriptInterface接口的SystemExposedJsApi对象请求Android时,PluginManager会从hashmap中查找到plugin,如果该plugin还未实例化,利用java反射机制实例化该plugin,并执行plugin的execute方法。
关于踩到的坑
1. 打包路径配置问题
cordova build的时候会默认使用项目www目录作为资源目录,打包进assets中。 项目工期紧,直接用了cra创建的项目,我们使用eject,再修改打包路径(实际当时有其他插件可以暴露打包路径配置)。
// webpack.config.prod.js module.exports = { ... output: { ... // The build folder. path: resolveApp('www'), } }
2. success不回调问题
场景: 我们项目集成其他项目组的自研plugin的功能后,用户输入device ID和divice Name后,点击connect按钮,就可以通过mobile连接上其他项目组的硬件设备
前端调用test.connect(deviceID, deviceName, callback), 其中callback的逻辑是接收并判断java端传回的message。在连接上device后, java使用contextCallback.success(”success”)来进行回调 ,callback接收到message为success, 就更改mobile端我们前端的相应状态为连接成功
第一次连接之后,即java调用了contextCallback.success(“success”), 然后js中调用了callback(‘success’) ,项目页面显示已连接, 断开后,第二次再次调用contextCallback.success(“success”)无法正常连接,无法正常调用js这边的callback
原因:
finished一开始没有初始化,走下面的else,被赋值为true, 然后调用webView.sendResult,第二次调用就直接return了。
解决方案:keepCallback (当时官方文档没有提到callback只能调一次的问题,翻源码才看到)
加载全部内容