Cordova插件开发指南

下文为对Cordova官网插件开发的翻译。

简介

插件是注入的代码包,可让Cordova Web视图在该视图中渲染应用程序并与其运行所在的本机平台进行通信。插件提供对通常基于Web的应用程序不可用的设备和平台功能的访问。 Cordova API的所有主要功能均作为插件实现,还有许多其他功能可启用诸如条形码扫描仪,NFC通信或定制日历界面等功能。

插件包含一个JavaScript接口(插件www目录下的js文件),以及每个受支持平台的相应本机代码库。本质上,这将各种本机代码实现隐藏在通用JavaScript接口的后面,您可以将其用作模型来构建更复杂的功能。本节讨论基本的插件结构和面向外部的JavaScript接口。

 Cordova 应用程序体系结构

向项目中添加插件

应用程序开发人员使用CLI的plugin add命令将插件添加到项目中。该命令的参数是包含插件代码的git存储库的URL(也可以是插件所在的本地路径)。此示例实现了Cordova的设备API:

cordova plugin add https://git-wip-us.apache.org/repos/asf/cordova-plugin-device.git

存储在git上的插件存储库必须具有顶级的plugin.xml清单文件。有许多方法可以配置此文件,有关详细信息,请参见插件规范
👇这个简短的Device插件版本提供了一个简单的示例展示:

<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
        id="cordova-plugin-device" version="0.2.3">
    <name>Device</name>
    <description>Cordova Device Plugin</description>
    <license>Apache 2.0</license>
    <keywords>cordova,device</keywords>
    <js-module src="www/device.js" name="device">
        <clobbers target="device" />
    </js-module>
    <platform name="ios">
        <config-file target="config.xml" parent="/*">
            <feature name="Device">
                <param name="ios-package" value="CDVDevice"/>
            </feature>
        </config-file>
        <header-file src="src/ios/CDVDevice.h" />
        <source-file src="src/ios/CDVDevice.m" />
    </platform>
</plugin>

顶级插件标签的id属性使用相同的反向域格式来标识插件包添加到他们的应用程序所能识别的插件包,js-module标记指定通用JavaScript接口(即与native层通信的js)的路径。在这种情况下,platform标签为ios平台指定了一组对应的本机代码(细化到每一个源代码文件的源文件位置和导出到平台后要放置的位置,以及引用的第三方库)。config-file标记封装了功能标记,该feature标记注入到特定于平台的config.xml文件中,以使平台知道其他的代码库。header-file和source-file标签指定头文件和源码文件的路径。

使用Plugman验证插件

您可以使用plugman实用程序来检查插件是否针对每个平台正确安装。使用以下节点命令安装plugman:

pm install -g plugman

然后运行以下命令来测试iOS依赖项是否正确加载:

plugman install --platform ios --project /path/to/my/project/www --plugin /path/to/my/plugin

JavaScript接口

JavaScript接口提供了面向前端调用Native层功能的接口,使其成为插件中最重要的部分。您可以随意构建插件的JavaScript,但需要使用以下语法调用cordova.exec与Native层进行通信:

cordova.exec(function(winParam) {},
             function(error) {},
             "service",
             "action",
             ["firstArgument", "secondArgument", 42, false]);

👇介绍每个参数的工作原理:

  • function(winParam) {}:成功的回调函数。假设您的exec调用成功完成,此函数将被执行并携带Native层传递的关于成功信息的参数。
  • function(error) {}: 失败的回调函数。如果操作未成功完成,此函数将被执行并携带Native层传递的关于失败信息的参数。
  • "service":在Native端调用的服务名称。这对应于Native层的类.
  • "action": 在Native端调用的操作名称。这通常对应于Native层类中用于实现插件功能的方法名称。
  • [/* arguments */]: 一个Json数组对象,作为参数传递到Native层的方法中。

JavaScript插件调用示例:

👇示例显示了一种实现调用插件JavaScript接口的方法:

window.echo = function(str, callback) {
    cordova.exec(callback, function(err) {
        callback('Nothing to echo.');
    }, "Echo", "echo", [str]);
};

在此示例中,插件将自身作为echo函数附加到window对象上,该插件将被用户按照以下方式调用:

window.echo("echome", function(echoValue) {
    alert(echoValue == "echome"); // should alert true.
});

Native层实现

为插件定义JavaScript接口后,至少需要使用一种本机实现对其进行补充。下面将介绍
Android层的实现(因为本人只熟悉Android平台的开发)。

Android插件开发说明

Android插件基于 Cordova-Android。它的作用是构建一座从WebView通向Native的桥梁。Android插件的Native部分至少要包含一个Java类,该类继承自CordovaPlugin类并重写父类的execute方法。

插件类映射

插件的JavaScript接口使用cordova.exec方法,如下所示:

exec(<successFunction>, <failFunction>, <service>, <action>, [<args>]);

这封装了从WebView到Android本机端的请求,完成了对service类上的action方法的调用,并向action方法中传递了args数组参数。

无论您是以Java文件还是以自己的jar文件发布插件,必须在您的Cordova-Android应用程序的res / xml / config.xml文件中指定该插件。有关如何使用plugin.xml文件注入此功能元素的更多信息,请参见应用程序插件:

 <platform name="android">
        <config-file parent="/*" target="res/xml/config.xml">
            <feature name="<service_name>">
                <param name="android-package" value="<full_name_including_namespace>" />
            </feature>
        </config-file>
        ...
</platform>

__service_name与JavaScript exec调用中使用的名称相匹配。value的值是继承了CordovaPlugin类的全路径。__如果配置有误,该插件虽然可能会通过编译,但Cordova仍无法完成插件调用。

插件初始化和生命周期

在每个WebView的生存期内,都会创建一个插件对象的实例。但是默认情况下,插件不会跟随WebView一同创建,而是直到JavaScript调用首次引用插件时,才会实例化插件。除非在config.xml中将具有onload name属性的设置为“ true”。例如,

<!-- 设置跟随WebView初始化插件 -->
<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生命周期事件,并且可以通过实现父类CordovaPlugin提供的方法(onResume,onDestroy等)来处理它们。具有长时间运行的请求、后台活动(如媒体播放、侦听器或内部状态)的插件应该实现 onReset ()方法。 它在 WebView 导航到新页面或刷新时执行,这会重新加载 JavaScript。

编写一个Android插件

一个JavaScript调用将向Native层发出一个插件调用请求,并且相应的Java插件已正确映射到config.xml文件中,但是最终的Android Java Plugin类是什么样的?使用JavaScript的exec函数调用的插件回将所有的内容传递到插件类的execute 方法中。大多数执行实现如下所示:

@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
    if ("beep".equals(action)) {
        this.beep(args.getLong(0));
        callbackContext.success();
        return true;
    }
    return false;  // Returning false results in a "MethodNotFound" error.
}

可以通过action参数来分配本次调用需要执行的方法。当插件类的execute方法执行后如果最后返回的是false,则会在JS层抛出 "MethodNotFound" 的错误。

线程

插件的JavaScript不在WebView界面的主线程中运行;相反,它与execute方法一样在WebCore线程上运行。如果需要与用户界面进行交互,则应使用Activity的runOnUiThread方法,如下所示:

@Override
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
    if ("beep".equals(action)) {
        final long duration = args.getLong(0);
        cordova.getActivity().runOnUiThread(new Runnable() {
            public void run() {
                Toast.make(context, "hi", Toast.LENGTH_SHORT).show();
                callbackContext.success(); // Thread-safe.
            }
        });
        return true;
    }
    return false;
}

如果不需要在UI线程上运行,但是也不想阻塞WebCore线程,则应通过cordova.getThreadPool()获得的Cordova的ExecutorService(Java中对线程池定义的一个接口)执行代码,如下所示:

@Override
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
    if ("beep".equals(action)) {
        final long duration = args.getLong(0);
        cordova.getThreadPool().execute(new Runnable() {
            public void run() {
                ...
                callbackContext.success(); // Thread-safe.
            }
        });
        return true;
    }
    return false;
}
添加依赖库

如果您的Android插件具有额外的依赖,则必须通过以下两种方式之一在plugin.xml中列出它们。

方法1:

使用 framework标记,以这种方式指定库允许通过Gradle的依赖管理逻辑对其进行解析。这允许多个插件使用通用库(例如gson,android-support-v4和google-play-services)而不会发生冲突。
下面为通过framework标记引入依赖库的示例:

<!-- Depend on latest version of GCM from play services -->
<framework src="com.google.android.gms:play-services-gcm:+" />
<!-- Depend on v21 of appcompat-v7 support library -->
<framework src="com.android.support:appcompat-v7:21+" />
<!-- Depend on library project included in plugin -->
<framework src="relative/path/FeedbackLib" custom="true" />

Framework也可以用于将自定义.gradle文件包含在插件主项目的build.gradle文件中:

方法2:

使用lib-file标记指定jar文件的位置,仅当您确定没有其他插件引用该库时才应使用此方法。(如果有多个插件通过lib-file标记引用了相同的jar,将会导致冲突发生)。否则,如果另一个插件添加相同的库,则可能会给插件的用户造成构建错误。

Android整合

Android具有Intent系统,该系统允许进程相互通信。插件可以访问CordovaInterface对象,该对象可以访问运行该应用程序的Android 。 CordovaInterface允许插件允许启动一个可以返回结果的Activity。从Cordova 2.0开始,插件不再可以直接访问Context,并且已弃用旧版ctx成员。所有ctx方法都存在于Context上,因此通过getContext()和getActivity()都可以返回所需的对象。

Android静态权限

对于在那些在安装时授权而非在运行时授权的权限,需要将这些在应用程序中使用层的权限添加到Android清单中。这可以通过使用config.xml将这些权限注入到AndroidManifest.xml文件来完成。下面为申请权限的示例:

    <config-file target="AndroidManifest.xml" parent="/manifest">
            <uses-permission android:name="android.permission.BLUETOOTH"/>
            <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
            <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
        </config-file>
Android运行时权限

Android 6.0“棉花糖”引入了新的权限模型,用户可以根据需要打开和关闭权限。这意味着应用程序必须处理这些权限更改,以确保时效性,这是Cordova-Android 5.0.0版本的重点。可以在此处的Android Developer文档中找到需要在运行时处理的权限。

就插件而言,可以通过调用权限方法来请求权限。方法签名如下:

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);
}

在getReadPermission方法中通过调用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;
    }
}

除了为单个权限请求权限外,还可以通过定义权限数组来请求整个组的权限,就像使用Geolocation插件那样:

String [] permissions = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION };

然后在对组的权限进行申请时,需要进行如下操作:

cordova.requestPermissions(this, 0, permissions);
启动其他活动

待添加

发布插件

您可以将插件发布到任何基于npmjs的注册表,但是推荐的插件是npm注册表。其他开发人员可以使用Plugman或Cordova CLI自动安装您的插件。

要将插件发布到npm,您需要执行以下步骤:

  1. 安装plugman命令行工具:
    $ npm install -g plugman
    
  2. 为插件创建一个package.json文件
    $ plugman createpackagejson /path/to/your/plugin
    
  3. 发布它:
    $ npm adduser # that is if you don't have an account yet
     $ npm publish /path/to/your/plugin