From ddfe7a90d426f342b585865bfdef8aaf2c2adeaf Mon Sep 17 00:00:00 2001 From: Tevin <tingquanren@163.com> Date: Wed, 26 Feb 2025 15:45:48 +0800 Subject: [PATCH] 添加知识库文档,项目介绍 --- common/Bridge.js | 493 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 files changed, 410 insertions(+), 83 deletions(-) diff --git a/common/Bridge.js b/common/Bridge.js index f1cec46..841fa9c 100644 --- a/common/Bridge.js +++ b/common/Bridge.js @@ -1,36 +1,54 @@ /** * 跨端双向通讯桥 * @author Tevin + * @version 1.3.0 * @tutorial - * 网页对 app 发起通讯 - * 1. app 注册接收机制 - * app 在网页打开之前,注入方法 linking - * 2. js 发起 - * bridge.invoking('methodName', {key1:'value1'}, function(res){console.log(res)}); - * 3. Bridge 转换为发送 + * 通讯策略 + * 1. App 对网页一般为单向通知,无资源管理要求 + * 2. 网页对 App 发起单向通知时,也无资源管理要求 + * 3. 网页对 App 发起单次通讯时,如诺 App 未执行回调,前端将在5分钟后字段释放资源 + * 4. 网页对 App 发起持续性通讯时,应转换为 App 对网页发起通知,通知无资源管理要求(而不是使用轮询或监听,需要手动停止释放资源) + * 5. 网页对 App 发起通讯,两端具体处理方式如下: + * - 如果 callback 有值,App 端在各种情况下,都尽量执行回调,以便于前端更快释放资源 + * - 如果 callback 无值,即为单向通知,此时 App 无需处理回调(消息提示由 App 端执行) + * - 如果 App 出现报错或异常,执行 callback 回调时不传任何内容(不能传空对象或空字符串,直接为空)(错误提醒由 App 执行) + * - 如果通讯正常,但是业务为支线流程,例如取消操作,则返回标识表明取消操作(消息提示由网页端执行) + * 网页对 App 发起通讯操作步骤 + * 1. 【App端】App 注册接收器,用于接收网页通知 + * 在页面打开之前,App 注入方法 linking = function(paramStr) { <Native Code> } + * 2. 网页业务中 js 发起通讯 + * $bridge.invoking('methodName', {key1:'value1'}, function(res) {console.log(res)}); + * 3. 由 Bridge 转换为调用注入的方法(含callback) * window.linking('{method:\'methodName\', param:{key1:\'value1\'}, callback:\'bridge.cb111at1541994536008\'}'); - * 4. app 处理完后直接调用网页上的回调,并传递结果 + * 4. 【App端】App 处理完后直接调用网页上的全局回调,并传递结果完成通讯 * bridge.cb101at1541994536008('{key2:\'value2\'}'); - * app 对网页发起通讯 - * 1. 网页注册接收器,用于接收 app - * window.telling = function(dataStr) {}; - * 2. 网页注册业务 - * bridge.register('methodName', function(data, callback){}); - * 3. app 调用(至少需要在页面完成再延迟至少1秒之后) + * App 对网页发起通讯操作步骤 + * 1. 网页注册接收器,用于接收 App 通知 + * window.telling = function(dataStr) { someJs }; + * 2. 网页业务中注册接收通讯 + * $bridge.register('methodName', function(res, callback) {}); + * 3. 【App端】App 调用网页中的全局方法(至少需要在页面完成并延迟至少1秒之后再调用) * telling('{method:\'methodName\', param:{key1:\'value1\'}, marker:\'mk222at1541994536008\'}'); - * 4. 网页完成业务逻辑处理,并回调 + * 4. 网页完成业务逻辑处理后调用注入的方法,并传递结果完成通讯(含marker、不含callback) * window.linking('{method:\'methodName\', param:{key2:\'value2\'}, marker:\'mk222at1541994536008\'}'); */ +import { Fetcher } from '@components/bases/Fetcher'; +import { Tools } from '@components/common/Tools'; +import { $fileTrans } from '@components/common/FileTransform'; export class Bridge { - constructor() { this._data = { count: 100, + fileSaved: {}, // 已保存图片名称列表 { 'blob:***' : 'bridge:***' } + fileLoaded: {}, // 已读取图片名称列表 { 'bridge:***' : 'blob:***' } }; this._receives = {}; + this._earlyInvok = []; this._init(); + // 挂载到全局 + window.bridge = this; } /** @@ -40,7 +58,7 @@ */ _checkLinking() { // 安卓注入 - if (window.iTop && window.iTop.linking) { + if (window.aisim && window.aisim.linking) { return 'android'; } // 没有注入 @@ -59,80 +77,109 @@ setTimeout(() => { this._init(++count); }, 500); - } else { - this._initReceive(); } - } - - /** - * 发送 - * @param {object} data - * @private - */ - _send(data) { - const platform = this._checkLinking(); - // 安卓注入 - if (platform === 'android') { - window.iTop.linking(JSON.stringify(data)); - } - // 没有注入 + // 开始工作 else { - console.error('没有注入 linking 方法!'); + this._initReceive(); + // 补发 + if (this._earlyInvok.length > 0) { + this._earlyInvok.forEach(invok => { + const { method, param, callback } = invok; + this._sendLinking(method, param, callback); + }); + } } } /** * 发起一次发送 - * @param {string} method - * @param {object} param - * @param {function} callback - 来自网页业务逻辑的回调 + * @param {String} method + * @param {Object} [param] + * @param {Function} [callback] - 来自网页业务逻辑的回调 * @private */ _sendLinking(method, param, callback) { // 数据检查 - if (Object.prototype.toString.call(param) !== '[object Object]') { + if (!Tools.isObject(param)) { console.error('$bridge.invoking 需要接受 JSON 对象!'); return; } + // 转换发送参数键名为下划线 + param = this.transKeyName('underline', param); + // 如果无回调,视为单向通知,不全局挂载 // 如果有回调,转存回调 - const name = 'cb' + this._data.count++ + 'at' + Date.now(); - this[name] = (res) => { - if (callback && Object.prototype.toString.call(callback) === '[object Function]') { - if (res) { - const data = typeof res === 'string' ? JSON.parse(res) : res; + let cbName = ''; + if (callback && Tools.isFunction(callback)) { + cbName = 'cb' + this._data.count++ + 'at' + Date.now(); + // 5分钟后超时,自动清除资源 + const timer = setTimeout(() => { + // 释放资源 + callback = null; + this[cbName] = () => { + delete this[cbName]; + Tools.toast('跨端通讯超时,请关掉页面重试!'); + }; + }, 5 * 60 * 1000); + // 挂载到全局 + this[cbName] = res => { + // res有值,正常通讯 + if (typeof res !== 'undefined') { + let data; + try { + // 转对象 + data = typeof res === 'string' ? JSON.parse(res) : res; + // 转换接收参数键名为驼峰 + data = this.transKeyName('camel', data); + } catch (e) { + Tools.toast('跨端通讯异常:解析数据失败!'); + return; + } + // 回传业务层,忽略来自业务层的报错 callback(data); - } else { - callback(); } - } - delete this[name]; - }; + // res 无值,视为 java 层异常,由 java 层进行异常提醒 + else { + // do nothing + } + // 执行一次后释放资源 + callback = null; + delete this[cbName]; + clearTimeout(timer); + }; + } // 发送 - this._send({ - method, - param, - callback: 'bridge.' + name, - }); + window.aisim.linking( + JSON.stringify({ + method, + param, + callback: cbName ? 'bridge.' + cbName : '', + }), + ); } /** * 向 app 发起通讯 - * @param {string} method - * @param {object|function} [param] - * @param {function} [callback] + * @param {String} method + * @param {Object|Function} [param] + * @param {Function} [callback] */ - invoking(method, param, callback) { - const trans = param; - if (trans) { - if (Object.prototype.toString.call(trans) === '[object Function]') { - callback = trans; - param = {}; - } - } else { - callback = null; + invoking(method, param = {}, callback) { + // param 无值时 + if (!param) { param = {}; } - this._sendLinking(method, param, callback); + // param 为函数时 + else if (param && Tools.isFunction(param)) { + callback = param; + param = {}; + } + if (this._checkLinking()) { + this._sendLinking(method, param, callback); + } + // 尚未准备好,挂起 + else { + this._earlyInvok.push({ method, param, callback }); + } } /** @@ -140,24 +187,33 @@ * @private */ _initReceive() { - if (!this._checkLinking()) { - return; - } - window.telling = (res) => { + window.telling = res => { const data = typeof res === 'string' ? JSON.parse(res) : res; - const method = data.method; - const param = data.param; - const marker = data.marker; + const { method, param, marker } = data; + // 转换接收参数键名为驼峰 + const param2 = this.transKeyName('camel', param); + // 已注册协议 if (this._receives[method]) { - this._receives[method](param, (param2) => { - this._sendTelling(method, param2, marker); - }); + // 有通知回调 + if (marker) { + this._receives[method](param2, param2 => { + this._sendTelling(method, param2 || {}, marker); + }); + } + // 无通知回调 + else { + this._receives[method](param2); + } + } + // 未注册的协议 + else { + console.warn('BridgeTelling:通讯协议【' + method + '】尚未注册!'); } }; } /** - * 发送响应 + * 回发App通知的响应 * @param {string} method * @param {object} param * @param {string} marker @@ -165,20 +221,24 @@ */ _sendTelling(method, param, marker) { // 数据检查 - if (Object.prototype.toString.call(param) !== '[object Object]') { + if (!Tools.isObject(param)) { console.error('$bridge.register 注册的函数需要接受 JSON 对象!'); return; } + // 转换发送参数键名为下划线 + param = this.transKeyName('underline', param); // 发送 - this._send({ - method, - param, - marker, - }); + window.aisim.linking( + JSON.stringify({ + method, + param, + marker, + }), + ); } /** - * 注册接收指令,可供 app 查询 + * 注册接收指令,可接收 app 通知 * @param method * @param callback */ @@ -194,6 +254,273 @@ return platform === 'android' || platform === 'iOS'; } + /** + * 键名转换 + * @param type + * @param json + */ + transKeyName(type, json) { + return Fetcher.prototype.transKeyName(type, json); + } + + /* ----- 文件系统 ----- */ + + /** + * 保存文件到 java 端,并返回文件名 + * @param objUrl + * @param callback + * @param onError + */ + fileSave(objUrl = '', callback, onError) { + // 非 ObjectURL 跳过 + if (objUrl.indexOf('blob:') < 0) { + callback(objUrl); + return; + } + if (this._data.fileSaved[objUrl]) { + callback(this._data.fileSaved[objUrl]); + return; + } + // 分段存储 + const saveFileChunk = (baseData, index) => { + const writeData = { + fileName: baseData.fileName, + currentIdx: index, + totalIdx: baseData.total, + data: baseData.baseArr[index], + }; + this.invoking('img_write', writeData, res => { + if (res.result === false) { + Tools.toast('离线图片存储:' + res.msg); + onError({ + method: 'img_write', + request: { + fileName: writeData.fileName, + currentIdx: writeData.currentIdx, + totalIdx: writeData.total, + data: + (writeData.data || '').substr(0, 10) + + '...(共' + + (writeData.data || '').length + + '个base64字符)', + }, + response: res, + }); + return; + } + // 按分段递归保存 + if (index < baseData.total - 1) { + setTimeout(() => { + saveFileChunk(baseData, index + 1); + }, 10); + } + // 已完成 + else { + callback && callback('bridge:' + baseData.fileName); + } + }); + }; + $fileTrans.transObjUrlToBaseData(objUrl, baseData => { + this._data.fileSaved[objUrl] = 'bridge:' + baseData.fileName; + saveFileChunk(baseData, 0); + }); + } + + /** + * 从 java 读取文件 base64 + * @param bridgeName + * @param callback + * @param onError + */ + fileLoad(bridgeName = '', callback, onError) { + // 非存储地址,跳过 + if (bridgeName.indexOf('bridge:') < 0) { + callback(bridgeName); + return; + } + if (this._data.fileLoaded[bridgeName]) { + callback(this._data.fileLoaded[bridgeName]); + return; + } + const fileName = bridgeName.split(':')[1]; + const chunkSize = $fileTrans.getChunkSize(); + const baseArr = []; + let totalSize = 0; + let totalCount = 0; + const loadFileChunk = index => { + const loadData = { + fileName, + offset: chunkSize * index, + length: chunkSize, + }; + this.invoking('img_read', loadData, res => { + if (res.result === false) { + Tools.toast('离线图片读取:' + res.msg); + onError({ + method: 'img_read', + request: { + ...loadData, + totalCount, + }, + response: { + result: res.result, + msg: res.msg, + total_size: res.totalSize, + data: + (res.data || '').substr(0, 10) + + '...(共' + + (res.data || '').length + + '个base64字符)', + }, + }); + return; + } + if (totalSize === 0) { + totalSize = res.totalSize; + totalCount = Math.ceil(res.totalSize / chunkSize); + } + baseArr.push(res.data); + // 按分段递归读取 + if (totalCount > 1 && totalCount - 1 > index) { + loadFileChunk(index + 1); + } + // 读取完成 + else { + const baseData = { + baseArr, + fileName, + }; + try { + $fileTrans.transBaseDataToObjUrl(baseData, objUrl => { + this._data.fileLoaded[bridgeName] = objUrl; + callback && callback(objUrl); + }); + } catch (e) { + onError({ + method: 'img_read@merge_after_base64_loaded', + request: { + ...loadData, + totalCount, + }, + response: { + result: res.result, + msg: res.msg, + total_size: res.totalSize, + data: + (res.data || '').substr(0, 10) + + '...(共' + + (res.data || '').length + + '个base64字符)', + }, + base64Arr: baseData.baseArr.map( + baseItem => + (baseItem || []).substr(0, 10) + + '...(共' + + (res.data || '').length + + '个base64字符)', + ), + message: 'Base64合并解析异常!', + }); + } + } + }); + }; + loadFileChunk(0); + } + + /** + * 从 java 端移除文件 + * @param bridgeName + */ + fileRemove(bridgeName = '') { + if (bridgeName.indexOf('bridge:') < 0) { + return; + } + const fileName = bridgeName.split(':')[1]; + this.invoking('img_del', { fileName }); + // 移除 + Object.keys(this._data.fileSaved).forEach(objUrl => { + if (bridgeName === this._data.fileSaved[objUrl]) { + delete this._data.fileSaved[objUrl]; + } + }); + } + + /** + * 文件存储批量操作服务 + * @param type + * @param names + * @param callback + */ + fileStore(type, names = [], callback) { + if (!names || names.length === 0) { + callback && callback([]); + return; + } + if (typeof names === 'string') { + names = names.split(','); + } + // 保存 + if (type === 'save') { + const list = []; + const save = index => { + this.fileSave( + names[index], + bridgeName => { + list.push(bridgeName); + // 递归下一个 + if (index < names.length - 1) { + setTimeout(() => { + save(index + 1); + }, 10); + } + // 完成 + else { + callback && callback(list); + } + }, + err => { + callback && callback(null, err); + }, + ); + }; + save(0); + } + // 读取 + else if (type === 'load') { + const list = []; + const load = index => { + this.fileLoad( + names[index], + objUrl => { + list.push(objUrl); + // 递归下一个 + if (index < names.length - 1) { + setTimeout(() => { + load(index + 1); + }, 10); + } + // 完成 + else { + callback && callback(list); + } + }, + err => { + callback && callback(null, err); + }, + ); + }; + load(0); + } + // 移除 + else if (type === 'remove') { + names.forEach((name, index) => { + setTimeout(() => { + this.fileRemove(name); + }, 10 * index); + }); + } + } } // 全局服务实例 -- Gitblit v1.9.1