大前端混合应用专题篇(4)

1️⃣、请选择一个你比较熟悉的 cordova 插件,介绍其功能、使用方式及运行原理。

插件:cordova-plugin-camera

功能:供照相机API与设备相机进行交互

调用示例和参数:

1
navigator.camera.getPicture(cameraSuccess, cameraError, cameraOptions);
Name Type Default Description
quality number 50 图像的保存质量,范围0-100,100是最大值,最高的分辨率,没有任何压缩损失(请注意有关该相机的分辨率信息不可用。)
destinationType DestinationType FILE_URI 选择返回值的格式
sourceType PictureSourceType CAMERA 获取图片的来
allowEdit Boolean false 允许在选择图片之前进行简单的编辑
encodingType EncodingType JPEG 选择图像的返回编码
targetWidth number 宽度像素用来缩放图像。必须和targetHeight一起使用。宽高比保持不变。
targetHeight number 高度像素用来缩放图像。必须和targetWidth一起使用。宽高比保持不变
mediaType MediaType PICTURE 选择media类型。它只适用PictureSourceType是PHOTOLIBRARY或SAVEDPHOTOALBUM
correctOrientation Boolean 如果是横向拍摄的照片,会自动旋转到正确的方向
saveToPhotoAlbum Boolean 设备拍照后的图像是否保存的图片库中
popoverOptions CameraPopoverOptions 仅ios可用,设定在ipad的popover的位置
cameraDirection Direction BACK 选择前置摄像头还是后置摄像头

以新建项目ionic1为例

1.新建项目

1
2
$ sudo npm install -g cordova ionic
$ ionic start myApp tabs --type ionic1

2.安装ngCordova(也可以忽略),这个其实就是多封装了一层promise的

1
2
##安装ngCordova
$ bower install ngCordova

3.引入js文件,同时引入了cordova.js

WX20190128-183902@2x

4.注入

WX20190128-184112@2x

5.添加平台并且安装插件

1
2
3
4
##添加平台
$ ionic cordova platform add ios
##安装插件
$ cordova plugin add cordova-plugin-camera

6.真机调试(调试前可以修改使用UIWebView还是WkWebView)

WX20190130-185249@2x

使用xcode安装到手机上,接着碰到一个问题导致调用插件的时候app直接闪退

WX20190129-124901@2x

解决方案:

WX20190129-125233@2x

7.在要使用插件的模块注入。

8.启动执行过程

1)前端加载 cordova.js

2)前端加载 cordova_plugins.js

3)通过 cordova_plugins.js 找到需要加载的 cordova 插件

4)通过动态创建 <script> 加载 cordova 插件

5)cordova 插件动态创建 iframe 或复写 prompt 方法调用

6)exec -> iOSExec -> pokeNative

7)原生层调用 nativeFetchMessages 获取调用参数

8)通过 cordova.callbackFromNative 获取原生回传的数据。

补充:

Cordova 内部自己实现了一个模块加载器,require 引入模块后,会根据 clobbers 字段配置的值,自动导出到指定的对象上。

2️⃣、请介绍一下开发 cordova 插件的主要流程和注意事项。

cordova插件编写与使用

1.安装 plugman

1
$ npm install -g plugman

2.创建插件

1
$ plugman create --name EchoPlugin --plugin_id com.joker.cordova --plugin_version 1.0.0

WX20190714-224039@2x

3.plugin.xml内容

1
2
3
4
5
6
7
<?xml version='1.0' encoding='utf-8'?>
<plugin id="com.joker.cordova" version="1.0.0" xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
<name>EchoPlugin</name>
<js-module name="EchoPlugin" src="www/EchoPlugin.js">
<clobbers target="cordova.plugins.EchoPlugin" />
</js-module>
</plugin>

id为plugman创建命令使用的–plugin_id
version为创建命令使用的–plugin_version

4.添加android平台(插件)

1
$ cd EchoPlugin/plugman platform add --platform_name android

EchoPlugin为默认插件,该插件返回调用的字符串参数。
EchoPlugin.java为继承CordovaPlugin插件的类,主要内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class EchoPlugin extends CordovaPlugin {
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if (action.equals("coolMethod")) {
String message = args.getString(0);
this.coolMethod(message, callbackContext);
return true;
}
return false;
}

private void coolMethod(String message, CallbackContext callbackContext) {
if (message != null && message.length() > 0) {
callbackContext.success(message);
} else {
callbackContext.error("Expected one non-empty string argument.");
}
}
}

核心方法execute执行JS的调用
接收参数:
action调用方法名,
args调用方法传递的参数
callbackContext异步回调函数,向JS返回执行结果

5.添加package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "cordova-plugin-echo",
"version": "1.0.0",
"description": "A sample Apache Cordova application that responds to the deviceready event.",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Apache Cordova Team",
"license": "Apache-2.0",
"cordova": {
"id": "cordova-plugin-echo",
"platforms": [
"android"
]
}
}

6.修改插件配置文件plugin.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version='1.0' encoding='utf-8'?>
<plugin
id="cordova-plugin-echo"
version="1.0.0"
xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
<name>EchoPlugin</name>
<js-module name="EchoPlugin" src="www/EchoPlugin.js">
<clobbers target="EchoPlugin" />
</js-module>
<platform name="android">
<config-file parent="/*" target="res/xml/config.xml">
<feature name="EchoPlugin">
<param name="android-package" value="com.joker.cordova.EchoPlugin" />
</feature>
</config-file>
<config-file parent="/*" target="AndroidManifest.xml"></config-file>
<source-file src="src/android/EchoPlugin.java" target-dir="src/com/joker/cordova" />
</platform>
</plugin>

WX20190714-224654@2x

3️⃣、 请选择一个你比较熟悉的 ionic-native 插件,介绍其功能、使用方式及运行原理。

ionic探索之路-ionic-native插件使用,调试

Ionic Native是Cordova / PhoneGap插件的一个TypeScript包装器

1.安装@ionic-native/插件名,和插件

1
2
$ ionic cordova plugin add cordova-plugin-camera
$ npm install @ionic-native/camera

单纯的使用cordova plugin add …安装了插件的话是不能引用到的,因为ts不能引用到js哦。
解决方式是:添加一个xx.d.ts文件即可。

什么是 “.d.ts”头文件?

简单一点讲,就是你可以在 ts 中调用的 js 的声明文件,它完全是个声明文件,没有任何实现代码,如何写.d.ts文件

2.在需要使用的地方的xxx.module.ts的地方引入,就可以使用了

调用示例和参数:和第一题一样

4️⃣、请介绍一下开发 ionic-native 插件的主要流程和注意事项。

ionic 自定义native插件

1.创建插件类

1
2
3
4
@Injectable()
class Geolocation {

}

2.创建类元数据,我们需要指定一些关于插件的一些信息,如@Plugin装饰器来添加一些插件的信息

1
2
3
4
5
6
7
8
@Plugin({
plugin: 'cordova-plugin-geolocation',
pluginRef: 'navigator.geolocation'
})
@Injectable()
export class Geolocation {

}

3.创建类方法,如getCurrentPosition方法

1
2
@Cordova()
getCurrentPosition(options?: GeolocationOptions): Promise<Geoposition> { return }

4.在platformModule中使用派生类注册provider,useClass使用对应平台的插件类

5.具体业务逻辑层import exe-插件名的派生类,依赖注入后使用

5️⃣、请分析一下 ionic-native 插件和 cordova 插件是如何协作的。

ionic-native插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { Injectable } from "@angular/core";
import { Cordova, IonicNativePlugin, Plugin } from "@ionic-native/core";
import { Observable } from "rxjs/observable";

@Plugin({
pluginName: "EXELocation",
plugin: "com.exe.EXELocation", // npm package name, example: cordova-plugin-camera
pluginRef: "plugins.EXELocation", // the variable reference to call the plugin, example: navigator.geolocation
repo: "", // the github repository URL for the plugin
install: "", // OPTIONAL install command, in case the plugin requires variables
installVariables: [], // OPTIONAL the plugin requires variables
platforms: ["Android", "iOS"] // Array of platforms supported, example: ['Android', 'iOS']
})
@Injectable()
export class EXELocation extends IonicNativePlugin {
@Cordova()
getCurrentPosition(message: any): Promise<any> {
return;
}

@Cordova({
observable: true
})
startContinuedLocation(message: any): Observable<any> {
return;
}

@Cordova()
stopContinuedLocation(): Promise<any> {
return;
}
}

@Plugin:

ionic-native/core/decorators.ts下,其实是个方法调用,传入config参数,然后为类添加静态方法,定义了一些方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
export function Plugin(config: PluginConfig): ClassDecorator {
return (cls: any) => {
// Add these fields to the class
for (const prop in config) {
cls[prop] = config[prop];
}

cls['installed'] = (printWarning?: boolean) => {
return !!getPlugin(config.pluginRef);
};

cls['getPlugin'] = () => {
return getPlugin(config.pluginRef);
};

cls['checkInstall'] = () => {
return checkAvailability(cls) === true;
};

cls['getPluginName'] = () => {
return config.pluginName;
};

cls['getPluginRef'] = () => {
return config.pluginRef;
};

cls['getPluginInstallName'] = () => {
return config.plugin;
};

cls['getPluginRepo'] = () => {
return config.repo;
};

cls['getSupportedPlatforms'] = () => {
return config.platforms;
};

return cls;
};
}

@Cordova:

会返回一个对象,里面有个方法调起对应的插件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function Cordova(opts: CordovaOptions = {}) {
return (
target: Object,
methodName: string,
descriptor: TypedPropertyDescriptor<any>
) => {
return {
value(...args: any[]) {
return wrap(this, methodName, opts).apply(this, args);
},
enumerable: true
};
};
}

wrap函数:

封装了返回的类型,根据装饰器方法参数,可返回promise,observable等类型函数,默认返回为promise。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* pluginObj: 插件对象
* methodName: 方法名
* opts:装饰器方法参数
*/
export const wrap = function (pluginObj: any, methodName: string,
opts: CordovaOptions = {}) {
return (...args: any[]) => {
if (opts.sync) {
// Sync doesn't wrap the plugin with a promise or observable,
// it returns the result as-is
return callCordovaPlugin(pluginObj, methodName, args, opts);
} else if (opts.observable) {
return wrapObservable(pluginObj, methodName, args, opts);
} else if (opts.eventObservable && opts.event) {
return wrapEventObservable(opts.event, opts.element);
} else if (opts.otherPromise) {
return wrapOtherPromise(pluginObj, methodName, args, opts);
} else {
return wrapPromise(pluginObj, methodName, args, opts);
}
};
};

setIndex方法适配不同的cordova插件调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
function setIndex(args: any[], 
opts: any = {},
resolve?: Function,
reject?: Function): any {
// ignore resolve and reject in case sync
// 同步的情况,则忽略resolve和reject方法
if (opts.sync) {
return args;
}

// If the plugin method expects myMethod(success, err, options)
if (opts.callbackOrder === 'reverse') {
// Get those arguments in the order [resolve, reject, ...restOfArgs]
args.unshift(reject);
args.unshift(resolve);
} else if (opts.callbackStyle === 'node') { // Node.js error-first 风格
args.push((err: any, result: any) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
} else if (opts.callbackStyle === 'object' && opts.successName && opts.errorName) {
let obj: any = {};
obj[opts.successName] = resolve;
obj[opts.errorName] = reject;
args.push(obj);
} else if (typeof opts.successIndex !== 'undefined' ||
typeof opts.errorIndex !== 'undefined') {
const setSuccessIndex = () => {
// If we've specified a success/error index
if (opts.successIndex > args.length) {
args[opts.successIndex] = resolve;
} else {
args.splice(opts.successIndex, 0, resolve);
}
};

const setErrorIndex = () => {
// We don't want that the reject cb gets spliced into the position of an optional argument that has not been defined and thus causing non expected behaviour.
if (opts.errorIndex > args.length) {
args[opts.errorIndex] = reject; // insert the reject fn at the correct specific index
} else {
args.splice(opts.errorIndex, 0, reject); // otherwise just splice it into the array
}
};

if (opts.successIndex > opts.errorIndex) {
setErrorIndex();
setSuccessIndex();
} else {
setSuccessIndex();
setErrorIndex();
}
} else {
// Otherwise, let's tack them on to the end of the argument list
// which is 90% of cases
args.push(resolve);
args.push(reject);
}
return args;
}