随着App的设计、开发和维护成本的水涨船高以及H5+JS制作UI的日益成熟,hybrid的App越来越成为很多开发者的选项之一。自然的,Apache的Cordova成为大部分开发者的首选。对于开发一个App来说,通常无法仅仅通过HTML+CSS+JS来实现诸如访问本地的照片、获取定位信息等还需要使用原生API来实现;这时插件便有了用武之地,通过它,Javascript代码能够调用原生API,同时,原生API中的事件也可以通过插件来传递到Web控件中,这样就大大hybrid应用的功能。
由于本人使用Mac开发,故仅贴上Mac上开发环境的建立过程
1 安装nodejs和npm
本人使用Homebrew(http://brew.sh/)安装,读者亦可到nodejs官网(https://nodejs.org)上下载安装包直接下载
brew install nodejs npm
2 安装命令行工具
sudo npm install -g cordova sudo npm install -g ios-sim #用于从命令行启动iOS模拟器 sudo npm install -g plugman #插件管理工具
3 设置PATH以及ANDROID_HOME环境变量(用于编译Android应用)
PATH=$PATH:/path/to/sdk/tools export PATH ANDROID_HOME=/path/to/sdk export ANDROID_HOME
4 安装XCode的Command Line Tools(仅针对Mac上开发iOS应用)
到XCode->Preferences中相应的设置页中安装
首先,使用plugman来创建插件工程
plugman create -name MyPlugin --plugin_id com.jxjx.www.plugins --plugin_version 0.1 cd MyPlugin/ plugman platform add --platform_name ios plugman platform add --platform_name android
会产生两个插件源代码,分别为MyPlugin/src/android/MyPlugin.java和MyPlugin/src/ios/MyPlugin.m
再创建Cordova应用
cordova create PluginDemo "com.jxjx.www.myplugindemos" “MyPluginDemos" cd PluginDemo/ cordova platform add ios cordova platform add android
我们html页面如下:
<!DOCTYPE html> <!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <html> <head> <!-- Customize this policy to fit your own app's needs. For more guidance, see: https://github.com/apache/cordova-plugin-whitelist/blob/master/README.md#content-security-policy Some notes: * gap: is required only on iOS (when using UIWebView) and is needed for JS->native communication * https://ssl.gstatic.com is required only on Android and is needed for TalkBack to function properly * Disables use of inline scripts in order to mitigate risk of XSS vulnerabilities. To change this: * Enable inline JS: add 'unsafe-inline' to default-src --> <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *"> <meta name="format-detection" content="telephone=no"> <meta name="msapplication-tap-highlight" content="no"> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width"> <link rel="stylesheet" type="text/css" href="css/index.css"> <title>Hello World</title> </head> <body> <div class="app"> <h1>Apache Cordova</h1> <div id="deviceready" class="blink"> <p class="event listening">Connecting to Device</p> <p class="event received">Device is Ready</p> </div> </div> <br /> <button id="btnEcho">echo</button> <button id="btnDelayedEcho">delayed echo</button> <script type="text/javascript" src="cordova.js"></script> <script type="text/javascript" src="js/index.js"></script> </body> </html>
我们的index.js如下:
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var app = { // Application Constructor initialize: function() { this.bindEvents(); }, // Bind Event Listeners // // Bind any events that are required on startup. Common events are: // 'load', 'deviceready', 'offline', and 'online'. bindEvents: function() { document.addEventListener('deviceready', this.onDeviceReady, false); document.getElementById("btnEcho").onclick = function() { cordova.exec(function(data){ alert(data); }, function(err) { alert("error: " + err); }, "MyPlugin", "echo", ["hiiii"] ); }; document.getElementById("btnDelayedEcho").onclick = function() { cordova.exec(function(data){ alert(data); }, function(err) { alert("error: " + err); }, "MyPlugin", "delayedEcho", ["hiiii"] ); }; }, // deviceready Event Handler // // The scope of 'this' is the event. In order to call the 'receivedEvent' // function, we must explicitly call 'app.receivedEvent(...);' onDeviceReady: function() { app.receivedEvent('deviceready'); }, // Update DOM on a Received Event receivedEvent: function(id) { var parentElement = document.getElementById(id); var listeningElement = parentElement.querySelector('.listening'); var receivedElement = parentElement.querySelector('.received'); listeningElement.setAttribute('style', 'display:none;'); receivedElement.setAttribute('style', 'display:block;'); console.log('Received Event: ' + id); } }; app.initialize();
插件是Cordova跟原生接口之间的桥梁,js通过如下接口调用原生接口:
cordova.exec(function(successParam) {}, function(err) {}, "service", "action", ["firstArgument", "secondArgument", 42, false] );
我们的示例插件提供了两个接口,一个为echo,直接返回传入的参数,用以演示立即返回参数值的情况;另一个为delayedEcho,调用后经过3秒后延时返回,用以演示原生程序通过事件方式通知js层的方法。
对于Android系统,其对应于从org.apache.cordova.CordovaPlugin继承的execute方法,我们的代码(MyPlugin.java)如下:
package com.jxjx.www.plugins; import java.util.Timer; import java.util.TimerTask; import android.os.Handler; import android.os.Message; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CallbackContext; import org.apache.cordova.PluginResult; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; /** * This class echoes a string called from JavaScript. */ public class MyPlugin extends CordovaPlugin { private CallbackContext cbCtx; private Timer mTimer; private String paramSaved; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { mTimer.cancel(); mTimer = null; PluginResult result = new PluginResult(PluginResult.Status.OK, "delayed echo: "+ paramSaved); result.setKeepCallback(false); if (cbCtx != null) { cbCtx.sendPluginResult(result); cbCtx = null; } } }; private void setTimerTask() { mTimer = new Timer(); mTimer.schedule(new TimerTask() { @Override public void run() { Message message = new Message(); message.what = 1; mHandler.sendMessage(message); } }, 3000, 3000); } @Override public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { if (action.equals("echo")) { String message = args.getString(0); this.echoMethod(message, callbackContext); return true; } else if (action.equals("delayedEcho")) { this.cbCtx = callbackContext; paramSaved = args.getString(0); PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT); result.setKeepCallback(true); setTimerTask(); callbackContext.sendPluginResult(result); return true; } return false; } private void echoMethod(String message, CallbackContext callbackContext) { if (message != null && message.length() > 0) { callbackContext.success(message); } else { callbackContext.error("Expected one non-empty string argument."); } } }
如果execute方法返回true,则exec输入的第一个回调函数被调用,返回参数通过一个org.apache.cordova.PluginResult参数传递;如果execute方法返回false,则exec输入的第二个回调函数被调用;
service参数为插件名;
exec的action对应于execute方法的action;
exec的第四个参数必须为一个数组,对应于execute的args参数;
execute方法的callbackContext参数为上下文;如果要将事件返回至js层,需要保存起来供后续调用,并且添加如下一行代码:
result.setKeepCallback(true);
对于iOS系统,其对应于CDVPlugin类的相应方法,我们的代码如下:
/********* MyPlugin.m Cordova Plugin Implementation *******/ #import <Cordova/CDV.h> @interface MyPlugin : CDVPlugin { // Member variables go here. } @property (nonatomic, strong) NSString *callbackId; @property (nonatomic, strong) NSString *echoStr; - (void)echo:(CDVInvokedUrlCommand*)command; - (void)delayedEcho:(CDVInvokedUrlCommand*)command; @end @implementation MyPlugin - (void)timeoutCb:(id)arg { NSTimer *timer = (NSTimer *)arg; NSString *echoStr = [NSString stringWithFormat:@"delayed echo: %@", timer.userInfo]; CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:echoStr]; [result setKeepCallbackAsBool:NO]; [self.commandDelegate sendPluginResult:result callbackId:self.callbackId]; } - (void)echo:(CDVInvokedUrlCommand*)command { CDVPluginResult *pluginResult = nil; NSString *echo = [command.arguments objectAtIndex:0]; if (echo != nil && [echo length] > 0) { pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:echo]; } else { pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR]; } [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } - (void)delayedEcho:(CDVInvokedUrlCommand*)command { CDVPluginResult* pluginResult = nil; NSString *echo = [command.arguments objectAtIndex:0]; if (echo != nil && [echo length] > 0) { pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT]; [pluginResult setKeepCallbackAsBool:YES]; self.callbackId = command.callbackId; self.echoStr = echo; [NSTimer scheduledTimerWithTimeInterval:3.0f target:self selector:@selector(timeoutCb:) userInfo:echo repeats:NO]; } else { pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR]; } [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } @end
与Android平台不同的是,action对应的是Objective-C类的方法名,其余完全类似。
在iOS平台中如果要传递异步消息至js层,需要保存的是callbackId;
插件编写完成后,需要在App的根目录中使用plugman命令来添加插件
#如果修改了插件源码需要先移除原插件:cordova plugin remove com.jxjx.www.plugins plugin add ../MyPlugin/ cordova run android cordova run ios
界面的效果如下
最后,附上源码链接:http://pan.baidu.com/s/1hq0h0X2