开发

扩展 API

介绍

若当前开源的 API 不能满足宿主需求,宿主可扩展其私有 API。

宿主可扩展的私有 API 分为两类,一类是百度 APP 私有 API,另一类是宿主私有 API。

接下来,首先介绍 API 分类,然后介绍宿主扩展 API 的方式,最后给出不同类型的 API 扩展方式的详述。

API 分类

图片

调用端能力的 API

  • 无前端参数特殊处理

    开发者调用 API 时,传入的参数通过底层 js-nativeargCheck 进行校验即可满足需求,不需要对开发者传入的参数进行校验或者过滤,这种情况属于无前端参数特殊处理的情况。其中,argCheck 依赖于描述表中的 args 参数。下面的描述表部分将有args参数的详述。

  • 有前端参数特殊处理

    如果开发者入参中存在 js-native 不能判断的类型或者客户端无法识别的类型,如 ArrayBuffer,那么需要前端做类型判断,并将 ArrayBuffer 转换成 js-native 能够判断或者客户端可以识别的类型后再进行端能力的调起。

  • 有复杂上下文逻辑

    在调起端能力之前,除了需要对开发者参数进行特殊校验和处理外,还需要其它前端逻辑的处理,这种情况属于有复杂上下文逻辑的情况。

不调用端能力的 API

  • 纯前端

    开发者调用此类 API,只通过前端的处理即可返回结果。

  • 监听类

    这类 API 虽然不需要调用端能力,但前端需要增加对客户端事件的监听,等待客户端派发事件,前端监听到事件后执行开发者回调。

宿主扩展 API 的方式

宿主扩展 API 有两个不可或缺的部分,一个是描述表,另一个是 API 的调起方法。

描述表

描述表是一份端能力的描述,里面描述了一些前端和客户端的通信方式、参数等,所以一定要正确书写描述表,否则端能力是无法生效的。

描述表撰写方法

了解 js-native 描述表的各个参数的含义,然后手动撰写。
单个端能力的描述表举例,此时假设宿主的名字为 hostA:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
name: 'hostA.test',
authority: 'swanAPI',
path: '/test',
args: [
{
// 参数名
name: 'key',
// 参数值类型的定义, 末尾添加 '='时,表明该参数非必填
value: 'string='
},
{
name: 'content',
value: 'string'
}
],
invoke: 'swan.method.json',
method: '_naSwan._naTest.test',
extension: 'swan'
}

具体的参数说明如下:

参数 类型 含义 备注
name string 端能力的名称,如 ‘hostA.test’ 请注意 API 命名规范,需要以 ${hostName}.${apiName} 的形式进行命名, 其中 hostName 为定义在 swan 下的宿主命名空间, 需要与通过客户端提供的全局方法 getEnvVariables 获取的 hostName 字段保持一致,否则将影响 API 的挂载。
authority string 调用端能力 scheme 中对应的 authority 可以参看 URI 的 authority 定义
path string 调用端能力 scheme 中对应的 path 可以参看 URI 的 path 定义
args Array.<object> 调用端能力时需要的参数 下边此参数的介绍和举例,更详细的内容请参考值类型系统
invoke string 调用过程的处理 有效值见下表
method string 调用端能力实际调起的客户端方法 只有 iOS 端 jsc 环境下的 API 以及 Android 的 API 需要此字段
handler string JS 使用 postMessage 向客户端发送信息时的 message name iOS 端独有,只有 webveiw 环境下的异步 API 才需要此字段
extension string 表示该端能力为私有的,可选值为'swan''boxjs' 'swan'表示将该 API 挂载在swan.${hostName}下供开发者调用,'boxjs'表示将该 API 挂载在 boxjs 上供宿主内部小程序框架调用,一般是不想对开发者开放的端能力,比如扩展组件中调用的端能力。

invoke有效值如下:

有效值 含义
'swan.message.url' scheme 方式调用,iOS端 webview 环境异步 API
'swan.prompt' scheme 方式调用,iOS 端 webview 环境同步 API
'swan.method.url' scheme 方式调用,Android 端 webview/v8 环境下的的同/异步 API,iOS 端 jsc 环境的异步 API
'swan.method.json' binding 方式调用,iOS 端 jsc 环境下的同/异步 API
'swan.method.jsonString' binding 方式调用,Android 端 webview/v8 环境下同/异步 API

args参数说明:
args参数与参数校验有关,当前参数校验支持 booleanstringnumberfunctionObjectArray* 这几种类型,args参数定义的举例如下:

  • 参数key应为string类型且为必传,data参数为Object类型且为非必传:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    'args': [
    {
    'name': 'key',
    'value': 'string'
    },
    {
    'name': 'data',
    'value': 'Object='
    }
    ]
  • 参数key必须为abc中的一个且为必传,使用oneOf属性:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    'args': [
    {
    'name': 'key',
    'value': {
    'oneOf': [
    'a',
    'b',
    'c'
    ],
    'isRequired': true

    }
    }
    ]
  • 参数data必须为stringArray类型且非必传,使用oneofType属性:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    'args': [
    {
    'name': 'data',
    'value': {
    'oneofType': ['string', 'Array'],
    'isRequired': false
    }
    }
    ]
  • 参数data必须为数组且数组中的每一项都是string类型且为必传,使用arrayOf属性:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    'args': [
    {
    'name': 'data',
    'value': {
    'arrayOf': 'string',
    'isRequired': true
    }
    }
    ]

描述表维护位置

描述表维护在客户端或前端 extension 模块中,推荐将描述表维护在客户端,客户端描述表详情请参阅 iOS 接入指南Android 接入指南。描述表举例如下,此时假设宿主的名字为 hostA:

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
[
{
name: 'hostA.add',
authority: 'swanAPI',
path: '/add',
args: [
{
name: 'id',
value: 'number'
},
{
name: 'detail',
value: 'string'
}
],
invoke: 'swan.method.json',
method: '_naSwan._naAdd.add',
extension: 'swan'
},
{
name: 'hostA.delete',
authority: 'swanAPI',
path: '/delete',
args: [
{
name: 'id',
value: 'number'
}
],
invoke: 'swan.method.json',
method: '_naSwan._naDelete.delete',
extension: 'swan'
},
...
]

前端调起方法挂载配置

在智能小程序运行时,会将描述表添加到 jsNative 的 container 中,并将每个端能力的调起方法挂载在 boxjs 上,前端通过boxjs[${name}]即可调起其对应的端能力。

extension 包中提到过前端的 swan-extension 模块(后统称为 extension 模块)的定义,其中一个字段是 methods,它表示前端 extension 模块所维护的挂载 API 的调起方法时使用的配置集合,类型为 Object,这个对象中,key 为要挂载到宿主命名空间下的 API 名称,value 为挂载该 API 调起方法时所使用的配置,这个配置包含以下几项:

  • method:挂载的方法,即调用 API 时调用的方法。
  • scope:此属性将决定 method 挂载的命名空间。如果此值为'root',小程序运行时会将 method 挂载在 swan 下,即开发者通过swan.${apiName}来调用;未定义 scope 时,挂载在宿主命名空间下,即开发者通过swan.${hostName}.${apiName}来调用。需要注意,只有当前命名空间下不存在此 API 时,才能挂载成功。

methods 定义的举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let methods = {
add: {
scope: 'root',
method: () => {
console.log('hostA.add API');
}
},
delete: {
method: () => {
console.log('hostA.delete API');
}
},
...
};

扩展方式

不管是扩展百度 APP 私有的 API 还是扩展宿主私有的 API,主要有以下四种扩展方式:
方式一:客户端维护描述表并实现相应的端能力。这种方式下,智能小程序运行时除了会将描述表添加到 container 中,还会依据描述表进行调起方法的挂载。详见下边的扩展方式详述部分。
方式二:客户端维护描述表并实现相应的端能力,extension 模块实现要挂载的调起方法。这种方式下,小程序框架运行时会将描述表添加到 container 中,并根据 swan-extension 模块进行调起方法的挂载。详见下边的扩展方式详述部分。
方式三:extension 模块维护描述表并实现要挂载 API 的调起方法。详见下边的扩展方式详述部分。
方式四:extension 模块实现要挂载的调起方法。详见下边的扩展方式详述部分。

对于各个类别的 API,可使用下图标注的方式进行扩展。
图片

扩展方式详述

方式一:客户端维护描述表并实现相应的端能力

此方式是指前端无需维护 extension 模块,仅宿主客户端维护 API 的端能力描述表并实现相应的端能力即可完成扩展。

原理

智能小程序运行时将宿主客户端维护的描述表注入到 js-native 的 container 中,并通过描述表中每个端能力的 name(形如${hostName}.${apiName}),将 API 的调起方法挂载到 swan.${hostName} 的命名空间下,此处的调起方法指智能小程序框架实现的通用的调起端能力的方法。

使用方式

开源宿主客户端在实现私有端能力的基础上,按照上述描述表撰写方法中的规则维护描述表,此时描述表中的 extension 字段建议定义为 ‘swan’。

以宿主 hostA 新增 API addBook 为例,宿主客户端需要维护如下所示的描述表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[{
name: 'hostA.addBook',
authority: 'swanAPI',
path: '/addBook',
args: [
{
name: 'bookName',
value: 'string='
},
{
name: 'bookId',
value: 'string'
}
],
invoke: 'swan.method.json',
method: '_naSwan._naBook.addBook',
extension: 'swan'
}]

方式二:客户端维护描述表并实现相应的端能力 + extension 模块实现要挂载的方法

此方式是指宿主客户端维护描述表,而要挂载到宿主名命名空间的方法由前端 extension 模块实现。

原理

智能小程序运行时将描述表注入到 js-native 的 container 中;在 extension 模块进行 API 调起方法的实现,当智能小程序运行时加载 extension 模块时,会将模块导出的调起方法挂载到模块定义的 name 的命名空间下,即挂载到 swan.${name} 上。

使用方式

1. 针对开发者入参进行特殊处理的 API
客户端进行描述表的维护以及相应端能力的实现,描述表的维护方式可参见上述描述表部分,此时描述表中的 extension 字段建议定义为 ‘boxjs’;挂载的方法由 extension 模块来实现,接下来通过举例的方式进行介绍。
假设此时的宿主为 hostA,若要开发一个有入参特殊处理的 API testDemo,具体如下:

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
/**
* @file testDemo API method
*/

/**
* 宿主扩展端能力有前端其他逻辑
*
* @param {Object} params 开发者参数
* @param {Object} boxjs boxjs
*/
export const testDemo = boxjs => params => {
let apiName = 'testDemo';

// 入参校验
// 校验 testDemo API 的参数 value 是否是 ArrayBuffer 类型,如果不是,调用开发者的 fail 和 complete 回调
if (param.value) {
let type = Object.prototype.toString.call(params.timeout).slice(8, -1);
if (type !== 'ArrayBuffer') {
callFailAndComplete(params, apiName, `parameter error: value should be ArrayBuffer instead of ${type}`)
}
}
// 入参处理
// 将 testDemo API 的参数 value 转换为 base64 类型,假设此时的转换函数为 arrayBufferToBase64
params.value = arrayBufferToBase64(params.value);

// 调起端能力,假设此时宿主的名字为 hostA
let invokePromise = boxjs[`hostA.${apiName}`](params);

// 执行开发者回调函数
executeCallback({
promise,
apiName
}, params);
};

此时 extension 模块的定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
import {testDemo} from './testDemo';

define('swan-extension', ['swan', 'boxjs'], function (require, module, exports, define, swan, boxjs) {
module.exports = {
name: 'hostA',
methods: {
testDemo: {
method: testDemo(boxjs)
}
}
};
});

通用函数如下:
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
* 执行函数并捕获异常
*
* @param {Function} fn 需要执行的函数
* @param {string} errMsg 错误信息
* @param {Object} params 函数执行的参数
* @return {undefined} undefined
*/
function executeByTryCatch(fn, errMsg, params) {
try {
fn && fn(params);
}
catch (err) {
console.error(`thirdScriptError\n${err.message} ; ${errMsg}\n${err.stack}`);
}
}

/**
* 执行开发者的 fail && complete 回调
*
* @public
* @param {Object} params 参数
* @param {string} apiName API 的名字
* @param {string} errMsg 错误信息
* @param {number} errCode 错误码,前端默认904
*/
function callFailAndComplete(params, apiName, errMsg, errCode = 904) {
errMsg = `${apiName}:fail ${errMsg}`;
if (Object.prototype.toString.call(params).slice(8, -1) === 'Object') {
executeByTryCatch(params.fail, 'at api ' + apiName + ' fail callback function', {errCode, errMsg});
executeByTryCatch(params.complete, 'at api ' + apiName + ' complete callback function', {errCode, errMsg});
}
};

/**
* 等待 promise 返回,拼接错误参数并执行开发者回调
*
* @param {Object} options 内部逻辑处理参数
* @param {Object|Array} options.promise 等待端上结果的 promise 对象
* @param {string} options.apiName API 名称
* @param {Object} params 开发者参数
* @return {undefined} undefined
*/
function executeCallback(options, params = {}) {
let {promise, apiName} = options;
if (promise instanceof Array) {
Promise.all(promise)
.then(function ([callbackRes, cbRes]) {
executeDeveloperCallback(apiName, cbRes, 'success', params);
}).catch(err => {
const result = err instanceof Error ? {errCode: 904, errMsg: err.message} : err;
executeDeveloperCallback(apiName, result, 'fail', params);
});
}
else {
promise.then(res => {
executeDeveloperCallback(apiName, res, 'success', params);
}).catch(err => {
const result = err instanceof Error ? {errCode: 904, errMsg: err.message} : err;
executeDeveloperCallback(apiName, result, 'fail', params);
});
}
};

/**
* 执行开发者回调函数
*
* @param {string} apiName API 的名称
* @param {Object} res 客户端返回信息
* @param {string} callbackName 开发者回调函数名称
* @param {Object} params 开发者入参
*/
function executeDeveloperCallback(apiName, res, callbackName, params) {
// 拼接错误信息的通用部分
const ERROR_PREFIX = 'at api';
const ERROR_SUFFIX = 'callback function';
const ERROR_MSG = {
success: `${ERROR_PREFIX} ${apiName} success ${ERROR_SUFFIX}`,
fail: `${ERROR_PREFIX} ${apiName} fail ${ERROR_SUFFIX}`,
complete: `${ERROR_PREFIX} ${apiName} complete ${ERROR_SUFFIX}`
};


// 执行开发者 success/fail 和 complete 回调函数
executeByTryCatch(params[callbackName], ERROR_MSG[callbackName], res);
executeByTryCatch(params.complete, ERROR_MSG.complete, res);
}

2. 有复杂上下文逻辑的 API
客户端维护描述表并实现相应的端能力,描述表的维护方式可参见上述描述表部分;挂载的方法由 extension 模块来实现,接下来通过举例的方式进行介绍。
假设宿主 hostA 想要新增一个有上下文依赖的的 API createTestContext,那么需要前端进行开发处理逻辑,最终通过 boxjs[${端能力名称}] 来调用端能力,需要注意的是 boxjs 调用端能力返回值为一个 Promise,开发者的回调函数需要在 Promise 的链式处理进行自定义逻辑处理,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @file createTestContext
*/
/**
* 宿主扩展端能力有前端其他逻辑
*
* @param {Object} params 开发者参数
* @param {Object} boxjs boxjs
*/
export const initTestContext = boxjs => params => {
class TestContext {
open(params = {}) {
boxjs['hostA.openTest'](params)
.then(res => {
// 宿主定义逻辑
})
.catch(err => {
// 宿主自定义逻辑
});
}
}
return () => new TestContext()
};

extension 模块的定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @file extension
*/
import {initTestContext} from './createTestContext';

define('swan-extension', ['swan', 'boxjs'], function (require, module, exports, define, swan, boxjs) {
module.exports = {
name: `hostA`,
methods: {
// 在 methods 中自定义对应的 API 方法
createTestContext: {
method: initTestContext(swan)
}
}
components: {},
customLog(swanEventFlow) {},
getCanIUseMap: () => {}
};
});

方式三:extension 模块维护描述表并实现要挂载的方法 + 客户端实现相应的端能力

此方式是指 extension 模块维护描述表并实现要挂载的方法,客户端实现相应的端能力。

原理

智能小程序运行时加载 extension 模块,将 extension 模块中维护的描述表注入到 boxjs 的 container 中;根据 extension 模块定义的 name,将 API 对应的方法挂载到 swan.${name} 的命名空间下。

使用方式

前端维护描述表的方式可参照描述表撰写方法中介绍的方式进行维护;API 的挂载方法的实现可参照方式二中介绍的方式进行实现,此处需要注意的是,由于前端维护的描述表会被注入到 boxjs 的 container 中,所以此时需要通过 boxjs 进行端能力的调起。我们还是以方式二中提到的 testDemo API 为例,介绍这种方式下具体该如何扩展。

此时描述表如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const hostMethodDescriptions = [
{

name: 'hostA.testDemo',
authority: 'swanAPI',
path: '/testDemo',
args: [
{
name: 'value',
value: 'string='
}
],
invoke: 'swan.method.json',
method: '_naSwan._naTestDemo.testDemo'
}
];

此时 method 以及 extension 模块的定义与方式二中的相同。

方式四:extension 模块实现要挂载的方法

此方式适用于不需要调用端能力的 API,所以不需要维护描述表且不需要客户端实现端能力,只通过前端逻辑即可实现。下面将介绍纯前端 API 的例子和监听类 API 的例子。

纯前端 API 举例

假设我们要提供给开发者 add API,此 API 的作用是对传入的两个数字进行相加并返回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @file 宿主私有的 纯前端 API 举例实现
* @description 实际目前并不存在此 API,且 API 的逻辑可能有漏洞,仅举例使用
*/


/**
* 纯前端 API 举例:两个数字相加的同步 API
* 假设此时的宿主名字为 hostA
*
* @param {number} a 待相加的数字
* @param {number} b 待相加的数字
* @return {number} result 相加后的结果
*/
export const add = (a, b) => {
if (!Number.isFinite(a) || !Number.isFinite(b)) {
throw new Error('hostA.add API: parameter error: param a and param b should be typeof Number');
}
return a + b;
};

extension 模块的定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @file extension.js
*/
import {add} from './add';

define('swan-extension', ['swan', 'boxjs'], function (require, module, exports, define, swan, boxjs) {
module.exports = {
name: 'hostA',
methods: {
// 在methods中自定义对应的API方法
add: {
method: add
}
}
components: {},
customLog(swanEventFlow) {},
getCanIUseMap: () => {}
};
});

监听类 API 举例

假设我们要提供给开发者一个对客户端 test 事件的监听,此时需要定义 onTest API 用于启用监听,offTest API 用于取消监听。

1. 增加监听 onTest
通过通用的 eventsListenerAdder 方法添加监听,用于监听客户端派发的事件,此方法接收1个参数对象,对象中共有5个参数,如下:

参数名 类型 必填 含义
listenerName string 客户端派发的事件名称,当前我们举例的 API 的名字为 onTest,那么这个事件名称需要约定为 test
callback Function 开发者传入的回调函数
methodName string 当前监听类 API 的名字,即 onTest
context Object 调用的上下文
parser Function 对客户端派发事件里的 data 数据的解析函数

eventsListenerAdder 方法如下:

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
const EventAction = {
ON: 'on',
OFF: 'off',
HAS: 'has'
};

// 维护添加事件是否已存在监听的标记
let eventAddedFlags = {};

// EventEmitter 定义可参阅:http://icode.baidu.com/repos/baidu/baiduapp/host-access-docs/blob/feature/baidumap-bluetooth:source/%E5%89%8D%E7%AB%AFswan.js%E6%8E%A5%E5%85%A5/%E6%89%A9%E5%B1%95%E7%AB%AF%E8%83%BD%E5%8A%9B%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/src/utils/event-handlers/event-emitter.js
const eventEmitter = new EventEmitter();


/**
* 对监听事件名进行处理,使之与调用方法名相对应
*
* @inner
* @param {string} listenerName 客户端派发事件名称
* @param {string} action 对应方法名标识
* @return {string} eventName 对应的调用方法名
*/
const composeEventName = (listenerName, action) => action + listenerName[0].toUpperCase() + listenerName.slice(1);


/**
* 增加事件监听 && 将开发者的回调函数 push 到队列中
*
* @public
* @param {Object} options 参数对象
* @param {string} options.listenerName 需要监听的事件名称,假设 API 的名字为 onTest,那么 listenerName 为 test
* @param {Function} options.callback 开发者的回调函数
* @param {string} options.methodName API 名称 或 实例对象的方法名称
* @param {Object} options.context 调用上下文
* @param {Function} options.parser 对客户端回传数据的处理函数
*/
export const eventsListenerAdder = options => {
let {
listenerName,
callback,
methodName,
context,
parser
} = options;
let eventFlag = composeEventName(listenerName, EventAction.HAS);
if (!eventAddedFlags[eventFlag]) {
global.addEventListener(listenerName, res => {
eventEmitter.handlerQueueSet.get(listenerName)
.queue.forEach(item => {
item.handler.call(context, parser(res.data));
});
});
eventAddedFlags[eventFlag] = true;
}
if (typeof callback === 'function') {
eventEmitter.on(listenerName, callback);
}
else {
let methodTipName = methodName || composeEventName(listenerName, EventAction.ON);
console.error(
`${methodTipName} Parameter Error: ${methodTipName} accepts a function instead of ${typeof callback}`
);
}
};

2. 取消监听
通过通用的 eventsListenerRemover 方法添加监听,用于监听客户端派发的事件,此方法接收1个参数对象,对象中共有3个参数:

参数名 类型 必填 含义
listenerName string 要取消监听的事件名称,当前我们举例的 API 的名字为 onTest,那么这个事件名称为 test
callback Function 如果不填写此参数,将取消 listenerName 事件的所有监听;如果填写此参数,则将此回调函数从事件队列中移除,若事件队列中没有匹配的回调函数,则不移除任何回调函数,若有多个匹配,则只移除队尾的回调函数
methodName string 当前取消监听 API 的名字,即 offTest

eventsListenerRemover 方法如下:

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
const EventAction = {
ON: 'on',
OFF: 'off',
HAS: 'has'
};

// 维护添加事件是否已存在监听的标记
let eventAddedFlags = {};

// EventEmitter 定义可参阅:http://icode.baidu.com/repos/baidu/baiduapp/host-access-docs/blob/feature/baidumap-bluetooth:source/%E5%89%8D%E7%AB%AFswan.js%E6%8E%A5%E5%85%A5/%E6%89%A9%E5%B1%95%E7%AB%AF%E8%83%BD%E5%8A%9B%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/src/utils/event-handlers/event-emitter.js
const eventEmitter = new EventEmitter();


/**
* 对监听事件名进行处理,使之与调用方法名相对应
*
* @inner
* @param {string} listenerName 客户端派发事件名称
* @param {string} action 对应方法名标识
* @return {string} eventName 对应的调用方法名
*/
const composeEventName = (listenerName, action) => action + listenerName[0].toUpperCase() + listenerName.slice(1);


/**
* 将开发者的回调函数从队列中移除
*
* @public
* @param {Object} options 参数对象
* @param {string} options.listenerName 需要监听的事件名称
* @param {Function} options.callback 开发者的回调函数
* @param {string} options.methodName API 名称 或 实例对象的方法名称
*/
export const eventsListenerRemover = options => {
let {
listenerName,
callback,
methodName
} = options;

if (callback && typeof callback !== 'function') {
let methodTipName = methodName || composeEventName(listenerName, EventAction.ON);
console.error(
`${methodTipName} Parameter Error: ${methodTipName} accepts a function instead of ${typeof callback}`
);
return;
}
eventEmitter.removeListener(listenerName, callback);
};

接下来,使用上述的通用函数,为宿主 hostA 增加监听类 API onTest 和 offTest

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* @file 通用类解析函数
*/

/**
* 用于端派发的事件的数据解析: 将端派发的 Event 对象中的 data 传入此函数中进行解析
*
* @public
* @param {string} data 端派发的事件中的 data
* @return {Object} result 解析结果
*/
export function eventDataParser(data) {
let result;
try {
result = JSON.parse(data);
}
catch (e) {
result = data;
}
return result;
}




/**
* @file test 事件
*/

/**
* 监听 test 事件
*
* @param {Function} callback 监听的事件回调
*/
export function onTest(callback) {
eventsListenerAdder({
listenerName: 'test',
callback,
methodName: 'onTest'
parser: eventDataParser
});
}


/**
* 取消监听 test 事件
*
* @param {Function} callback 监听的事件回调
*/
export function offTest(callback) {
eventsListenerRemover({
listenerName: 'test',
callback,
methodName: 'offTest'
});
}




/**
* @file extension.js
*/
import {onTest, offTest} from './test';
define('swan-extension', ['swan', 'boxjs'], function (require, module, exports, define, swan, boxjs) {
module.exports = {
name: 'hostA',
methods: {
// 在methods中自定义对应的API方法
onTest: {
method: onTest
},
offTest: {
method: offTest
}
}
components: {},
customLog(swanEventFlow) {},
getCanIUseMap: () => {}
};
});

扩展百度 APP 私有 API

仅限开源部分的百度 APP 私有 API

前面的章节主要介绍了扩展 API 的方式,宿主私有 API 和百度 APP 私有 API 均可以使用上述方式来进行扩展,但是百度 APP 私有 API 由于优化升级的原因,升级前后的扩展方式略有差异,接下来将阐述这些差异。

从百度智能小程序 SDK 2.23.0 开始,优化升级了私有 API,我们将升级前的 SDK 版本称为老版本,升级后的称为新版本。

老版本

老版本下,小程序框架与百度 APP 的 extension 是分离的,即百度 APP 私有 API 的部分是通过 extension 来注入的,因此宿主必须使用前端 extension 注入的方式来扩展百度 APP 私有 API。

宿主在自己的 extension 中维护想要扩展的百度 APP 私有 API 的描述表及方法。具体可参照以下示例。

extension.js 举例如下:

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
define('test-extension', ['swan', 'boxjs'], function (require, module, exports, define, swan, boxjs) {
module.exports = {
// 宿主名,运行时中会被merge到swan下, 作为扩展方法集合的名称
name: 'baidu',

// 宿主需要注册的端能力集合
hostMethodDescriptions: [
{
'name': 'baidu.commonDescriptionExample',
'authority': 'swanAPI',
'path': '/commonDescriptionExample',
'args': [
{name: 'test', value: 'string'},
{name: 'extension', value: 'string='}
]
}
],

// 宿主需要扩展的可调用方法合集
methods: {
commonDescriptionExample: {
scope: 'root',
method(args = {}) {
const {filter, parser, isCbRequired} = self.baiduPrivateMethodMap[apiName];

let params = {...args};

if (filter) {
params = filter(params);
}

if (apiName.match('Sync')) {
return self.boxjs[`baidu.${apiName}`](params);
}

let cbPromise = isCbRequired
? cbManager.createCbPromise(params, parser || JSON.parse)
: null;

const invokePromise = self.boxjs[`baidu.${apiName}`](params);

const promise = isCbRequired ? [invokePromise, cbPromise] : invokePromise;

executeCallback({
promise,
apiName
}, params);
}
}
}
};
});

新版本

新版本下,智能小程序运行时通过宿主客户端维护的描述表进行描述表的注入和方法的挂载,同时宿主客户端需要进行相应端能力的实现,即前端无需做额外的扩展即可实现 API 的扩展。

因此对于曾经使用上述老版本的方式接入的宿主,我们推荐对百度智能小程序 SDK 进行升级,从而降低百度 APP 私有 API 的扩展和维护成本。具体的升级方式如下:

假设当前的 extension.js 如下,此时假设宿主的名字为 hostA:

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
define('test-extension', ['swan', 'boxjs'], function (require, module, exports, define, swan, boxjs) {
module.exports = {
// 宿主名,运行时中会被merge到swan下, 作为扩展方法集合的名称
name: 'hostA',

// 宿主需要注册的端能力集合
hostMethodDescriptions: [
{
'name': 'baidu.commonDescriptionExample',
'authority': 'swanAPI',
'path': '/commonDescriptionExample',
'args': [
{name: 'test', value: 'string'},
{name: 'extension', value: 'string='}
]
}
],

// 宿主需要扩展的可调用方法合集
methods: {
commonDescriptionExample: {
scope: 'root',
method(args = {}) {
const {filter, parser, isCbRequired} = self.baiduPrivateMethodMap[apiName];

let params = {...args};

if (filter) {
params = filter(params);
}

if (apiName.match('Sync')) {
return self.boxjs[`baidu.${apiName}`](params);
}

let cbPromise = isCbRequired
? cbManager.createCbPromise(params, parser || JSON.parse)
: null;

const invokePromise = self.boxjs[`baidu.${apiName}`](params);

const promise = isCbRequired ? [invokePromise, cbPromise] : invokePromise;

executeCallback({
promise,
apiName
}, params);
}
}
}
};
});

那么升级为新版本后,前端 extension.js 中维护的描述表和方法删除即可,即上述的 extension.js 修改为

1
2
3
4
5
6
7
8
9
10
11
12
define('test-extension', ['swan', 'boxjs'], function (require, module, exports, define, swan, boxjs) {
module.exports = {
// 宿主名,运行时中会被merge到swan下, 作为扩展方法集合的名称
name: 'hostA',

// 宿主需要注册的端能力集合
hostMethodDescriptions: [],

// 宿主需要扩展的可调用方法合集
methods: {}
};
});