PhoneGap Plugin(1):自製原生插件

我們都知道 PhoneGap 是一套使用 HTML5 撰寫 App 的”語言”,它相當於在 App 中使用 WebView 的方式呈現我們所設計的介面。當我們需要使用到手機上的特殊功能,例如相機、GPS等等時,則是透過 PhoneGap Plugin 來實作這些功能。而 PhoneGap Plugin 其實寫的就是原生的程式,PhoneGap 只是定義了一些程式溝通的接口,讓原生程式和 PhoneGap 產生的介面能互相溝通。這也是 PhoneGap 之所以強大的原因。我們可以利用 HTML、JavaScript(JS)、CSS 設計並控製複雜的前端介面,並且將硬體的控制交給 Plugin(原生程式) 來處理。所以在這樣的架構下,原生程式做得到的事情,PhoneGap 當然也能做到。當然,由於是 WebView 的關係,早期的 PhoneGap 在處理 JavaScript 的效能並不是很好。不過隨著硬體發展與作業系統的進步,現在這已經不是太大的問題了。

過去在這個部落格上也有介紹一些 Plugin(插件) 的使用方式,不過那些都是別人寫好的原生程式。如果你想要做的功能不幸沒有人提供 Plugin,那你只好考慮自己撰寫了。這篇文章就是要來教大家如何撰寫自己想要的 Plugin。注意一下,我並不是要教大家如何寫”原生程式”,而是教大家如何實作原生程式與 JS 的接口,如何互相呼叫與傳遞參數。所以你在這一系列的文章中會看到如何建立 Plugin、定義呼叫原生程式的方法、參數傳遞的方式、以及原生程式呼叫前端JS的方法。也就是說除了原生程式碼外,也會看到JS的程式碼。相關的資料可以參考官方文件

Plugin 的檔案結構

開始撰寫插件之前,我們一樣要先了解它的檔案結構。要完成一個插件至少要有三個檔案:plugin.xml、myPlugin.js、原生程式(例如 myPlugin.java)。plugin.xml 是描述插件設定的檔案,一定要放在插件資料夾的根目錄下。JS 檔中定義了 PhoneGap 程式要如何使用此插件,因此我把它稱作介面設定檔,通常是放在 www 的資料夾下。原生程式是集合在 src 目錄下,並且放在平台各自的名稱底下,例如 android、ios、windows 等。

myPlugin ─┬─ plugin.xml

      ├─ www ─── myPlugin.js (可自由命名)

      └─ src ─┬─ android

           └─ ios

plugin.xml 設定

如同我們建立 PhoneGap 專案要寫 config.xml 一樣,建立插件也要撰寫 plugin.xml。不過 config.xml 比較多是對於 app 參數的設定和調整,而 plugin.xml 最重要的工作則是在註冊插件的關鍵字,以及指定 JS 與原生程式等相關檔案的位置。

第 2 行我們必須指定此 plugin 的 ID 名稱,命名的規則和 PhoneGap 一樣,是反向的網址。5-7 行可以設定此 plugin 的基本資料,對於公開的 plugin 來說可以幫助大家了解此 plugin 的作用。第 10 行可以設定插件的名稱,並指定 JS 檔案的位置。第 12-13 行是註冊此插件的關鍵字,我們可以根據需求註冊多個關鍵字。既然稱為關鍵字,代表我們在專案裡不能使用此名稱作為變數名稱,而且不同的插件也不能重覆註冊關鍵字。例如第 12 行註冊了 pluginName,代表我的 PhoneGap 寫到 pluginName 這個關鍵字時,系統會自動把它視為此插件的物件。

第 17-32 行是 android 的設定,不同的平台設定就會使用 標籤作區格,如果你的插件沒有該平台的版本,就請自動省略吧。我們可以透過 標籤來改寫原始的專案設定。19-22 行是 android 註冊插件 package 的方式。25-28 行替專案增加了權限設定。你會注意到,這些參數要寫入那個設定檔,上面都會有很仔細的標明,我們必須對原生程式有所了解,才會知道這些設定該放在那裡。第 31 行是把我們的原生程式從插件 src 位置複製到專案 platform 的 target-dir 目錄底下,由於 android 的專案檔案是不能亂擺放的,所以 target-dir 資料夾必須對應第 21 行的 package 才行。iOS 的設定原則上和 android 大同小異,不過它的檔案是直接註冊到 .xcodeproj 檔案中,所以不需要填寫 target-dir。

<plugin id="tw.moke.mypackagename" version="0.0.1" 
        xmlns="http://apache.org/cordova/ns/plugins/1.0"
        xmlns:android="http://schemas.android.com/apk/res/android">
    <name>Plugin 的名字</name>
    <description>對此 Plugin 的描述</description>
    <author>Plugin 的作者資料</author>

    


    <js-module name="pluginName" src="www/pluginName.js">
        


        <clobbers target="pluginName"/>
        <clobbers target="cordova.plugin.pluginName"/>
    </js-module>

    


    <platform name="android">
        <config-file parent="/*" target="res/xml/config.xml">
            <feature name="pluginName">
                


                


</feature>
        </config-file>
        
        <config-file target="AndroidManifest.xml" parent="/manifest">
            

            <uses-permission android:name="android.permission.INTERNET" />
        </config-file>

        


        <source-file src="src/android/pluginName.java" target-dir="src/tw/moke/mypackagename"/>
    </platform>

    


    <platform name="ios">
        <config-file parent="/*" target="config.xml">
            <feature name="pluginName">
                


                


</feature>
        </config-file>

        

        <header-file src="src/ios/CDVpluginName.h" />
        <source-file src="src/ios/CDVpluginName.m" />
    </platform>
</plugin>

JavaScript 介面定義

由於 PhoneGap 的程式是以 JavaScript 為主,並不認識原生程式。因此必須撰寫額外的 JS 介面設定檔,用來連接 PhoneGap 與原生程式。JS 介面設定檔主要的內容是在定義呼叫原生程式的方法、傳遞的參數,並且指定呼叫成功或失敗時所要執行的動作。

撰寫的內容可參考下列程式碼,你會發現它的內容並不多。第 2 行我們定義此插件有一個 myFunction,呼叫時傳入 success(必須是 function) 以及 parameter。第 3 行表示我們會呼叫 plugin.xml 第 19 與 37 行 pluginName 指定的 package,並且指定 myFunction 的動作。parameter 會傳給原生程式,如果沒有參數就直接打 [] 即可,複數的話用逗號分隔。成功時執行 success,失敗時執行 function(args) {}。如果在失敗時也想做動作,可以像 success 帶入參數即可(例如取作 error),這些參數都可以根據我們的需求做設定。如果你的插件有二個以上的方法,只要把 2-4 行再複製一次後修改即可。

var exec = require('cordova/exec');
exports.myFunction = function (parameter, success) {
    exec(success, function(args) {}, 'pluginName', 'myFunction', [parameter]);
};

在 PhoneGap 專案中使用 Plugin

上面的介面設定在 PhoneGap 專案中該如何呼叫呢?只要根據上面第 2 行的順序傳入參數或方法即可,可參考下面的程式碼。注意第 1 行的 pluginName 就是在 plugin.xml 第 12 行註冊的關鍵字。

pluginName.myFunction(parameter, function(result) {
    //成功呼叫後執行的動作
});

Android 插件介面實作

原生程式的檔案可以有很多,但是一定要有一個檔案是實作 PhoneGap 的 plugin 介面,以便和我們的 PhoneGap 程式介接。Android 實作的方法如下,第 1 行是 package 名稱,第 9 行標明 pluginName。上面 JS 設定檔中第 3 行的 myFunction 會傳遞到下面 11 行的 action,所以 12-21 行是透過字串比對的方式來決定要做什麼動作。通常我們會呼叫其它自行撰寫的 function 來完成工作。

package tw.moke.mypackagename;

import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CallbackContext;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;

public class pluginName extends CordovaPlugin {

    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
        if ("myFunction".equals(action)) {
            String parameter = args.getString(0);

            if( parameter != null ) {
                callbackContext.success(parameter);
                return true;
            }
            else callbackContext.error("error message");
        }
        return false;
    }
}

iOS 插件介面實作

由於本人目前還沒有自己撰寫過 iOS 的插件,所以這邊只能從文件中找來範例檔給大家參考,原則上觀念是相通的。

/********* CDVpluginName.h Cordova Plugin Header *******/
#import <Cordova/CDVPlugin.h>

@interface CDVpluginName : CDVPlugin

- (void)myFunction:(CDVInvokedUrlCommand*)command;

@end
/********* CDVpluginName.m Cordova Plugin Implementation *******/
#import "CDVpluginName.h"
#import <Cordova/CDVPlugin.h>

@implementation CDVpluginName

- (void)myFunction:(CDVInvokedUrlCommand*)command
{
    CDVPluginResult* pluginResult = nil;
    NSString* myarg = [command.arguments objectAtIndex:0];

    if (myarg != nil) {
        pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
    } else {
        pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Arg was null"];
    }
    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}

@end

安裝自製插件

自行撰寫的插件建議放在 PhoneGap 專案目錄”外”,然後使用安裝插件的語法加入。一旦插件有更新修改時,只要移除之後再重新加入即可,這樣的方法才能確保插件真的有被更新。加入的插件會被放在 plugins 資料夾中。如果你的 PhoneGap 專案已經產生 platforms 的資料夾,插件就會根據 plugin.xml 的內容將檔案與設定寫入 platforms 中。如果你只是想對插件的原生程式 debug,可以直接修改 platforms 裡的原生程式,但要記得將修改的內容複製到 plugins 裡的程式以及你自己維護的插件專案中。如果你有修改 plugin.xml 或 JS 介面設定檔的內容,就”一定”要用移除再重新加入插件的方式更新,否則 PhoneGap 專案是不會更新的。

在執行 cordova 指令時,可能自動修改 platforms 裡的原生程式,因此我們在 platforms 資料夾裡做的改動可能都會消失。例如 iOS 的 .xcodeproj 檔在每次指令動作時都會被重寫一次,之前的設定全部都會不見。所以在前一章才會建議大家維護 plugins 與 www 資料夾即可。一定要在 platforms 下完成工作時,也必須記錄下自己做了那些事情,不然未來更新 PhoneGap 或插件的版本時會變得非常麻煩。

安裝插件:cordova plugin add 本機的絕對/相對路徑
插件移除:cordova plugin remove tw.moke.mypackagename