im-sdk.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. /**
  2. * IM 平台全端通用 Web-SDK (im-sdk.js)
  3. * 作用:统一子应用调用原生设备能力的接口,防止 WebView 直接调用引发的内存崩溃 (OOM)。
  4. * 版本:v1.0.0
  5. */
  6. (function(window, document) {
  7. // ---------------------------------------------------------
  8. // 1. 环境准备:确保注入 DCloud 官方 Webview 通信 SDK
  9. // ---------------------------------------------------------
  10. if (!window.uni || !window.uni.webView) {
  11. var script = document.createElement('script');
  12. script.type = 'text/javascript';
  13. // 动态引入 uni.webview.js
  14. script.src = 'https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js';
  15. document.head.appendChild(script);
  16. }
  17. // ---------------------------------------------------------
  18. // 2. 通信基建:回调注册与响应机制
  19. // ---------------------------------------------------------
  20. window.imBridgeCallbacks = {};
  21. /**
  22. * 供 App 壳子 (uni-app) 调用的全局方法,用于将原生执行结果塞回给 H5
  23. * @param {string} callbackId - 唯一回调标志
  24. * @param {object} data - 原生层返回的数据
  25. */
  26. window.imBridgeInvoke = function(callbackId, data) {
  27. if (window.imBridgeCallbacks[callbackId]) {
  28. // 执行业务代码中传入的 success/fail 回调
  29. window.imBridgeCallbacks[callbackId](data);
  30. // 执行完毕,销毁内存中的回调函数
  31. delete window.imBridgeCallbacks[callbackId];
  32. }
  33. };
  34. /**
  35. * 内部核心方法:向 App 壳子发送指令
  36. */
  37. function sendCommandToApp(action, params, callback) {
  38. var callbackId = 'cb_' + action + '_' + Date.now() + '_' + Math.floor(Math.random() * 1000);
  39. if (typeof callback === 'function') {
  40. window.imBridgeCallbacks[callbackId] = callback;
  41. }
  42. // 等待 uni.webview.js 就绪;CDN 失败时超时,避免无限轮询
  43. var attempts = 0;
  44. var maxAttempts = 50;
  45. var checkAndSend = function() {
  46. if (window.uni && window.uni.webView) {
  47. console.log('[imSDK] postMessage action=', action, 'callbackId=', callbackId);
  48. try {
  49. window.uni.webView.postMessage({
  50. data: Object.assign({ action: action, callbackId: callbackId }, params)
  51. });
  52. } catch (e) {
  53. console.error('[imSDK] postMessage 异常:', e);
  54. if (typeof callback === 'function') {
  55. delete window.imBridgeCallbacks[callbackId];
  56. callback({ errMsg: 'postMessage failed: ' + (e && e.message) });
  57. }
  58. }
  59. } else {
  60. attempts += 1;
  61. if (attempts > maxAttempts) {
  62. console.error('[imSDK] uni.webView 未就绪(请检查 CDN 或是否在 App 内),action=', action);
  63. if (typeof callback === 'function') {
  64. delete window.imBridgeCallbacks[callbackId];
  65. callback({ errMsg: 'uni.webView not ready' });
  66. }
  67. return;
  68. }
  69. setTimeout(checkAndSend, 100);
  70. }
  71. };
  72. checkAndSend();
  73. }
  74. /**
  75. * 是否在 UniApp / DCloud App WebView 内(走 postMessage,由壳子调 uni API)
  76. * 不可使用 uni.webView 判断:普通浏览器加载 uni.webview.js 后也会有该对象,会误判导致点击无反应。
  77. * Android 常见 UA 无 "uni-app" 字样,但有 Html5Plus。
  78. */
  79. function isUniAppIm() {
  80. if (window.__IN_UNIAPP__ === true) {
  81. console.log('[imSDK] isUniAppIm: true (__IN_UNIAPP__)');
  82. return true;
  83. }
  84. var ua = navigator.userAgent || '';
  85. if (/uni-app/i.test(ua)) {
  86. console.log('[imSDK] isUniAppIm: true (UA: uni-app)');
  87. return true;
  88. }
  89. if (/Html5Plus/i.test(ua)) {
  90. console.log('[imSDK] isUniAppIm: true (UA: Html5Plus / DCloud Android 等)');
  91. return true;
  92. }
  93. console.log('[imSDK] isUniAppIm: false — 使用浏览器 file。UA:', ua.slice(0, 160));
  94. return false;
  95. }
  96. // ---------------------------------------------------------
  97. // 3. 平台标准 API (供子应用开发者主动调用)
  98. // ---------------------------------------------------------
  99. window.imSDK = {
  100. /** @returns {boolean} */
  101. isUniAppIm: isUniAppIm,
  102. /**
  103. * 调起原生相机拍照或选择相册 (返回本地路径)
  104. */
  105. chooseImage: function(options) {
  106. options = options || {};
  107. sendCommandToApp('chooseImage', {
  108. count: options.count || 1,
  109. sourceType: options.sourceType || ['camera', 'album']
  110. }, options.success);
  111. },
  112. /**
  113. * 调起原生相机录像或选择视频 (返回本地路径)
  114. */
  115. chooseVideo: function(options) {
  116. options = options || {};
  117. sendCommandToApp('chooseVideo', {
  118. sourceType: options.sourceType || ['camera', 'album'],
  119. maxDuration: options.maxDuration || 60,
  120. camera: options.camera || 'back'
  121. }, options.success);
  122. },
  123. /**
  124. * 选择系统文件 (返回本地路径)
  125. */
  126. chooseFile: function(options) {
  127. options = options || {};
  128. sendCommandToApp('chooseFile', {
  129. count: options.count || 1,
  130. extension: options.extension || []
  131. }, options.success);
  132. },
  133. /**
  134. * 【核心】将本地路径委托给原生 App 进行上传,防止大文件导致 H5 内存溢出
  135. */
  136. uploadFile: function(options) {
  137. options = options || {};
  138. if (!options.url || !options.filePath) {
  139. console.error('[imSDK] uploadFile 缺少必填参数: url 或 filePath');
  140. return;
  141. }
  142. sendCommandToApp('uploadFile', {
  143. url: options.url,
  144. filePath: options.filePath,
  145. name: options.name || 'file',
  146. formData: options.formData || {},
  147. header: options.header || {}
  148. }, options.success);
  149. }
  150. };
  151. // ---------------------------------------------------------
  152. // 4. 终极防线:全局 DOM 劫持 (保护老旧/不规范的 H5 页面)
  153. // ---------------------------------------------------------
  154. document.addEventListener('click', function(event) {
  155. var target = event.target;
  156. // 锁定目标:点击了 <input type="file">
  157. if (target.tagName && target.tagName.toUpperCase() === 'INPUT' && target.type === 'file') {
  158. // 在 UniApp WebView 内则拦截,避免系统 file 选择器;普通浏览器放行
  159. if (isUniAppIm()) {
  160. // 【核心防御】拦截浏览器默认调起系统相册/相机的行为
  161. event.preventDefault();
  162. event.stopPropagation();
  163. console.log('[imSDK] 已拦截危险的原生 input 调用,正转交底层处理...');
  164. // 判断 input 是想选图片还是视频
  165. var isVideo = target.accept && target.accept.indexOf('video') !== -1;
  166. var action = isVideo ? 'chooseVideo' : 'chooseImage';
  167. var sourceType = target.hasAttribute('capture') ? ['camera'] : ['camera', 'album'];
  168. // 强制转为调用我们的标准 API
  169. window.imSDK[action]({
  170. sourceType: sourceType,
  171. success: function(res) {
  172. // 注意:这里拿到的是 App 传回的本地路径 (tempFilePath)。
  173. // 如果是老旧 HTML 页面,强行重写 DOM 属性可能受限于浏览器跨域安全策略。
  174. // 这里派发一个自定义事件,供高级开发者捕获;普通业务建议直接改用 imSDK.chooseImage。
  175. console.log('[imSDK] 劫持执行完毕,获取到原生路径:', res);
  176. var customEvent = new CustomEvent('im-file-choosed', { detail: res });
  177. target.dispatchEvent(customEvent);
  178. }
  179. });
  180. }
  181. }
  182. }, true); // true: 捕获阶段拦截,最高优先级
  183. })(window, document);