扩展 API 介绍 若当前开源的 API 不能满足宿主需求,宿主可扩展其私有 API。
宿主可扩展的私有 API 分为两类,一类是百度 APP 私有 API,若要扩展此类 API,宿主实现要扩展的 API 的端能力后,即可完成扩展;另一类是宿主私有 API,这一类 API 的端能力和前端扩展均需宿主实现。
接下来,首先介绍 API 分类,然后介绍宿主扩展 API 的方式,最后给出不同类型的 API 扩展方式的详述。
API 分类
调用端能力的 API
无前端参数特殊处理
开发者调用 API 时,传入的参数通过底层 js-native 的 argCheck 进行校验即可满足需求,不需要对开发者传入的参数进行校验或者过滤,这种情况属于无前端参数特殊处理的情况。其中,argCheck 依赖于描述表 中的 args 参数。下面的描述表部分将有args参数的详述。
有前端参数特殊处理
如果开发者入参中存在 js-native 不能判断的类型或者客户端无法识别的类型,如 ArrayBuffer,那么需要前端做类型判断,并将 ArrayBuffer 转换成 js-native 能够判断或者客户端可以识别的类型后再进行端能力的调起。
有复杂上下文逻辑
在调起端能力之前,除了需要对开发者参数进行特殊校验和处理外,还需要其它前端逻辑的处理,这种情况属于有复杂上下文逻辑的情况。
不调用端能力的 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参数与参数校验有关,当前参数校验支持 boolean、string、number、function、Object、Array 和 * 这几种类型,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必须为a、b和c中的一个且为必传,使用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必须为string或Array类型且非必传,使用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 } } ]
描述表维护位置 描述表维护在客户端,客户端描述表详情请参阅 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 的端能力描述表并实现相应的端能力即可完成扩展。
原理 智能小程序运行时将宿主客户端维护的描述表注入到 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 export const testDemo = boxjs => params => { let apiName = 'testDemo' ; 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} ` ) } } params.value = arrayBufferToBase64 (params.value ); 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 function executeByTryCatch (fn, errMsg, params ) { try { fn && fn (params); } catch (err) { console .error (`thirdScriptError\n${err.message} ; ${errMsg} \n${err.stack} ` ); } } 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}); } }; 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); }); } }; 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} ` }; 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 24 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 import {initTestContext} from './createTestContext' ;define ('swan-extension' , ['swan' , 'boxjs' ], function (require , module , exports , define, swan, boxjs ) { module .exports = { name : `hostA` , methods : { createTestContext : { method : initTestContext (swan) } } components : {}, customLog (swanEventFlow ) {}, getCanIUseMap : () => {} }; });
方式三: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 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 import {add} from './add' ;define ('swan-extension' , ['swan' , 'boxjs' ], function (require , module , exports , define, swan, boxjs ) { module .exports = { name : 'hostA' , methods : { 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 = {};const eventEmitter = new EventEmitter ();const composeEventName = (listenerName, action ) => action + listenerName[0 ].toUpperCase () + listenerName.slice (1 );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 = {}; const eventEmitter = new EventEmitter (); const composeEventName = (listenerName, action ) => action + listenerName[0 ].toUpperCase () + listenerName.slice (1 ); 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 export function eventDataParser (data ) { let result; try { result = JSON .parse (data); } catch (e) { result = data; } return result; } export function onTest (callback ) { eventsListenerAdder ({ listenerName : 'test' , callback, methodName : 'onTest' parser : eventDataParser }); } export function offTest (callback ) { eventsListenerRemover ({ listenerName : 'test' , callback, methodName : 'offTest' }); }
extension 模块的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import {onTest, offTest} from './test' ;define ('swan-extension' , ['swan' , 'boxjs' ], function (require , module , exports , define, swan, boxjs ) { module .exports = { name : 'hostA' , methods : { onTest : { method : onTest }, offTest : { method : offTest } } components : {}, customLog (swanEventFlow ) {}, getCanIUseMap : () => {} }; });
扩展百度 APP 私有 API
仅限开源部分的百度 APP 私有 API
前面主要介绍了扩展 API 的方式,宿主私有 API 和百度 APP 私有 API 均可以使用上述方式来进行扩展。但对于百度 APP 私有的 API 来说,小程序框架运行时会通过宿主客户端维护的描述表进行描述表的注入和方法的挂载,并且实现对应的能力,因此前端无需做额外的扩展。