谈到移动端混合式开发,我们很容易想到当下如日中天的React Native(以下简称 RN)或者Cordova 移动端开发框架。其实关于RN以及Cordova的开发实践网上的总结也很多,所以这里不是我介绍的重点。我想介绍的“混合式开发”是将 Cordova 构建的工程作为一个插件库的形式添加到 native 工程中。
问题背景
对于新项目的技术选型来说,没有最完美的,只有最合适的。技术选型过程中我们通常会考虑如下几点:
- 技术本身的成熟度,社区资源
- 项目成员的技术栈
- 项目成本预算
而我们接手的客户项目是一个Android native 的遗留项目,APP功能主要是文本、图片等业务信息显示。团队成员上 Android 开发人员较少,相较而言前端开发人员较多。并且项目上线时间比较紧急。所以团队在预研可行性并评估之后决定采用 Cordova 来做一次混合式开发。
与我们最常认识的混合式开发不同的是,我们的混合开发的是一个模块,并且这个子模块和原有的 Android native 之后有较多的页面交互。所以我们一路摸索开始了这次尝试。
最值得一提的是,由于某些客观因素,我们必须在公司外网构建这个Cordova 工程,然后再集成到内网的 native 应用中。这对开发的影响是,Native 工程和 Cordova 工程在开发过程中都是相互隔离的,集成阶段才会将 Cordova 构建出来的包放置到内网 native 工程中。
核心难点
其实在项目开发过程中 Cordova + Angular + ionic 这一套也有很多可以总结的点。而我则主要将 Cordova 和 Native 工程的沟通桥梁来谈。可能做过RN 或者 Cordova 的人或多或少都知道这个桥梁叫做 “React Context Module” 或者 “Cordova Plugin”。这个桥梁连接了 Native 和 JS 这两个不同世界。
那么在这个开发过程中,我们需要解决哪些问题呢?
- App 集成方式
- 应用基本信息共享
- 用户授权信息管理
- Native 打开指定的 Cordova 页面
- Cordova 打开指定的 Native 页面
解决手段
1. App 集成方式
通常而言,Cordova 打包 Android 平台会产生 APK 文件,然后对外发布 APK 即可。但是为了满足模块化混合开发方式,只需要将 platforms/android/build.gradle
修改为 library 形式即可:
apply plugin: 'com.android.library'
buildscript {
repositories {
mavenCentral()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
}
}
...
通过 cordova build android --release
在 Android 和 CordovaLib 下找到打包完成的 AAR 文件。这个工程的主入口是MainActivity
,我们只需要在 Native 工程中打开 MainActivity
,这样便完成了 Cordova 与 Native 的集成。唯一的改变是MainActivity
在 Cordova 里面算是个主入口,但是集成到 Native 工程中就只能算是一个子模块入口。后期开发只需要更新 AAR 文件即可。
2. 应用基本信息共享
应用基本信息包括,用户名、用户头像、App运行环境(sit/uat/pro)等。按照第一点集成的流程,在打开 Cordova 主入口的时候可以传入用户的基本信息。主入口的 Activity 获取到基本信息之后,JS 中通过插件的形式从 Activity 中获取,就完成了这次传递。
public class DataPlugin extends CordovaPlugin {
// Do not modify the string variable!!
private static final String ACTION_GET_CONFIG = "getConfig";
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
try {
if (ACTION_GET_CONFIG.equals(action)) {
MainActivity mainActivity = (MainActivity) cordova.getActivity();
String host = mainActivity.getHost();
String language = mainActivity.getLanguage();
String currentUser = mainActivity.getCurrentUser();
JSONObject configJsonObject = new JSONObject();
configJsonObject.put("host", host);
configJsonObject.put("language", language);
configJsonObject.put("username", currentUser);
callbackContext.success(configJsonObject);
return true;
} else {
callbackContext.error("Invalid action");
return false;
}
} catch (Exception e) {
callbackContext.error(e.getMessage());
return true;
}
}
}
比如上面的 DataPlugin
就将 username
, language
, host
等信息暴露给 JS 端。从而实现了这种基本信息的共享。
3. 用户授权信息管理
用户授权管理包括:登录、权限过期、注销等主要场景。用户进入 Cordova 模块中,不应该再次要求用户登录,所以只需要将 Native 中用户登录产生的 Token 或者 Session 等信息通过上述手段共享给 Corodva 层,然后 JS 中按需使用即可。
授权过期的问题,比如我们发现每 30 分钟 token 都会失效。这时候就需要 JS 中进行判断,一旦出现权限问题,就通过“广播插件”发送App广播,Native中注册token失效监听逻辑被触发,重新进入 Cordova 子模块,从而实现刷新 token 的效果。
注销登录,和授权过期的处理类似,通过局部广播调用 native 层的注销登录逻辑,然后退出当前应用,完成注销操作。
4. Native 打开指定的 Cordova 页面
Native 打开指定的 Cordova 页面,还是通过页面信息共享的方式传入参数。在 JS 工程初始化完成,通过 DataPlugin
读取页面跳转参数之后,通过前端的路由进行相关跳转。
一般而言,Native 打开 H5 前端页面,比较常见于接收到推送通知,用户点击跳转的逻辑。需要 Native 和 Corodva 约定跳转数据格式。我比较习惯于采用 Restful 的模式指定页面路由进行跳转。
5. Cordova 打开指定的 Native 页面
在 H5 页面中打开 Naitve 的页面,场景需求还比较多。比如 H5 页面中有需要直接复用原有 Native 页面的逻辑。或者类似于选择手机相册的功能,前端时间效果不好的时候,可以考虑使用 Native 页面实现。Corodva 前端页面打开 Native 的工程还是通过 CordovaPlugin 实现。
private CallbackContext callbackContext;
@TargetApi(Build.VERSION_CODES.CUPCAKE)
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
this.callbackContext = callbackContext;
if (ACTION_APP_EXIT.equals(action)) {
cordova.getActivity().finish();
callbackContext.success();
return true;
}
if (ACTION_OPEN_MY_SETTING.equals(action)) {
Intent intent = new Intent();
intent.setComponent(new ComponentName(getApplicationId(), MAIN_APP_PACKAGE_NAME + ".activity.MeDetailActivity"));
cordova.startActivityForResult(this, intent, REQUEST_CODE_GALLERY);
return true;
}
if (ACTION_OPEN_GALLERY.equals(action)) {
Intent galleryIntent = new Intent();
galleryIntent.setComponent(new ComponentName(getApplicationId(), MAIN_APP_PACKAGE_NAME + ".activity.reception.GalleryChooserActivity"));
cordova.startActivityForResult(this, galleryIntent, REQUEST_CODE_GALLERY);
return true;
}
return false;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_GALLERY) {
String[] imageUris = intent.getStringArrayExtra("galleryImages");
callbackContext.success(parseJsonArrayFromStrings(imageUris));
return;
}
}
super.onActivityResult(requestCode, resultCode, intent);
}
虽然是通过 CordovaPlugin 开发,Native 中能实现的 startActivity
, startActivityForResult
也都可以轻松实现。
经验总结
1. 善于利用 Native 中的资源
在混合开发中,由于 Cordova Plugin 基本能够无缝的和原生页面进行交互,所以如果 Native 模块已经有的功能尽量不要在 H5 中重复添加,页面或者数据尽量的共用,降低维护成本。
除了资源共用问题,我们还需要多利用 Native 的优势,比如 Native 优于 H5 的性能。对于某些复杂的需求,比如读取手机相册、加载大几百条列表数据,应该考虑到 H5 可能会存在性能问题,这时候不妨试试用 Native 实现这个页面。
2. 模块依赖过深时尝试使用反射
如果你的两个模块在不同的工程中,相互引用不太方便。但是在页面间传数据的时候, Cordova 又不得不引用 Native 中的某个 model 类。这时候尝试使用反射调用来规避这种限制。当然这样之后,一旦 Native 中的文件需要重命名或者更改路径,都必须在 Cordova 插件中同步修改。
3. 使用本地广播进行消息传递
Cordova H5 工程中如果出现某些变更需要及时通知 Native 层,比如切换 App 语言、更新用户名或者头像等。可以借用广播这种模式来传递。因为在插件中能够轻易地引用 Android 相关的库,但是调用 Native 工程自定义的库或者类比较困难。所以广播在这种场景中效果相当方便。
4. 使用基础数据类型在页面间传递数据
由于在 Cordova 工程中直接引用 Native 工程中某个类比较的困难,所以在可以选择的情况下,页面之前的数据传递尽量越简单越好。而且尽量使用基础数据类型,对于一些结构化的数据可以使用 json 字符串传输。
5. 警惕 Cordova 首次加载速度问题
Cordova 本质上是一个 WebView,直接加载打包好的前端 index.html 页面。在项目中发现应用启动后,首次进入 Cordova 模块的时间稳定在 3-5 秒。如果 Cordova 对应的页面是应用的主入口,这个时间倒没什么不妥。但是对于混合开发中,进入一个二级子页面,却耗时 3-5 秒,的确是一个令人头疼的事情,如果这时候客户对子页面的响应速度要求过高。这个或许是一个最大的风险。Cordova 启动耗时主要是 Cordova 插件加载以及前端 JS 框架的加载。我们项目中有尝试过优化,但是效果比较一般。
6. 用户体验问题
原生和非原生的用户体验相差的确很多,在 Cordova 上这个问题体验尤其明显。比如项目中有个几百人的联系人列表,在 Native 上使用列表组件流畅无比,但是如果在 H5 上绘制几百个 DIV,并且滑动过程中Dom元素很难做到自动回收。使用效果就不尽如人意。所以对于某些问题突出的页面可以直接用 native 代替也未尝不是一种解决思路。
结语
混合式开发模式,能够在体验 Hybrid 开发移动应用的同时,也可以最大限度的利用 Native 的一些优势,做到效率和用户体验之间的平衡。其中有优点也有缺点,需要我们根据项目实际情况选择适合当前的。