para-bridge-demo
Version:
a bridge api for js-ios/andriod rest
250 lines (227 loc) • 8.49 kB
text/typescript
import { set, isFunction, cacheFactory, isFacWebview, lt, get, getPlatform, Platform, appVersion } from 'para-utils';
import { INtvCallback, INtvCallbackResult } from '../types/INtvDtoProtocol';
import { parseCallBackDataFromNative, buildBridgeParameter } from '../utils/bridgeUtils';
const webviewGroupCache = {};
function generateUniqueName(ntvMethod): string {
if (!webviewGroupCache[ntvMethod]) {
webviewGroupCache[ntvMethod] = cacheFactory(ntvMethod);
}
return webviewGroupCache[ntvMethod]();
}
export abstract class BridgePlugin<T, K>{
protected param?: T;
// bridge api的唯一标识
protected pluginName: string;
constructor(param?: T) {
this.param = param;
}
/**
* 当前bridge对应的pluginName,和ios/android之间的约定
*/
protected get ntvPluginName(): string {
throw new Error('Method Not Implement');
}
/**
* 当前bridge插件最低可运行的Native版本支持.
*/
protected get availableMinVersion(): string {
throw new Error('Method Not Implement');
}
protected get ntvVersion() {
return appVersion();
}
/**
* 当前的平台类型 Android, Ios, Webview, Broswer
*/
protected get platform(): Platform {
return getPlatform();
}
/**
* Chrome浏览器运行时容器:可以实现在浏览器或者其他平台运行时执行逻辑,弥补平台调用差异性
* @param pluginName 真实传递到Native的插件的函数调用名字window._ntv_bar_set_navbar_1
*/
protected abstract bridgeExecChrome(pluginName: string): void;
protected bridgeExecIos(url): void {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
const docContent = document.body || document.documentElement;
docContent.appendChild(iframe);
setTimeout(() => {
if (iframe && iframe.parentNode) {
iframe.parentNode.removeChild(iframe);
}
}, 200);
}
/**
* 插件调用失败,需要清理已经绑定的错误的注册回调函数
* 调用端需要catch error,纠正,并重新执行绑定注册流程(插件重新初始化)
*/
private clearBridgeMemory(pluginName) {
if (pluginName) {
set(window, pluginName, null);
}
}
/**
* 检查当前的运行时环境是否能正常支持当前的plugin去执行native插件调用
*/
protected canInvokeNtv(): boolean {
if (this.platform !== Platform.FacIosWebview && this.platform !== Platform.FacAndroidWebview) {
return false;
}
// native version >= plugin required min version
if (lt(this.ntvVersion, this.availableMinVersion)) {
throw new Error(`当前版本不支持此方法 当前版本:V${this.ntvVersion}, 插件要求最新版本${this.availableMinVersion}`);
}
// Android: 是否当前WEBVIEW已经存在此方法签名的定义
if (this.platform === Platform.FacAndroidWebview && !get(window, this.pluginName)) {
throw new Error(`当前版本此调用的ntv方法签名不存在 ${this.pluginName}`);
}
return true;
}
/**
* 包装Native回调函数全局window对象的自动签名 如: _ntv_bar_set_navbar_1
* 回调客户端签名可能是 window._ntv_bar_set_navbar_1();
* Note: 同一个实例只注册一个pluginName方法签名.如果已经注册,则返回已有的。这意味着同一页面调用多次bridge,实际使用的是同一个实例
* @param pluginName 注册的插件名
* @param callbackDto 回调客户端的执行逻辑
* @returns {string} cbUniqueName 返回生成的唯一的插件签名
*/
protected registWebviewCb(pluginName: string, callbackDto): string {
const cbUniqueName = generateUniqueName(pluginName);
set(window, cbUniqueName, callbackDto);
return cbUniqueName;
}
protected buildNtvParameter(pluginName?): string {
return buildBridgeParameter(this.param, pluginName);
}
/**
* 调用iOS Bridge执行机制
* @param param bridge调用入参
*/
protected calliOSEasyJsBridge(param: string) {
const argStr = ':s' + encodeURIComponent(':' + encodeURIComponent(param));
this.bridgeExecIos('easy-js:' + encodeURIComponent(this.pluginName) + argStr);
}
/**
* Webview运行时容器:执行bridge Ntv调用, 默认在webview里面运行,
* Note:提供其他模拟器环境(如chrome浏览器),需要自己根据情况实现
* @param pluginName 注册的插件名
*/
protected bridgeExec(pluginName?: string): void {
// Android: webview只能调window对象,不能申明自定义变量,再调用
if (this.canInvokeNtv()) {
const ntvEncodedParameters = this.buildNtvParameter(pluginName);
if (this.platform === Platform.FacAndroidWebview) {
// 针对Android 的实现
window[this.pluginName](ntvEncodedParameters);
} else if (this.platform === Platform.FacIosWebview) {
// 针对ios 的实现
this.calliOSEasyJsBridge(ntvEncodedParameters);
}
}
if (!isFacWebview()) {
// 可针对不同运行环境实现特定的调用逻辑来MOCK,弥补实现差异性.
this.bridgeExecChrome(this.pluginName);
}
}
/**
* callbackDto 对回调的一层封装
* todo: error 的捕获和通知
*/
protected executeCallBack(callback?: INtvCallback<K>) {
if (callback && isFunction(callback)) {
const callbackDto = (...args) => {
try {
callback(null, parseCallBackDataFromNative<K>(args));
} catch (error) {
this.clearBridgeMemory(this.pluginName);
if (callback) {
callback(error);
}
}
};
if (!this.pluginName) {
// 把生成唯一的函数签名绑定到this.pluginName上
this.pluginName = this.registWebviewCb(
this.pluginName, callbackDto
);
} else {
// 如已经绑定过,更新回调函数为最新的传进来的callback
set(window, `${this.pluginName}`, callbackDto);
}
}
try {
this.bridgeExec(this.pluginName);
} catch (error) {
this.clearBridgeMemory(this.pluginName);
if (callback) {
callback(error);
}
}
}
/**
* 执行promise形式的回调,返回promise<K>对象类型。成功执行完后就销毁挂载在桥(全局变量)下注册的函数
* 多次调用生成的函数签名是不同的, 即便是同一个插件实例化的invokeOnce都是生成不同的ntv回调函数签名.
*/
protected executePromise(): Promise<INtvCallbackResult<K>> {
return new Promise((resolve, reject) => {
const uniquePluginName = this.registWebviewCb(this.pluginName, (...args) => {
// 注意这里的Promise执行一次就结束了,[每次调用]对应一个pluginName
this.clearBridgeMemory(uniquePluginName);
try {
// 走标准Ntv回调协议解析data callback回调参数
resolve(parseCallBackDataFromNative<K>(args));
} catch (error) {
reject(error);
}
});
try {
this.bridgeExec();
} catch (error) {
this.clearBridgeMemory(uniquePluginName);
reject(error);
}
});
}
/**
* 桥(全局变量下)绑定的唯一的回调函数,可一直存在,并被多次调用执行
* 如:setNavBar时,前端只绑定一次,但会由ios/andorid多次触发回调,
* 从回调中获取用户的点击动作
* @param callback 回调函数
* @param param 如果参数传进来,将会覆盖构造器的初始化参数
*/
public invokeBind(callback?: INtvCallback<K>, param?: T): void {
if (param) {
this.param = param;
}
this.executeCallBack(callback);
}
/**
* 不需要接收Native回调,直接执行NTV 方法即可.
* @param param 参数
* todo: 错误处理
*/
public invoke(param?: T): void {
if (param) {
this.param = param;
}
try {
this.bridgeExec();
} catch (error) {
console.log(error);
}
}
/**
* 需要回调函数返回一个, 回调函数只执行一次
* 如何: 调用NATIVE 发送数据请求,当回调回来被客户端接收后,会自动释放callback hook.
* 自动清理内存
* @param param 如果参数传进来,将会覆盖构造器的初始化参数
*/
public invokeOnce(param?: T): Promise<INtvCallbackResult<K>> {
if (param) {
this.param = param;
}
return this.executePromise();
}
}