创建Cordova插件
本文承接上篇《使用Cordova API开发(下)》。
前面讨论的工具和插件都是Cordova框架一部分,但如果框架缺少相应的插件可以自已构建。
3.0以后由plugman和CLI提供的功能让插件有所改变。接下来将会讨论如何创建只有js的插件,还有Android的Native插件,其他平台的构建过程基本也是一样的。
剖析Cordova插件
在构建之前先解析下插件的结构。Cordova有大量的关于如何构建插件的文档。"Plugin Development Guide"说明了如何为插件创建js接口,还有为每个移动平台创建native插件组件各自的指南。
Cordova插件是一组扩展或提升Cordova应用功能的文件。开发者向项目中添加插件,通过提供的js接口和插件交互。插件中包括一个叫plugin.xml的配置文件,一个或多个js文件,加上一些native代码文件(可选),库文件,和相关的内容(HTML, CSS和其他内容文件)。
plugin.xml文件描述了插件并且告诉CLI复制哪些部分的文件,这些文件在每个平台上都不一样。plugin.xml中还有配置,由CLI用来设置平台方面的config.xml配置。plugin.xml文件中有许多可用选项。
一个插件中至少有一个js源文件,用来定义插件公开的方法、对象和属性。可以把所有的都封装到一个文件或多个。也可以把其他js类库打包(如jQuery)。
除了上述两种文件,剩下的文件主要用来定义插件。一般来说,插件要有每个支持平台的一个或多个native源码文件。它们上面还包括其他native文件(源码或预编译的)或内容(图像文件,样式表,HTML文件)。
构建插件的一个好参考是有大量的可用的示例。项目中下载的插件是zip包,可以解压并分析如何构建。一个方法是修改现有插件来满足需求。
创建一个简单的插件
Cordova插件不一定要有native代码,可以完全由js代码组成。在写native插件之前开始写个简单的插件用来说明插件结构和格式。
给下面这个例子中的插件起名叫"Meaning of Life"。创建插件前首先创建一个叫"mol"的文件夹(Meaning of Life"的缩写),在其中创建一个描述插件的plugin.xml文件,代码如下,它是由其他插件的plugin.xml复制过来修改的。
<?xml version="1.0" encoding="UTF-8"?> <plugin xmlns="http://apache.org/cordova/ns/plugins/1.0" id="com.learningplugin.meaningoflife" version="1.0.0"> <name>mol</name> <description>Calculates the meaning of the life</description> <author>Zhang San</author> <keywords>mol, meaning of life</keywords> <license>Apache 2.0 License</license> <engines> <engine name="cordova" version="5.0.0" /> </engines> <js-module src="mol.js" name="mol"> <clobbers target="mol" /> </js-module> <info>This plugin exists to demonstrate how to build a simple, Javascript-Only plugin for Apache Cordova.</info> </plugin>
plugin.xml中一些内容作为文档说明作者和目的。 其他的用于驱动plugman或Cordova CLI插件安装过程。下表列出不同的plugin.xml元素。
元素 | 描述 |
---|---|
plugin | 定义命名空间,ID和插件版本。应该用定义在*http://apache.org/cordova/ns/plugins/1.0*命名空间。plugin的ID在输入*cordova plugins*命令时在插件列表中显示。 |
name | 定义插件的名字。 |
description | 定义插件的描述信息。 |
author | 定义插件作者的名字。 |
keywords | 定义与插件相关的关键字。Cordova研发组建立了公开、可搜索的插件仓库,添加的关键字能在你把插件提交到仓库后帮助被发现。 |
license | 定义插件的许可。例中是Cordova默认的许可,还可以是许可描述或许可期限的链接。 |
engines | 用来定义插件支持的Cordova版本。再添加*engine*元素定义每个支持的Cordova版本。 |
js-module | 指js文件名,而这个文件会自动以`<script`>标签的形式添加到Cordova项目的起始页。通过在*js-module*中列出插件,可以减少开发者的工作。*clobbers*元素说明了*module.exports*自动添加到*window*对象,让插件方法能够在窗口级别使用。 |
info | 它是另一个除了*description*外说插件信息的地方。 |
接下来在mol文件夹中定义一个叫mol.js的文件。代码如下,它简单的创建了一个mol对象,然后定义了一个calculateMOL方法计算"生活的意义"。最后一行输出了mol对象,它等于对象和相关函数以便使用插件的应用调用。
var mol = { calculateMOL: function() { return 42; } }; module.exports = mol;
这样就完成了创建一个简单插件的工作。为了证明它能起作用,创建一个简单的MoL Demo应用。首先在命令行工具中进入开发文件夹,输入以下CLI命令:
cordova creat simplePlugin com.learningplugin.simplePlugin SimplePlugin cd simplePlugin cordova platform add android cordova plugin add your_plugin_location
创建一个名叫simpleplugin.html的页面,其中有一个按钮,点击后执行doMOL函数,调用了mol.calculateMOL()并用alert对话框显示结果。代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> <meta name="viewport" content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width;"> <script src="cordova.js"></script> <script type="text/javascript" charset="utf-8"> function onBodyLoad() { document.addEventListener("deviceready", onDeviceReady, false); }; function onDeviceReady() { } function doMol() { var res = mol.calculateMOL(); alert("Meaning of Life =' + res); } </script> <title>Meaning of Life Demo</title> </head> <body onload="onBodyLoad()"> <h1>MoL Demo</h1> <p> This is a Cordova application that uses my custom Meaning of Life plugin. </p> <button onclick="doMOL();"> Calculate Meaning of Life </button> </body> </html>
运行结果如下图:
创建一个Native插件
下面的例子用来说明如何创建一个Native插件,它向Cordova容器公开了一些native电话API.参考了Android SDK文档找到一些简单有趣的可供公开的接口。接下来几个部分是如何组织plugin代码;知道插件如何工作后向插件添加其他API或较复杂的API会很容易。
首先创建一个叫"carrier"的插件的文件夹,其中有js接口定义和plugin.xml。还创建了一个叫src的子文件夹并向其中创建了一个名叫"android"的文件夹用来存放native代码。这么做不是必要的但显得有条理。
在写native代码前要定义向Cordova应用公开的js接口。创建方法跟上个例子一样,这个例子中创建一个叫carrier的对象,在其中定义了一个或多个能被Cordova应用调用的方法,本例中将公开getCarrierName和getCountryCode方法。
不像上个MoL插件,这些方法不做计算,也不直接返回任何值;相反,它们调用cordova.exec方法,由它向native代码要控制权。这就是著名的Javscript-to-native桥接,可以让Cordova应用执行native API。native代码执行完后,调用回调函数并把结果返回给js对象。
cordova.exec方法声明如下:
cordova.exec(successCallback, errorCallback, 'Pluginobject', 'pluginMethod', [arguments]);
successCallback和errorCallback参数是插件方法调用成功或失败时执行的函数名。'Pluginobject'参数是一个字符串用于识别包括被调用方法的native对象,'pluginMethod'参数是一个字符串,用来识别要执行的方法,最后的'arguments'是一个可选的参数数组,用来传递给pluginMethod。
例子中的getCarrierName和getCountryCode方法不需要任何参数,arguments参数为空并表示为"[]"。
下面是carrier.js的代码,插件的js接口。它开始用声明了一个cordova对象,指向加载的公开exec方法的cordova js库。接下来创建carrier对象,定义两个方法。每个方法都调用了cordova.exec并传入了cordova对象必要的函数名、对象和方法名,用来定位正确的完成功能的native对象和方法。文件最后是module.exports赋值,让carrier对象向Cordova应用公开。代码如下:
var cordova = require('cordova'); var carrier = { getCarrierName : function(successCallback, errorCallback) { cordova.exec(successCallback, errorCallback, 'CarrierPlugin', 'getCarrierName', []); }, getCountryCode : function(successCallback, errorCallback) { cordova.exec(successCallback, errorCallback, 'CarrierPlugin', 'getCountryCode', []); } }; module.exports = carrier;
接下来就可以写插件的native部分的代码了。先写js还是native部分不重要,一般来说比较复杂的插件先写native会更容易。
创建Android插件
可以通过创建一个Cordova项目创建插件,然后向应用中写Java类和js接口文件。应用中插件可以工作后再把它们抽取出来放在单独的文件夹用来发布。照这么做的话,可以为插件创建一个文件夹,然后创建一个新的Cordova项目,并使用CLI把插件添加进去。这样就可以独立开发插件并同时测试plugin.xml了。
这个插件将向Cordova应用返回的信息来自于电话API。使用这个API前应用必须引入Context和TelephoneManager类:
import android.content.context; import android.telephony.TelephonyManager;
然后在应用里定义一个对象实例来公开一些方法, 我们用这些方法调用carrier名和国家代码:
tm = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
要确定carrier名,插件会调用tm.getSimOperatorName()方法,要获得国家代码会调用tm.getSimCountryIso()。
下面的表列出了用于Android插件的Java代码;它定义了一个叫CarrierPlugin的简单的类,这个类公开了exec方法,它是向js的cordova.exec调用执行的内容。
这个类定义了两个常量ACTION_GET_CARRIER和ACTION_GET_COUNTRY_CODE,用来确定Cordova应用调用哪个方法。这样做在以后更改方法名时更容易。
接下来类定义了一个tm对象,它首先调用了super.initialize,这个方法让cordova对象适当的初始化。没有这个方法Java代码就不知道Cordova容器。接下来代码获得了一个当前应用上下文的句柄,用它把tm对象传递给由Telephony API公开的服务。
接下来Java代码重载了exec方法并实现了直接处理来自Cordova应用的调用的代码。在这里实现了一个单独的操作,它确定了调用了哪个动作(能过比较由调用cordova.exec传递的动作名和开始定义的常触发相应的动作)。
如果exec方法确定请求的是getCarrierName动作,之后它调用了Android的getSimOperatorName方法,并通过调用callbackContext.success()方法把结果传回Cordova应用。如果请求的是getCountryCode动作,之后调用Android的getSimCountryIso()方法并通过callbackContext.success()方法把结果传回Cordova应用。
如果过程中某处失败了,代码执行callbackContext.error并把适当的错误消息或错误对象传回用来指出出了什么错。
package com.learningplugin.carrier; import org.apache.cordova.CordovaInterface; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CordovaWebView; import android.content.Context; import android.telephony.TelephonyManager; import org.json.JSONArray; import org.json.JSONException; public class CarrierPlugin extends CordovaPlugin { public static final String ACTION_GET_CARRIER_NAME = "getCarrierName"; public static final String ACTION_GET_COUNTRY_CODE = "getCountryCode"; public TelephonyManager tm; public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); Context context = this.cordova.getActivity().getApplicationContext(); tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); } @Override public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { try { if (ACTION_GET_CARRIER_NAME.equals(action)) { callbackContext.success(tm.getSimOperatorName()); return true; } else { if (ACTION_GET_COUNTRY_CODE.equals(action)) { callbackContext.success(tm.getSimCountryIso()); return true; } } callbackContext.error("Invalid Action"); return false; } catch (Exception e) { System.err.println("Exception: " + e.getMessage()); callbackContext.error(e.getMessage()); return false; } } }
以上是所有需要的编码。在使用CLI添加新插件之前还需要创建一个plugin.xml文件,它和前边MoL示例相似,但其中因为多了native组件,还需要些额外的设置。内容如下:
<?xml version="1.0" encoding="utf-8"?> <plugin xmlns="http://apache.org/cordova/ns/plugins/1.0" id="com.learningplugin.carrier" version="1.0.0"> <name>Carrier</name> <author>Zhang San</author> <description>Expose mobile carrier related values to Cordova application.</description> <keywords>carrier</keywords> <license>Apache 2.0 License</license> <engines> <engine name="cordova" version="5.0.0" /> </engines> <js-module src="carrier.js" name="carrier"> <clobbers target="carrier" /> </js-module> <platform name="android"> <source-file src="src/android/CarrierPlugin.java" target-dir="src/com/cordovalearingplugin/carrier" /> <config-file target="res/xml/config.xml" parent="/*"> <feature name="CarrierPlugin"> <param name="android-package" value="com.cordovalearingplugin.carrier.CarrierPlugin"/> </feature> </config-file> <config-file target="AndroidManifest.xml"> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> </config-file> </platform> </plugin>
文件中js-module元素定义了js的名字,它将在应用开始时自动加载。它定义了向Cordova公开的js接口。clobbers元素指明了js对象赋值给加载的js对象。本例中,Carrier插件通过一个carrier对象向Cordova应用公开。
<js-module src="carrier.js" name="carrier"> <clobbers target="carrier" /> </js-module>
Cordova应用通过carrier对象访问getCarrierName方法:
carrier.getCarrierName(onSuccess, onFailure);
和前边例子的plugin.xml不同地方是platform节:
<platform name="android"> <source-file src="src/android/CarrierPlugin.java" target-dir="src/com/cordovalearingplugin/carrier" /> <config-file target="res/xml/config.xml" parent="/*"> <feature name="CarrierPlugin"> <param name="android-package" value="com.cordovalearingplugin.carrier.CarrierPlugin"/> </feature> </config-file> <config-file target="AndroidManifest.xml"> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> </config-file> </platform>
它定义了某个移动平台专用的设置,包括了相关native代码的设置。可以有一个或多个平台。里边的source-file元素指出了一个或多个Android native源代码文件,当插件安装时由CLI安装。下面的例子指示plugman或CLI复制CarrierPlugin.java文件到Cordova项目Android平台文件夹的*src/com/cordovalerningplugin/carrier文件夹中。
<source-file src="src/android/CarrierPlugin.java" target-dir="src/com/cordovalearingplugin/carrier" />
config-file元素定义了在插件安装过程中的改动。在例子中,一个叫CarrierPlugin的特性添加到Android项目的config.xml文件中,指向Java类com.cordovalearningplugin.carrier.CarrierPlugin:
<config-file target="res/xml/config.xml" parent="/*"> <feature name="CarrierPlugin"> <param name="android-package" value="com.cordovalearingplugin.carrier.CarrierPlugin"/> </feature> </config-file>
platform最后一个元素定义了另一个配置文件设置。在Android上访问Telephony API需要特别授权。任何要用API的应用都必须向应用的AndroidManifest.xml文件添加入口。这里就要向清单添加android.permission.READ_PHONE_STATE许可:
<config-file target="AndroidManifest.xml"> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> </config-file>
为了测试插件创建一个简单的应用。打开命令行,输入以下命令:
cordova create nativePlugin com.cordovalearningplugin.nativePlugin NativePlugin cd nativePlugin cordova platform add android cordova plugin add your_carrier_plugin_location
在应用的index.html中显示两个按钮,一个调用getCarrierName另一个调用getCountryCode。执行相同的execute函数来显示两个方法的结果,代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Carrier Demo</title> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> <meta name="viewport" content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width"> <script type="text/javascript" charset="utf-8" src="cordova.js"></script> <script type="text/javascript" charset="utf-8"> function onBodyLoad() { document.addEventListener("deviceready", onDeviceReady, false); }; function onDeviceReady() { }; function doSomething1() { carrier.getCarrierName(onSuccess, onFailure); } function doSomething2() { carrier.getCountryCode(onSuccess, onFailure); } function onSuccess(result) { var resStr = "Result: " + result; } function onFailure(err) { console.log("onFailure: " + JSON.stringify(err)); alert("Failure: " + err); } </script> </head> <body onload="onBodyLoad()"> <h1>Carrier Demo</h1> <p> This is a Cordova application that uses my fancy new Carrier Plugin. </p> <button onclick="doSomething1();">GetCarrierName</button> <button onclick="doSomething2()">GetCountryCode</button> </body> </html>
程序运行后如下图1,点各按钮的弹出信息见图2和3。
部署插件
部署插件的时候有几种方法。可以把插件文件夹打包成zip格式(插件文件夹包括plugin.xml文件和所有的源代码文件)。用户拿到之后先解压到一个文件夹,再使用cordova plugin add安装。
还可以通过gitHub发布,你可以创建GitHub帐号并把插件放在里边。文件升级时可以让其他开发者知道这一消息。PhoneGap开发团队创建了一个仓库(https://github.com/phonegap/phonegap-plugins),在这你可以发布你的插件的信息。团队致力于开发一个插件发现系统,以便让开发者更方便的定位在应用中用到的插件。