前言
来新公司的第一个任务,研究hybrid App中间层实现原理,做中间层插件开发。这个任务挺有意思,也很有挑战性,之前在DCloud虽然做过5+ App开发,但是中间层的东西确实涉及不多。本系列文章属于系列开篇cordova学习笔记,本文主要是从零开始搭建一个cordova工程,并了解cordova开发的基本内容。
创建第一个App
Apache Cordova是一个开源的移动开发框架。允许你用标准的web技术-HTML5,CSS3和JavaScript做跨平台开发。 应用在每个平台的具体执行被封装了起来,并依靠符合标准的API绑定去访问每个设备的功能,比如说:传感器、数据、网络状态等。
Cordova官网:http://cordova.apache.org/
Cordova中文网:http://cordova.axuer.com/
Cordova中文站:http://www.cordova.org.cn/
1.安装 Cordova CLI
npm install -g cordova
安装完成可以通过cordova -v
查看版本号,本文是在V6.5.0下构建。
2.新建项目
cordova create <PATH> [ID [NAME [CONFIG]]] [options]
Create a Cordova project:
- PATH —— 项目路径
- ID —— app 包名 - used in <widget id>
- NAME —— app 名称
- CONFIG —— 配置文件地址 json string whose key/values will be included in [PATH]/.cordova/config.json
Options:
- --template=<PATH|NPM PACKAGE|GIT URL> ... use a custom template located locally, in NPM, or GitHub.
- --copy-from|src=<PATH> .................. deprecated, use --template instead.
- --link-to=<PATH> ........................ symlink to custom www assets without creating a copy.
Example:
cordova create hello-cordova io.zhaomenghuan HelloCordova
这将会为你的cordova应用创造必须的目录。默认情况下,cordova create命令生成基于web的应用程序的骨骼,项目的主页是 www/index.html 文件。
3.添加平台
所有后续命令都需要在项目目录或者项目目录的任何子目录运行:
cd hello-cordova
给你的App添加目标平台。我们将会添加ios
和android
平台,并确保他们保存在了config.xml中:
cordova platform add ios --save
cordova platform add android --save
运行add
或者remove
平台的命令将会影响项目platforms
的内容,在这个目录中每个指定平台都有一个子目录。
注意:在你使用CLI创建应用的时候, 不要 修改/platforms/目录中的任何文件。当准备构建应用或者重新安装插件时这个目录通常会被重写。
检查你当前平台设置状况:
cordova platform is
Installed platforms:
android 6.1.2
Available platforms:
amazon-fireos ~3.6.3 (deprecated)
blackberry10 ~3.8.0
browser ~4.1.0
firefoxos ~3.6.3
webos ~3.7.0
windows ~4.4.0
wp8 ~3.8.2 (deprecated)
安装构建先决条件:
要构建和运行App,你需要安装每个你需要平台的SDK。另外,当你使用浏览器开发你可以添加 browser平台,它不需要任何平台SDK。
检测你是否满足构建平台的要求:
cordova requirements
Requirements check results for android:
Java JDK: installed 1.8.0
Android SDK: installed true
Android target: installed android-7,android-8,android-9,android-10,android-11,android-12,android-13,android-14,android-15,android-16,android-17,android-18,android-19,android-20,android-21,android-22,android-23,android-24,android-25
Gradle: installed
初次使用我们可能会遇到下面的报错:
Error: Failed to find 'ANDROID_HOME' environment variable. Try setting setting it manually.
Failed to find 'android' command in your 'PATH'. Try update your 'PATH' to include path to valid SDK directory.
这是因为我们没有配置环境变量:
- 设置JAVA_HOME环境变量,指定为JDK安装路径
- 设置ANDROID_HOME环境变量,指定为Android SDK安装路径
- 添加Android SDK的tools和platform-tools目录到你的PATH
对于android平台下的环境配置在这里不再赘述,具体可以参考:
4.构建App
默认情况下, cordova create生产基于web应用程序的骨架,项目开始页面位于www/index.html
文件。任何初始化任务应该在www/js/index.js文件中的deviceready事件的事件处理函数中。
运行下面命令为所有添加的平台构建:
cordova build
你可以在每次构建中选择限制平台范围 - 这个例子中是android
:
cordova build android
注意:首次使用时,命令行提示 Downloading https://services.gradle.org/distributions/gradle-2.14.1-all.zip
,是在下载对应的gradle并自动解压安装,根据网络状况,可能耗时极长,且容易报错。
使用Cordova编译Android平台程序提示:Could not reserve enough space for 2097152KB object heap。
Error occurred during initialization of VM
Could not reserve enough space for 2097152KB object heap
大体的意思是系统内存不够用,创建VM失败。试了网上好几种方法都不行,最后这个方法可以了:
开始->控制面板->系统->高级设置->环境变量->系统变量
新建变量:
变量名: _JAVA_OPTIONS
变量值: -Xmx512M
5.运行App
我们有多种方式运行我们的App,在不同场景下使用不同的方式有助于我们快速开发和测试我们的应用。
在命令行运行下面的命令,会重新构建App并可以在特定平台的模拟器上查看:
cordova emulate android
你可以将你的手机插入电脑,在手机上直接测试App:
cordova run android
在进行打包操作前,我们可以通过创建一个本地服务预览app UI,使用指定的端口或缺省值为8000运行本地Web服务器www/assets。访问项目:http://HOST_IP:PORT/PLATFORM/www。
cordova serve [port]
参考文档:
6.安装插件
cordova的强大之处在于我们可以通过安装插件,拓展我们web工程的能力,比如调用系统底层API来调用设备上的底层功能,如摄像头、相册。通过cordova plugin
命令实现插件管理。
可以在这里搜索需要的插件:Cordova Plugins 。
cordova {plugin | plugins} [
add <plugin-spec> [..] {--searchpath=<directory> | --noregistry | --link | --save | --browserify | --force} |
{remove | rm} {<pluginid> | <name>} --save |
{list | ls} |
search [<keyword>] |
save |
]
添加插件:
cordova plugin add <plugin-spec> [...]
移除插件:
cordova plugin remove [...]
7.平台为中心的工作流开发App
上面我们是在跨平台(CLI)的工作流进行,原则上如果我们不需要自己写原生层自定义组件,我们完全可以只在CLI上完成我们的工作,当然如果需要进一步深入了解cordova native与js的通信联系,我们需要切换到平台为中心的工作流,即将我们的cordova工程导入到原生工程。例如:我们可以使用android studio导入我们新建的cordova工程。
自定义插件开发
官方推荐的插件遵循相同的目录结构,根目录下是plugin.xml
配置文件,src目录下放平台原生代码,www下放js接口代码,基本配置方法和代码结构由一定规律,我们使用plugman可以生成一个插件模板,改改就可以写一个自定义插件。
1.安装 plugman ,使用 plugman 创建插件模板
npm install -g plugman
比如这里我们创建一个nativeUI的插件:
plugman create --name NativeUI --plugin_id cordova-plugin-nativeui --plugin_version 0.0.1
参数介绍:
pluginName: 插件名字:NativeUI
pluginID: 插件id : cordova-plugin-nativeui
oversion: 版本 : 0.0.1
directory:一个绝对或相对路径的目录,该目录将创建插件项目
variable NAME=VALUE: 额外的描述,如作者信息和相关描述
进入插件目录
cd NativeUI
给 plugin.xml 增加Android平台
plugman platform add --platform_name android
生成的插件文件结构为:
NativeUI:
├── src
└── android
└── NativeUI.java
├── www
└── NativeUI.js
└── plugin.xml
2.修改配置文件
plugin.xml
文件字段含义:
元素 | 描述 |
---|---|
plugin | 定义命名空间,ID和插件版本。应该用定义在http://apache.org/cordova/ns/...命名空间。plugin的ID在输入cordova plugins命令时在插件列表中显示。 |
name | 定义插件的名字。 |
description | 定义插件的描述信息。 |
author | 定义插件作者的名字。 |
keywords | 定义与插件相关的关键字。Cordova研发组建立了公开、可搜索的插件仓库,添加的关键字能在你把插件提交到仓库后帮助被发现。 |
license | 定义插件的许可。 |
engines | 用来定义插件支持的Cordova版本。再添加engine元素定义每个支持的Cordova版本。 |
js-module | 指js文件名,而这个文件会自动以<script >标签的形式添加到Cordova项目的起始页。通过在js-module中列出插件,可以减少开发者的工作。 |
info | 它是另一个除了description外说插件信息的地方。 |
<?xml version='1.0' encoding='utf-8'?>
<plugin id="cordova-plugin-nativeui" version="0.0.1" xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
<name>NativeUI</name>
<js-module name="NativeUI" src="www/NativeUI.js">
<clobbers target="agree.nativeUI" />
</js-module>
<platform name="android">
<config-file parent="/*" target="res/xml/config.xml">
<feature name="NativeUI">
<param name="android-package" value="cn.com.agree.nativeui.NativeUI" />
</feature>
</config-file>
<config-file parent="/*" target="AndroidManifest.xml"></config-file>
<source-file src="src/android/NativeUI.java" target-dir="src/cn/com/agree/nativeui" />
</platform>
</plugin>
这个配置文件有几个地方很关键,一开始没有认真看,将插件导进工程跑的时候各种问题,十分头痛,不得不重新认真看看plugin.xml文档。
- id:原则上没有严格规定,参考官方插件写法,这里我也写的是
cordova-plugin-nativeui
,通过plugman创建插件模板的时候需要指定。 - name:插件名称。
- clobbers->target:用于指定插入module.exports的窗口对象下的命名空间,也就是用户调用该插件时的js层暴露的顶层对象。这个很关键,虽然可以任意指定,但是涉及到我们调用插件的属性或者方法,所以需要特别关注。plugman默认生成的是将id中的
-
转换成`.'的对象。这里需要说明的是我们可以写多个js-module,每个js-module下可以指定不同的clobbers。 - feature -> param - > value 标识了实际提供服务的Native类别名称,这里直接定位至具体类,然而上述通过plugman生成模板的时候中没有指定NativeUI的包名,会自定生成
cordova-plugin-nativeui.NativeUI
,这里我们需要改成符合自己要求的类名,如我这里使用公司的域名:cn.com.agree.nativeui.NativeUI
。需要说明的是这里的类名可以与插件名称不同。 - source-file -> target-dir 同理
target-dir
需要修改为:src/cn/com/agree/nativeui
,同时需要修改平台下的native部分的代码:如:package cn.com.agree.nativeui;
- platform -> config-file下可以指定程序所需的权限
uses-permission
,如:
<config-file target="AndroidManifest.xml" parent="/*">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</config-file>
3.导入到平台工程中的目录结构
这里我们以android平台为例:
cordova.js在创建Android工程的时候,是从cordova的lib目录下Copy到platforms\android\assets\www\cordova.js
的。同时备份到platforms\android\platform_www\cordova.js
。下一篇文章我会试着读一下cordova.js的源码,这里对cordova.js暂不做深入探究。
这里我们主要关心几个地方,我们的原生代码在src目录下,assets/www目录下是我们的web 程序。www目录下的plugins文件夹就是我们的插件js部分,cordova_plugins.js是根据plugins文件夹的内容生成的。
cordova_plugins.js的整体结构:
cordova.define('cordova/plugin_list', function(require, exports, module) {
module.exports = [
{
"id": "cordova-plugin-nativeui.NativeUI",
"file": "plugins/cordova-plugin-nativeui/www/NativeUI.js",
"pluginId": "cordova-plugin-nativeui",
"clobbers": [
"agree.nativeUI"
]
},
...
];
module.exports.metadata =
// TOP OF METADATA
{
"cordova-plugin-nativeui": "0.0.1",
...
};
// BOTTOM OF METADATA
});
Android插件开发指南
Android插件基于Cordova-Android,它是基于具有Javscript-to-native桥接的Android WebView构建的。 Android插件的本机部分至少包含一个扩展CordovaPlugin类的Java类,并重写其一个执行方法。
插件类映射
插件的JavaScript接口使用cordova.exec方法,如下所示:
cordova.exec(<successFunction>, <failFunction>, <service>, <action>, [<args>]);
-
function(winParam) {}
: 成功回调 -
function(error) {}
: 错误回调 -
service
: 原生层服务名称 -
action
: js层调用方法名 -
[args]
: js层传递到原生层的数据
这将WebView的请求传递给Android本机端,有效地在服务类上调用action方法,并在args数组中传递其他参数。无论您将插件分发为Java文件还是作为自己的jar文件,必须在Cordova-Android应用程序的res / xml / config.xml文件中指定该插件。 有关如何使用plugin.xml文件注入此要素的详细信息,请参阅应用程序插件:
<feature name="<service_name>">
<param name="android-package" value="<full_name_including_namespace>" />
</feature>
插件初始化及其生命周期
一个插件对象的一个实例是为每个WebView的生命创建的。 插件不会被实例化,直到它们被JavaScript的调用首次引用为止,除非在config.xml中将具有onload name属性的<param>设置为“true”。
<feature name="Echo">
<param name="android-package" value="<full_name_including_namespace>" />
<param name="onload" value="true" />
</feature>
插件使用 initialize 初始化启动:
@Override
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
super.initialize(cordova, webView);
// your init code here
}
插件还可以访问Android生命周期事件,并可以通过扩展所提供的方法(onResume,onDestroy等)来处理它们。 具有长时间运行请求的插件,媒体播放,侦听器或内部状态等背景活动应实现onReset()方法。 当WebView导航到新页面或刷新时,它会执行,这会重新加载JavaScript。
编写Android Java插件
一个JavaScript调用触发对本机端的插件请求,并且相应的Java插件在config.xml文件中正确映射,但最终的Android Java Plugin类是什么样的? 使用JavaScript的exec函数发送到插件的任何东西都被传递到插件类的execute方法中。
插件的JavaScript不会在WebView界面的主线程中运行; 而是在WebCore线程上运行,执行方法也是如此。 如果需要与用户界面进行交互,应该使用Activity的runOnUiThread方法。
如果不需要在UI线程上运行,但不希望阻止WebCore线程,则应使用cordova.getThreadPool()
获得的Cordova ExecutorService
执行代码。
...
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if (action.equals("toast")) {
this.toast(args.getString(0));
return true;
}
return false;
}
/**
* Builds and shows a native Android toast with given Strings
*
* @param message The message the toast should display
*/
private void toast(final String message) {
final CordovaInterface cordova = this.cordova;
if (message != null && message.length() > 0) {
final int duration = Toast.LENGTH_SHORT;
Runnable runnable = new Runnable() {
public void run() {
Toast toast = Toast.makeText(cordova.getActivity().getApplicationContext(), message, duration);
toast.show();
}
};
cordova.getActivity().runOnUiThread(runnable);
}
}
...
js部分的代码:
var exec = require('cordova/exec');
module.exports = {
toast: function(message) {
exec(null, null, 'NativeUI', 'toast', [message]);
}
}
callbackContext.success
可以将原生层字符串作为参数传递给JavaScript层的成功回调,callbackContext.error
可以将给JavaScript层的错误回调函数传递参数。
添加依赖库
如果你的Android插件有额外的依赖关系,那么它们必须以两种方式之一列在plugin.xml中:
- 首选的方法是使用<framework />标签(有关详细信息,请参阅插件规范)。以这种方式指定库可以通过Gradle的依赖管理逻辑来解决。这允许诸如gson,android-support-v4和google-play-services之类的常用库被多个插件使用而没有冲突。
- 第二个选项是使用<lib-file />标签来指定jar文件的位置(有关更多详细信息,请参阅插件规范)。 只有当您确定没有其他插件将依赖于您所引用的库(例如,该库特定于您的插件)时,才应使用此方法。 否则,如果另一个插件添加了相同的库,则可能导致插件用户造成构建错误。 值得注意的是,Cordova应用程序开发人员不一定是本地开发人员,因此本地平台构建错误可能特别令人沮丧。
Android集成
Android具有Intent系统,允许进程相互通信。插件可以访问CordovaInterface对象,可以访问运行应用程序的Android Activity。 这是启动新的Android Intent所需的上下文。 CordovaInterface允许插件为结果启动Activity,并为Intent返回应用程序时设置回调插件。
从Cordova 2.0开始,插件无法再直接访问上下文,并且旧的ctx成员已被弃用。 所有的ctx方法都存在于Context中,所以getContext()和getActivity()都可以返回所需的对象。
运行权限(Cordova-Android 5.0.0+)
Android 6.0 "Marshmallow" 引入了新的权限模型,用户可以根据需要启用和禁用权限。这意味着应用程序必须将这些权限更改处理为将来,这是Cordova-Android 5.0.0发行版的重点。
就插件而言,可以通过调用权限方法来请求权限,该签名如下:
cordova.requestPermission(CordovaPlugin plugin, int requestCode, String permission);
为了减少冗长度,将此值分配给本地静态变量是标准做法:
public static final String READ = Manifest.permission.READ_CONTACTS;
定义requestCode的标准做法如下:
public static final int SEARCH_REQ_CODE = 0;
然后,在exec方法中,应该检查权限:
if(cordova.hasPermission(READ)) {
search(executeArgs);
} else {
getReadPermission(SEARCH_REQ_CODE);
}
在这种情况下,我们只需调用requestPermission:
protected void getReadPermission(int requestCode) {
cordova.requestPermission(this, requestCode, READ);
}
这将调用该活动并引起提示出现要求该权限。 一旦用户拥有权限,结果必须使用onRequestPermissionResult方法处理,每个插件应该覆盖该方法。 一个例子可以在下面找到:
public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException {
for(int r:grantResults) {
if(r == PackageManager.PERMISSION_DENIED) {
this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR));
return;
}
}
switch(requestCode) {
case SEARCH_REQ_CODE:
search(executeArgs);
break;
case SAVE_REQ_CODE:
save(executeArgs);
break;
case REMOVE_REQ_CODE:
remove(executeArgs);
break;
}
}
上面的switch语句将从提示符返回,并且根据传入的requestCode,它将调用该方法。 应该注意的是,如果执行不正确地处理权限提示可能会堆叠,并且应该避免这种情况。
除了要求获得单一权限的权限之外,还可以通过定义权限数组来请求整个组的权限,如同Geolocation插件所做的那样:
String [] permissions = {
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
};
然后当请求权限时,需要完成的所有操作如下:
cordova.requestPermissions(this, 0, permissions);
这将请求数组中指定的权限。 提供公开访问的权限阵列是一个好主意,因为可以使用插件作为依赖关系使用,尽管这不是必需的。
启动其他活动
如果你的插件启动将Cordova活动推送到后台的活动,则需要特别考虑。 如果设备运行内存不足,Android操作系统将在后台销毁活动。在这种情况下,CordovaPlugin实例也将被销毁。 如果您的插件正在等待其启动的活动的结果,则当Cordova活动返回到前台并获得结果时,将创建一个新的插件实例。 但是,插件的状态不会自动保存或恢复,插件的CallbackContext将丢失。 CordovaPlugin可以实现两种方法来处理这种情况:
/**
* Called when the Activity is being destroyed (e.g. if a plugin calls out to an
* external Activity and the OS kills the CordovaActivity in the background).
* The plugin should save its state in this method only if it is awaiting the
* result of an external Activity and needs to preserve some information so as
* to handle that result; onRestoreStateForActivityResult() will only be called
* if the plugin is the recipient of an Activity result
*
* @return Bundle containing the state of the plugin or null if state does not
* need to be saved
*/
public Bundle onSaveInstanceState() {}
/**
* Called when a plugin is the recipient of an Activity result after the
* CordovaActivity has been destroyed. The Bundle will be the same as the one
* the plugin returned in onSaveInstanceState()
*
* @param state Bundle containing the state of the plugin
* @param callbackContext Replacement Context to return the plugin result to
*/
public void onRestoreStateForActivityResult(Bundle state, CallbackContext callbackContext) {}
总结
cordova 是否能够发挥出它出彩的一面还是源于我们对原生的熟练程度,只有对原生足够熟练,对cordova的运行机制足够熟悉才能做出一个相对比较令人满意的App,后面的文章我会尝试阅读cordova的源码,深入解析cordova的实现原理和插件机制,也会教大家封装一些常用的自定义组件。本文内容基本取材于官方文档,只是借助谷歌翻译以及自己在探索过程中的一些问题,做了一些增删,如果有任何问题,希望各位不吝指教。
写文章不容易,也许写这些代码就几分钟的事,写一篇大家好接受的文章或许需要几天的酝酿,然后加上几天的码字,累并快乐着。如果文章对您有帮助请我喝杯咖啡吧!
转载需标注本文原始地址:https://zhaomenghuan.github.io/
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。