cordova 研习笔记(一) —— 初试牛刀之 cordova.js 概要
前言
来新公司的第一个任务,研究 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/plugins/1.0命名空间。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 层调用方法名
- [arguments]: 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 属性的
设置为“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 中:
- 首选的方法是使用
标签(有关详细信息,请参阅插件规范)。以这种方式指定库可以通过 Gradle 的依赖管理逻辑来解决。这允许诸如 gson,android-support-v4 和 google-play-services 之类的常用库被多个插件使用而没有冲突。 - 第二个选项是使用
标签来指定 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 的实现原理和插件机制,也会教大家封装一些常用的自定义组件。本文内容基本取材于官方文档,只是借助谷歌翻译以及自己在探索过程中的一些问题,做了一些增删,如果有任何问题,希望各位不吝指教。
写这些代码也许就一两个小时的事,写一篇大家好接受的文章需要几天的酝酿,如果文章对您有帮助请我喝杯咖啡吧!
