UNPKG

para-bridge-demo

Version:

a bridge api for js-ios/andriod rest

250 lines (227 loc) 8.49 kB
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(); } }