From faeeef86559ed8f0cfcf9c7f257853371be09d2d Mon Sep 17 00:00:00 2001 From: feige996 <1020102647@qq.com> Date: Sun, 15 Jun 2025 16:36:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BC=95=E5=85=A5devTools,=20=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E5=90=84=E7=A7=8D=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages.config.ts | 22 + src/devTools/READEME.MD | 9 + src/devTools/config.js | 69 + src/devTools/core/components/mpDevBubble.vue | 156 ++ src/devTools/core/libs/createH5Bubble.js | 142 ++ src/devTools/core/libs/devCache.js | 131 ++ src/devTools/core/libs/devOptions.js | 184 ++ src/devTools/core/libs/drawView.js | 118 ++ src/devTools/core/libs/errorReport.js | 60 + src/devTools/core/libs/jsonCompress.js | 333 +++ src/devTools/core/libs/logReport.js | 63 + src/devTools/core/libs/pageLinkList.js | 124 ++ src/devTools/core/libs/pageStatistics.js | 71 + src/devTools/core/libs/timeFormat.js | 94 + src/devTools/core/proxy/console.js | 413 ++++ src/devTools/core/proxy/index.js | 33 + src/devTools/core/proxy/request.js | 246 +++ src/devTools/core/proxy/storage.js | 96 + src/devTools/core/proxy/uniBus.js | 160 ++ src/devTools/core/proxy/uniListen.js | 195 ++ src/devTools/core/proxy/vueMixin.js | 160 ++ src/devTools/index.js | 177 ++ src/devTools/page/components/bottomTools.vue | 914 +++++++++ .../page/components/dialog/addStorage.vue | 185 ++ .../page/components/dialog/createDir.vue | 356 ++++ .../components/dialog/dayOnlinePageList.vue | 202 ++ .../page/components/dialog/editDialog.vue | 178 ++ .../page/components/dialog/routeDialog.vue | 169 ++ .../page/components/dialog/sendRequest.vue | 443 ++++ .../components/dialog/textFileEditDialog.vue | 361 ++++ .../page/components/libs/appDelDir.js | 77 + .../page/components/libs/dirReader.js | 216 ++ src/devTools/page/components/libs/fileSize.js | 15 + .../page/components/libs/getRuntimeInfo.js | 132 ++ .../page/components/listItem/consoleItem.vue | 271 +++ .../components/listItem/dayOnlineItem.vue | 89 + .../page/components/listItem/errorItem.vue | 265 +++ .../page/components/listItem/fileSysItem.vue | 380 ++++ .../page/components/listItem/infoList.vue | 62 + .../page/components/listItem/jsRunnerItem.vue | 197 ++ .../page/components/listItem/logItem.vue | 140 ++ .../page/components/listItem/networkItem.vue | 330 +++ .../components/listItem/objectAnalysis.vue | 731 +++++++ .../page/components/listItem/pageItem.vue | 99 + .../page/components/listItem/pages.vue | 194 ++ .../page/components/listItem/routeItem.vue | 74 + .../page/components/listItem/setting.vue | 746 +++++++ .../page/components/listItem/storageList.vue | 313 +++ .../page/components/listItem/tools.vue | 292 +++ .../page/components/listItem/vuexList.vue | 196 ++ src/devTools/page/components/main.vue | 1777 +++++++++++++++++ .../components/mixins/animationControl.js | 80 + src/devTools/page/components/mixins/mp.js | 77 + src/devTools/page/components/ui/btnTabs.vue | 68 + .../page/components/ui/codeHisPicker.vue | 161 ++ src/devTools/page/components/ui/h5Cell.vue | 7 + src/devTools/page/components/ui/menuBtn.vue | 218 ++ .../page/components/ui/mobileSwiperScroll.vue | 158 ++ .../page/components/ui/requestSpeedLimit.vue | 47 + .../page/components/ui/requestTimeoutMock.vue | 50 + .../page/components/ui/subTitleBar.vue | 86 + src/devTools/page/index.nvue | 77 + src/devTools/page/static/copy.png | Bin 0 -> 772 bytes src/devTools/page/static/delete.png | Bin 0 -> 761 bytes src/devTools/page/static/fileSys/AI.png | Bin 0 -> 596 bytes src/devTools/page/static/fileSys/DWG.png | Bin 0 -> 841 bytes src/devTools/page/static/fileSys/EXE.png | Bin 0 -> 664 bytes src/devTools/page/static/fileSys/GIF.png | Bin 0 -> 595 bytes src/devTools/page/static/fileSys/HTML.png | Bin 0 -> 616 bytes src/devTools/page/static/fileSys/PSD.png | Bin 0 -> 676 bytes src/devTools/page/static/fileSys/RVT.png | Bin 0 -> 693 bytes src/devTools/page/static/fileSys/SKP.png | Bin 0 -> 762 bytes src/devTools/page/static/fileSys/SVG.png | Bin 0 -> 877 bytes src/devTools/page/static/fileSys/excel.png | Bin 0 -> 637 bytes src/devTools/page/static/fileSys/pdf.png | Bin 0 -> 641 bytes src/devTools/page/static/fileSys/pptl.png | Bin 0 -> 503 bytes src/devTools/page/static/fileSys/shipin.png | Bin 0 -> 444 bytes src/devTools/page/static/fileSys/tupian.png | Bin 0 -> 532 bytes src/devTools/page/static/fileSys/txt.png | Bin 0 -> 404 bytes .../page/static/fileSys/weizhiwenjian.png | Bin 0 -> 538 bytes .../page/static/fileSys/wenjianjia.png | Bin 0 -> 255 bytes src/devTools/page/static/fileSys/word.png | Bin 0 -> 740 bytes src/devTools/page/static/fileSys/yasuo.png | Bin 0 -> 487 bytes src/devTools/page/static/fileSys/yinpin.png | Bin 0 -> 626 bytes src/devTools/page/static/fold.png | Bin 0 -> 543 bytes src/devTools/page/static/menu.png | Bin 0 -> 592 bytes src/devTools/page/static/refresh.png | Bin 0 -> 1089 bytes src/devTools/page/static/unfold.png | Bin 0 -> 561 bytes src/devTools/tools.vue | 22 + src/main.ts | 13 + src/pages.json | 23 +- src/pages/about/about.vue | 1 + 92 files changed, 13546 insertions(+), 2 deletions(-) create mode 100644 src/devTools/READEME.MD create mode 100644 src/devTools/config.js create mode 100644 src/devTools/core/components/mpDevBubble.vue create mode 100644 src/devTools/core/libs/createH5Bubble.js create mode 100644 src/devTools/core/libs/devCache.js create mode 100644 src/devTools/core/libs/devOptions.js create mode 100644 src/devTools/core/libs/drawView.js create mode 100644 src/devTools/core/libs/errorReport.js create mode 100644 src/devTools/core/libs/jsonCompress.js create mode 100644 src/devTools/core/libs/logReport.js create mode 100644 src/devTools/core/libs/pageLinkList.js create mode 100644 src/devTools/core/libs/pageStatistics.js create mode 100644 src/devTools/core/libs/timeFormat.js create mode 100644 src/devTools/core/proxy/console.js create mode 100644 src/devTools/core/proxy/index.js create mode 100644 src/devTools/core/proxy/request.js create mode 100644 src/devTools/core/proxy/storage.js create mode 100644 src/devTools/core/proxy/uniBus.js create mode 100644 src/devTools/core/proxy/uniListen.js create mode 100644 src/devTools/core/proxy/vueMixin.js create mode 100644 src/devTools/index.js create mode 100644 src/devTools/page/components/bottomTools.vue create mode 100644 src/devTools/page/components/dialog/addStorage.vue create mode 100644 src/devTools/page/components/dialog/createDir.vue create mode 100644 src/devTools/page/components/dialog/dayOnlinePageList.vue create mode 100644 src/devTools/page/components/dialog/editDialog.vue create mode 100644 src/devTools/page/components/dialog/routeDialog.vue create mode 100644 src/devTools/page/components/dialog/sendRequest.vue create mode 100644 src/devTools/page/components/dialog/textFileEditDialog.vue create mode 100644 src/devTools/page/components/libs/appDelDir.js create mode 100644 src/devTools/page/components/libs/dirReader.js create mode 100644 src/devTools/page/components/libs/fileSize.js create mode 100644 src/devTools/page/components/libs/getRuntimeInfo.js create mode 100644 src/devTools/page/components/listItem/consoleItem.vue create mode 100644 src/devTools/page/components/listItem/dayOnlineItem.vue create mode 100644 src/devTools/page/components/listItem/errorItem.vue create mode 100644 src/devTools/page/components/listItem/fileSysItem.vue create mode 100644 src/devTools/page/components/listItem/infoList.vue create mode 100644 src/devTools/page/components/listItem/jsRunnerItem.vue create mode 100644 src/devTools/page/components/listItem/logItem.vue create mode 100644 src/devTools/page/components/listItem/networkItem.vue create mode 100644 src/devTools/page/components/listItem/objectAnalysis.vue create mode 100644 src/devTools/page/components/listItem/pageItem.vue create mode 100644 src/devTools/page/components/listItem/pages.vue create mode 100644 src/devTools/page/components/listItem/routeItem.vue create mode 100644 src/devTools/page/components/listItem/setting.vue create mode 100644 src/devTools/page/components/listItem/storageList.vue create mode 100644 src/devTools/page/components/listItem/tools.vue create mode 100644 src/devTools/page/components/listItem/vuexList.vue create mode 100644 src/devTools/page/components/main.vue create mode 100644 src/devTools/page/components/mixins/animationControl.js create mode 100644 src/devTools/page/components/mixins/mp.js create mode 100644 src/devTools/page/components/ui/btnTabs.vue create mode 100644 src/devTools/page/components/ui/codeHisPicker.vue create mode 100644 src/devTools/page/components/ui/h5Cell.vue create mode 100644 src/devTools/page/components/ui/menuBtn.vue create mode 100644 src/devTools/page/components/ui/mobileSwiperScroll.vue create mode 100644 src/devTools/page/components/ui/requestSpeedLimit.vue create mode 100644 src/devTools/page/components/ui/requestTimeoutMock.vue create mode 100644 src/devTools/page/components/ui/subTitleBar.vue create mode 100644 src/devTools/page/index.nvue create mode 100644 src/devTools/page/static/copy.png create mode 100644 src/devTools/page/static/delete.png create mode 100644 src/devTools/page/static/fileSys/AI.png create mode 100644 src/devTools/page/static/fileSys/DWG.png create mode 100644 src/devTools/page/static/fileSys/EXE.png create mode 100644 src/devTools/page/static/fileSys/GIF.png create mode 100644 src/devTools/page/static/fileSys/HTML.png create mode 100644 src/devTools/page/static/fileSys/PSD.png create mode 100644 src/devTools/page/static/fileSys/RVT.png create mode 100644 src/devTools/page/static/fileSys/SKP.png create mode 100644 src/devTools/page/static/fileSys/SVG.png create mode 100644 src/devTools/page/static/fileSys/excel.png create mode 100644 src/devTools/page/static/fileSys/pdf.png create mode 100644 src/devTools/page/static/fileSys/pptl.png create mode 100644 src/devTools/page/static/fileSys/shipin.png create mode 100644 src/devTools/page/static/fileSys/tupian.png create mode 100644 src/devTools/page/static/fileSys/txt.png create mode 100644 src/devTools/page/static/fileSys/weizhiwenjian.png create mode 100644 src/devTools/page/static/fileSys/wenjianjia.png create mode 100644 src/devTools/page/static/fileSys/word.png create mode 100644 src/devTools/page/static/fileSys/yasuo.png create mode 100644 src/devTools/page/static/fileSys/yinpin.png create mode 100644 src/devTools/page/static/fold.png create mode 100644 src/devTools/page/static/menu.png create mode 100644 src/devTools/page/static/refresh.png create mode 100644 src/devTools/page/static/unfold.png create mode 100644 src/devTools/tools.vue diff --git a/pages.config.ts b/pages.config.ts index 547d0da..383d5fc 100644 --- a/pages.config.ts +++ b/pages.config.ts @@ -48,4 +48,26 @@ export default defineUniPages({ }, ], }, + subPackages: [ + { + root: 'devTools/page', + pages: [ + { + path: 'index', + style: { + navigationStyle: 'custom', + // #ifdef APP-PLUS + softinputMode: 'adjustResize', + // backgroundColor: 'transparent', + animationDuration: 1, + animationType: 'none', + popGesture: 'none', + bounce: 'none', + titleNView: false, + // #endif + }, + }, + ], + }, + ], }) diff --git a/src/devTools/READEME.MD b/src/devTools/READEME.MD new file mode 100644 index 0000000..a0a48e1 --- /dev/null +++ b/src/devTools/READEME.MD @@ -0,0 +1,9 @@ +# UniDevTools - 调试工具 + +支持 Vue2+Vue3 的跨平台调试工具 + +> 文档&安装教程 https://dev.api0.cn/ + +当前版本:v3.8 + +更新日期:2025.5.5 diff --git a/src/devTools/config.js b/src/devTools/config.js new file mode 100644 index 0000000..77ab4dd --- /dev/null +++ b/src/devTools/config.js @@ -0,0 +1,69 @@ +let config = { + status: true, //调试工具总开关 + route: '/devTools/page/index', // 调试页面的路由,不建议更改 + bubble: { + //调试弹窗气泡设置 + status: true, // 气泡标签是否显示,生产环境建议关闭 + text: 'DevTools', // 气泡上展示的文字 + color: '#ffffff', // 气泡文字颜色 + bgColor: 'rgba(250, 53, 52,0.7)', // 气泡背景颜色 + }, + + // 注意: 以下配置不建议更改 + + pageStatistics: { + // 页面统计开关 + status: true, // 统计状态开关 + size: 1024 * 100, // 缓存上限,单位byte + dayOnlineRowMax: 30, // 活跃数据缓存天数 + }, + console: { + //console日志配置 + status: true, //功能总开关 + isOutput: true, //打印的日志是否对外输出到浏览器调试界面,建议在生产环境时关闭 + cache: { + status: true, //是否启用本地缓存 + size: 512 * 1024, //缓存上限,单位byte + rowSize: 1024 * 4, //单条记录缓存上限,单位byte + }, + }, + uniBus: { + // uni event bus 监听设置 + status: true, + cache: { + status: true, + size: 1024 * 512, // bus调用日志上限 byte + rowSize: 1024 * 10, + countMaxSize: 1024 * 10, // bus统计上限 byte + }, + }, + error: { + //报错拦截配置 + status: true, + cache: { + status: true, + size: 512 * 1024, + rowSize: 1024 * 4, + }, + }, + network: { + //请求拦截配置 + status: true, + cache: { + status: true, + size: 512 * 1024, + rowSize: 1024 * 4, + }, + }, + logs: { + //运行日志 + status: true, + cache: { + status: true, + size: 512 * 1024, + rowSize: 1024 * 4, + }, + }, +} + +export default config diff --git a/src/devTools/core/components/mpDevBubble.vue b/src/devTools/core/components/mpDevBubble.vue new file mode 100644 index 0000000..593db89 --- /dev/null +++ b/src/devTools/core/components/mpDevBubble.vue @@ -0,0 +1,156 @@ + + + diff --git a/src/devTools/core/libs/createH5Bubble.js b/src/devTools/core/libs/createH5Bubble.js new file mode 100644 index 0000000..aeb6523 --- /dev/null +++ b/src/devTools/core/libs/createH5Bubble.js @@ -0,0 +1,142 @@ +/** + *! 创建h5页面上拖动的气泡标签 + */ +function createH5Bubble(options, devTools) { + let tagConfig = localStorage.getItem('devTools_tagConfig') + if (!tagConfig) { + tagConfig = {} + } else { + tagConfig = JSON.parse(tagConfig) + } + + tagConfig = Object.assign( + { + show: options.bubble.status, + x: window.innerWidth - 90, + y: window.innerHeight - 90, + }, + tagConfig, + ) + + tagConfig.show = options.bubble.status + + // 拖动范围限制 + let dragLimit = { + min: { x: 0, y: 0 }, + max: { + x: window.innerWidth - 70, + y: window.innerHeight - 24, + }, + } + + tagConfig.x = Math.min(Math.max(tagConfig.x, dragLimit.min.x), dragLimit.max.x) + tagConfig.y = Math.min(Math.max(tagConfig.y, dragLimit.min.y), dragLimit.max.y) + + let tag = document.createElement('div') + tag.style = ` + box-sizing: border-box; + position: fixed; + z-index: 9999999; + left: ${tagConfig.x}px; + top: ${tagConfig.y}px; + width: 70px; + height: 24px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4px; + border-radius: 6px; + background-color: ${options.bubble.bgColor}; + color: ${options.bubble.color}; + font-size: 10px; + cursor: grab; + box-shadow: 0px 0px 6px ${options.bubble.bgColor}; + backdrop-filter: blur(1px); + ` + tag.innerHTML = options.bubble.text + tag.setAttribute('id', 'debugTag') + + if (tagConfig.show) { + document.body.appendChild(tag) + } + + /** + * 标签单击事件 + */ + function tagClick() { + let pages = getCurrentPages() + let route = options.route.substring(1, options.route.length - 2) + if (pages[pages.length - 1].route == route) { + // 已经处于debug页面,不响应点击事件 + return + } + devTools.show() + } + + let isTouch = false + let touchStartPoint = { + clientX: 0, + clientY: 0, + tagX: tagConfig.x, + tagY: tagConfig.y, + hasMove: false, + } + + function touchStart(e) { + if (isTouch) return + if (e.preventDefault) { + e.preventDefault() + } + let clientX = e.clientX ? e.clientX : e.targetTouches[0].clientX + let clientY = e.clientX ? e.clientY : e.targetTouches[0].clientY + touchStartPoint.clientX = clientX + touchStartPoint.clientY = clientY + touchStartPoint.tagX = tagConfig.x + touchStartPoint.tagY = tagConfig.y + touchStartPoint.hasMove = false + isTouch = true + } + function touchMove(e) { + if (!isTouch) return + if (e.preventDefault) { + e.preventDefault() + } + let clientX = e.clientX ? e.clientX : e.targetTouches[0].clientX + let clientY = e.clientX ? e.clientY : e.targetTouches[0].clientY + touchStartPoint.hasMove = true + + let offsetX = touchStartPoint.clientX - clientX + let offsetY = touchStartPoint.clientY - clientY + let tx = touchStartPoint.tagX - offsetX + let ty = touchStartPoint.tagY - offsetY + tx = Math.min(Math.max(tx, dragLimit.min.x), dragLimit.max.x) + ty = Math.min(Math.max(ty, dragLimit.min.y), dragLimit.max.y) + tag.style.left = `${tx}px` + tag.style.top = `${ty}px` + tagConfig.x = tx + tagConfig.y = ty + } + function touchEnd(e) { + if (!isTouch) return + if (e.preventDefault) { + e.preventDefault() + } + isTouch = false + localStorage.setItem('devTools_tagConfig', JSON.stringify(tagConfig)) + if (!touchStartPoint.hasMove) { + tagClick() + } + } + tag.addEventListener('touchstart', touchStart) + tag.addEventListener('touchmove', touchMove) + tag.addEventListener('touchend', touchEnd) + + tag.addEventListener('mousedown', touchStart) + document.addEventListener('mousemove', touchMove) + document.addEventListener('mouseup', touchEnd) + + localStorage.setItem('devTools_tagConfig', JSON.stringify(tagConfig)) +} + +export default createH5Bubble diff --git a/src/devTools/core/libs/devCache.js b/src/devTools/core/libs/devCache.js new file mode 100644 index 0000000..d735d57 --- /dev/null +++ b/src/devTools/core/libs/devCache.js @@ -0,0 +1,131 @@ +import devOptions from './devOptions' +/** + * dev工具缓存管理 + */ +export default { + /** + * 存储的键开始名称 + */ + cacheKey: 'devTools_v3_', + options: null, + /** + * 临时缓存对象 + */ + tempCache: { + errorReport: [], + logReport: [], + console: [], + request: [], + uniBus: [], + }, + /** + * 临时数据存放 + */ + tempData: {}, + /** + * 向缓存内写入数据 + */ + set(key, value) { + try { + if (['errorReport', 'logReport', 'console', 'request', 'uniBus'].indexOf(key) != -1) { + let setting = this.getLongListSetting(key) + if (!setting.status) return + if (!setting.cache.status) { + // !不使用缓存 + this.tempCache[key] = value + return + } + } + key = `${this.cacheKey}${key}` + + // #ifdef APP-NVUE + let pages = getCurrentPages() + if (pages[pages.length - 1].route == 'devTools/page/index') { + // devtools 页面直接走设置缓存 + return uni.setStorageSync(key, value) + } + // #endif + + this.tempData[key] = value + } catch (error) { + console.log('devCache.set error', error) + } + }, + /** + * 同步读取缓存数据 + */ + get(key) { + try { + if (['errorReport', 'logReport', 'console', 'request', 'uniBus'].indexOf(key) != -1) { + let setting = this.getLongListSetting(key) + if (!setting.status) return [] + if (!setting.cache.status) { + // !不使用缓存 + return this.tempCache[key] + } + } + key = `${this.cacheKey}${key}` + + // #ifdef APP-NVUE + let pages = getCurrentPages() + if (pages[pages.length - 1].route == 'devTools/page/index') { + // devtools 页面直接走设置缓存 + return uni.getStorageSync(key) + } + // #endif + + if (this.tempData.hasOwnProperty(key)) { + return this.tempData[key] + } else { + let value = uni.getStorageSync(key) + this.tempData[key] = value + return value + } + } catch (error) { + console.log('devCache.get error', error) + return '' + } + }, + getLongListSetting(key) { + let optionsKey = { + errorReport: 'error', + logReport: 'logs', + console: 'console', + request: 'network', + uniBus: 'uniBus', + } + if (this.options) return this.options[optionsKey[key]] + this.options = devOptions.getOptions() + return this.options[optionsKey[key]] + }, + /** + * 同步本地缓存 + */ + syncLocalCache() { + let that = this + setTimeout( + () => { + try { + let waitSetKeys = Object.keys(that.tempData) + for (let i = 0; i < waitSetKeys.length; i++) { + const key = waitSetKeys[i] + uni.setStorage({ + key, + data: that.tempData[key], + success() { + // console.log("set " + key + " success,length=" + that.tempData[key].length); + delete that.tempData[key] + }, + }) + } + } catch (error) { + console.log('devCache error: ', error) + } + setTimeout(() => { + that.syncLocalCache() + }, 500) + }, + Math.round(Math.random() * 3 * 1000) + 2000, + ) + }, +} diff --git a/src/devTools/core/libs/devOptions.js b/src/devTools/core/libs/devOptions.js new file mode 100644 index 0000000..d8efb72 --- /dev/null +++ b/src/devTools/core/libs/devOptions.js @@ -0,0 +1,184 @@ +import devCache from './devCache' + +/** + * 设置各端大小 kb + */ +const defSize = (h5, app, mp) => { + let r = 0 + // #ifdef H5 + r = h5 + // #endif + // #ifdef MP + r = mp + // #endif + // #ifdef APP-PLUS || APP-NVUE + r = app + // #endif + return Math.ceil(r * 1024) +} + +/** + * 获取配置 + */ +export default { + /** + * 配置缓存key + */ + cacheKey: 'options_v8', + /** + * 默认配置项 + */ + defaultOptions: { + version: 3.81, + status: false, //调试工具总开关 + route: '/devTools/page/index', // 调试页面的路由,不建议更改 + bubble: { + //调试弹窗气泡设置 + status: false, // 气泡标签是否显示,生产环境建议关闭 + text: '调试工具', // 气泡上展示的文字 + color: '#ffffff', // 气泡文字颜色 + bgColor: 'rgba(250, 53, 52,0.7)', // 气泡背景颜色 + }, + console: { + status: true, // 开关 + isOutput: true, //打印的日志是否对外输出到浏览器调试界面,建议在生产环境时开启 + cache: { + status: true, //是否启用console缓存 + size: defSize(512, 1024 * 2, 512), + rowSize: defSize(5.12, 20, 10), + }, + }, + error: { + status: true, + cache: { + status: true, + size: defSize(512, 1024 * 2, 512), + rowSize: defSize(5.12, 20, 10), + }, + }, + network: { + status: true, + cache: { + status: true, + size: defSize(512, 1024 * 2, 512), + rowSize: defSize(5.12, 20, 10), + }, + }, + logs: { + status: true, + cache: { + status: true, + size: defSize(512, 1024 * 2, 512), + rowSize: defSize(0.4, 0.4, 0.4), + }, + }, + // 页面统计开关 + pageStatistics: { + status: true, // 统计状态开关 + size: defSize(200, 1024 * 2, 512), + // #ifdef H5 + dayOnlineRowMax: 30, // 日活跃时间的保存条数 + // #endif + // #ifdef APP-PLUS || APP-NVUE + dayOnlineRowMax: 90, // 日活跃时间的保存条数 + // #endif + // #ifdef MP-WEIXIN + dayOnlineRowMax: 60, // 日活跃时间的保存条数 + // #endif + }, + // uni event bus 监听设置 + uniBus: { + status: true, + cache: { + status: true, + size: defSize(512, 1024 * 2, 512), + rowSize: defSize(5.12, 20, 10), + countMaxSize: defSize(512, 1024 * 2, 512), // bus统计上限 kb + }, + }, + }, + /** + * 获取配置信息 + */ + getOptions() { + try { + let options = devCache.get(this.cacheKey) + if (!options) { + return { + status: false, //默认关闭调试工具 + } + } + let r = String(options.route) + // ! 增加 devRoute 参数 + options.devRoute = r.substring(1, r.length) + return options + } catch (error) { + console.log('devOptions.getOptions error: ', error) + return { + status: false, //默认关闭调试工具 + } + } + }, + /** + * 保存配置项 + */ + setOptions(options) { + try { + if (!options) { + options = this.defaultOptions + } + + if (options.status) { + if (!options.route || typeof options.route != 'string' || options.route.indexOf('/') != 0) { + return this.outputError(`devTools 调试工具配置出错: [route] 参数配置错误!`) + } + } + + let data = deepMerge(this.defaultOptions, options) + + devCache.set(this.cacheKey, data) + } catch (error) { + console.log('devOptions.setOptions error: ', error) + } + }, + /** + * 弹出错误信息 + */ + outputError(msg) { + console.log( + '%c' + msg, + ` + padding: 4px; + background-color: red; + color: #fff; + font-size: 15px; + `, + ) + }, +} + +/** + * 深度合并对象 + */ +function deepMerge(target, ...sources) { + try { + if (!sources.length) return target // 如果没有源对象则直接返回目标对象 + + const source = sources[0] + + for (let key in source) { + if (source.hasOwnProperty(key)) { + if (typeof source[key] === 'object' && typeof target[key] !== 'undefined') { + target[key] = deepMerge({}, target[key], source[key]) // 若属性值为对象类型且目标对象已存在该属性,则递归调用deepMerge函数进行合并 + } else { + target[key] = source[key] // 否则将源对象的属性赋值到目标对象上 + } + } + } + + return deepMerge(target, ...sources.slice(1)) // 处理完第一个源对象后再次调用deepMerge函数处理其他源对象 + } catch (error) { + console.log('deepMerge error', error) + return {} + } +} diff --git a/src/devTools/core/libs/drawView.js b/src/devTools/core/libs/drawView.js new file mode 100644 index 0000000..96f5491 --- /dev/null +++ b/src/devTools/core/libs/drawView.js @@ -0,0 +1,118 @@ +/** + * 绘制调试工具 + */ + +/** + * 入口文件 + */ +function init(options, devTools) { + let sysInfo = uni.getSystemInfoSync() + + let tagConfig = uni.getStorageSync('devTools_tagConfig') + if (!tagConfig) { + tagConfig = {} + } + + tagConfig = Object.assign( + { + show: options.bubble.status, + x: sysInfo.screenWidth - 90, + y: sysInfo.screenHeight - 90, + }, + tagConfig, + ) + tagConfig.show = options.bubble.status + + // 拖动范围限制 + let dragLimit = { + min: { x: 0, y: 0 }, + max: { + x: sysInfo.screenWidth - 70, + y: sysInfo.screenHeight - 24, + }, + } + + let view = new plus.nativeObj.View('debugTag', { + top: tagConfig.y + 'px', + left: tagConfig.x + 'px', + height: '24px', + width: '70px', + backgroundColor: options.bubble.bgColor, + }) + view.drawText( + options.bubble.text, + {}, + { + size: '12px', + color: options.bubble.color, + weight: 'bold', + }, + ) + + if (tagConfig.show) { + view.show() + } + + let isTouch = false + + let touchStart = { + l: 0, + t: 0, + x: 0, + y: 0, + time: 0, + hasMove: false, + } + + view.addEventListener('touchstart', (e) => { + isTouch = true + touchStart.l = e.clientX + touchStart.t = e.clientY + touchStart.time = new Date().getTime() + touchStart.hasMove = false + }) + + view.addEventListener('touchmove', (e) => { + if (!isTouch) return + if (!touchStart.hasMove) { + touchStart.hasMove = true + } + let x = e.screenX - touchStart.l + let y = e.screenY - touchStart.t + x = Math.min(Math.max(x, dragLimit.min.x), dragLimit.max.x) + y = Math.min(Math.max(y, dragLimit.min.y), dragLimit.max.y) + + view.setStyle({ + top: y + 'px', + left: x + 'px', + }) + touchStart.x = x + touchStart.y = y + }) + + view.addEventListener('touchend', (e) => { + isTouch = false + if (!touchStart.hasMove || touchStart.time > new Date().getTime() - 300) { + // 单击事件 + + let pages = getCurrentPages() + let route = options.route.substring(1, options.route.length - 2) + if (pages[pages.length - 1].route == route) { + // 已经处于debug页面,不响应点击事件 + return + } + devTools.show() + } else { + //拖拽结束事件 + + tagConfig.x = touchStart.x + tagConfig.y = touchStart.y + + uni.setStorageSync('devTools_tagConfig', tagConfig) + } + }) + + uni.setStorageSync('devTools_tagConfig', tagConfig) +} + +export default init diff --git a/src/devTools/core/libs/errorReport.js b/src/devTools/core/libs/errorReport.js new file mode 100644 index 0000000..c94c6cc --- /dev/null +++ b/src/devTools/core/libs/errorReport.js @@ -0,0 +1,60 @@ +import devCache from './devCache' +import devOptions from './devOptions' +import jsonCompress from './jsonCompress' +/** + * ! vue报错捕获 + */ + +/** + * * vue错误日志上报 + * @param {'ve'|'vw'|'oe'|'n'} type 错误类型 + */ +function errorReport(msg, trace, type = 'n') { + try { + if (!msg) return false + + if (msg instanceof Error) { + msg = msg.message + } + let options = devOptions.getOptions() + if (!options.error.status) return + + let page = '未知' + try { + let pages = getCurrentPages() + let item = pages[pages.length - 1] + if (item && item.route) { + page = item.route + } + } catch (error) {} + + let logs = devCache.get('errorReport') + if (!logs) logs = [] + if (logs.length >= options.error.cache.rowMax) { + logs = logs.splice(0, options.error.cache.rowMax) + } + + msg = String(msg) + msg = jsonCompress.compressObject(msg, options.error.cache.rowSize / 2) + trace = String(trace) + trace = jsonCompress.compressObject(trace, options.error.cache.rowSize / 2) + + logs.unshift({ + t: new Date().getTime(), + m: msg, + tr: trace, + p: page, + type, + }) + + console.error('__ignoreReport__', msg, trace) + + logs = jsonCompress.compressArray(logs, 'end', options.error.cache.size) + + devCache.set('errorReport', logs) + } catch (error) { + console.log('errorReport error: ', error) + } +} + +export default errorReport diff --git a/src/devTools/core/libs/jsonCompress.js b/src/devTools/core/libs/jsonCompress.js new file mode 100644 index 0000000..f93efcb --- /dev/null +++ b/src/devTools/core/libs/jsonCompress.js @@ -0,0 +1,333 @@ +/** + * json压缩工具 + */ +export default { + /** + * 压缩js对象成json字符串,并控制json字节大小,多余部分裁剪 + */ + compressObject(obj = {}, maxSize = 1024 * 10.24) { + try { + if (obj === undefined || obj === null) return obj + if (typeof obj == 'string') { + return this.truncateStrBySize(obj, maxSize) + } + if (typeof obj == 'number') { + return obj + } + + let t = new Date().getTime() + + const type = typeof obj + + if (type === 'symbol') { + obj = 'Symbol->' + obj.toString() + } else if (type === 'bigint') { + obj = 'bigint->' + obj.toString() + } else if (typeof Error != 'undefined' && obj instanceof Error) { + obj = `Error->${obj.name}\n${obj.message}\n${obj.stack}` + } else if (typeof Date != 'undefined' && obj instanceof Date) { + obj = 'Date->' + obj.toISOString() + } else if (typeof obj == 'function') { + obj = 'Function->' + obj.toString() + } else if (typeof RegExp != 'undefined' && obj instanceof RegExp) { + obj = 'RegExp->' + obj.toString() + } else if (typeof Map != 'undefined' && obj instanceof Map) { + obj = `Map->(${obj.size}) { ${Array.from(obj.entries()) + .map(([key, value]) => `${convertToString(key)} => ${convertToString(value)}`) + .join(', ')} }` + } else if (typeof Set != 'undefined' && obj instanceof Set) { + obj = `Set->(${obj.size}) { ${Array.from(obj.values()) + .map((value) => convertToString(value)) + .join(', ')} }` + } else if (typeof Blob != 'undefined' && obj instanceof Blob) { + obj = `Blob->{ size: ${obj.size}, type: ${obj.type} }` + } else if (typeof File != 'undefined' && obj instanceof File) { + obj = `File->{ name: "${obj.name}", size: ${obj.size}, type: ${obj.type}, lastModified: ${new Date(obj.lastModified).toISOString()} }` + } else if (typeof URL != 'undefined' && obj instanceof URL) { + obj = `URL->{ href: "${obj.href}", protocol: "${obj.protocol}", host: "${obj.host}", pathname: "${obj.pathname}", search: "${obj.search}", hash: "${obj.hash}" }` + } else if (typeof FormData != 'undefined' && obj instanceof FormData) { + const entries = [] + obj.forEach((key, item) => { + entries.push(key) + }) + obj = `FormData->{ ${entries.join(', ')} }` + } else if (typeof Location != 'undefined' && obj instanceof Location) { + obj = `Location->{ href: "${obj.href}", protocol: "${obj.protocol}", host: "${obj.host}", pathname: "${obj.pathname}", search: "${obj.search}", hash: "${obj.hash}" }` + } else if (typeof Document != 'undefined' && obj instanceof Document) { + obj = `Document->{ title: "${obj.title}", URL: "${obj.URL}" }` + } else if (typeof Window !== 'undefined' && obj instanceof Window) { + obj = `Window->{ location: ${this.compressObject(obj.location)}, document: ${this.compressObject(obj.document)} }` + } else if (typeof Element != 'undefined' && obj instanceof Element) { + obj = `Element->{ tagName: "${obj.tagName}", id: "${obj.id}", class: "${obj.className}" }` + } else if (typeof HTMLCanvasElement != 'undefined' && obj instanceof HTMLCanvasElement) { + obj = `Canvas->{ width: ${obj.width}, height: ${obj.height} }` + } else if (typeof HTMLAudioElement != 'undefined' && obj instanceof HTMLAudioElement) { + obj = `Audio->{ src: "${obj.src}", duration: ${obj.duration} }` + } else if (typeof HTMLVideoElement != 'undefined' && obj instanceof HTMLVideoElement) { + obj = `Video->{ src: "${obj.src}", width: ${obj.videoWidth}, height: ${obj.videoHeight}, duration: ${obj.duration} }` + } else if (typeof Storage != 'undefined' && obj instanceof Storage) { + obj = `Storage->{ length: ${obj.length} }` + } else if ( + typeof Worker != 'undefined' && + typeof ServiceWorker != 'undefined' && + typeof SharedWorker != 'undefined' && + (obj instanceof Worker || obj instanceof ServiceWorker || obj instanceof SharedWorker) + ) { + obj = `Worker->${obj.constructor.name} { scriptURL: "${obj.scriptURL}" }` + } else if (typeof WebSocket != 'undefined' && obj instanceof WebSocket) { + obj = `WebSocket->{ url: "${obj.url}", readyState: ${obj.readyState} }` + } else if (typeof XMLHttpRequest != 'undefined' && obj instanceof XMLHttpRequest) { + obj = `XMLHttpRequest->{ readyState: ${obj.readyState}, status: ${obj.status} }` + } else if (typeof EventSource != 'undefined' && obj instanceof EventSource) { + obj = `EventSource->{ url: "${obj.url}", readyState: ${obj.readyState} }` + } else if (typeof MediaStream != 'undefined' && obj instanceof MediaStream) { + obj = `MediaStream->{ id: "${obj.id}", active: ${obj.active} }` + } else if (typeof RTCPeerConnection != 'undefined' && obj instanceof RTCPeerConnection) { + obj = `RTCPeerConnection->{ connectionState: "${obj.connectionState}" }` + } else if (typeof AudioContext != 'undefined' && obj instanceof AudioContext) { + obj = `AudioContext->{ state: "${obj.state}" }` + } else if (typeof Element != 'undefined' && obj instanceof Element) { + obj = `Element->{ tagName: "${obj.tagName}", id: "${obj.id}", class: "${obj.className}" }` + } else if (typeof HTMLCanvasElement != 'undefined' && obj instanceof HTMLCanvasElement) { + obj = `Canvas->{ width: ${obj.width}, height: ${obj.height} }` + } else if (typeof HTMLAudioElement != 'undefined' && obj instanceof HTMLAudioElement) { + obj = `Audio->{ src: "${obj.src}", duration: ${obj.duration} }` + } else if (typeof HTMLVideoElement != 'undefined' && obj instanceof HTMLVideoElement) { + obj = `Video->{ src: "${obj.src}", width: ${obj.videoWidth}, height: ${obj.videoHeight}, duration: ${obj.duration} }` + } else if (typeof Geolocation != 'undefined' && obj instanceof Geolocation) { + obj = `Geolocation->{ }` + } else if (typeof Performance != 'undefined' && obj instanceof Performance) { + obj = `Performance->{ now: ${obj.now()} }` + } else if (typeof Event != 'undefined' && obj instanceof Event) { + obj = `Event->{ type: "${obj.type}", target: "${obj.target}" }` + } + + if (typeof obj == 'string') { + return this.truncateStrBySize(obj, maxSize) + } + if (typeof obj != 'object') { + return obj + } + if (maxSize < 2) return {} + + let addEndOut = false + if (maxSize > 50) { + let objSize = this.calculateStringByteSize(obj) + if (objSize > maxSize) { + maxSize = maxSize - 50 + addEndOut = true + } + } + + let sizeCount = 2 + let str = this.safeJsonStringify(obj, (key, value) => { + let keySize = this.calculateStringByteSize(key) + if (typeof value == 'object') { + if (sizeCount + keySize + 6 > maxSize) { + return + } + sizeCount = sizeCount + keySize + 6 + return value + } + let valueSize = this.calculateStringByteSize(value) + let rowSize = keySize + valueSize + 6 + if (rowSize + sizeCount > maxSize) return + sizeCount = sizeCount + rowSize + return value + }) + let outPut = JSON.parse(str) + if (addEndOut) { + if (Array.isArray(outPut)) { + outPut.push('(已截断其余部分)') + } else if (typeof outPut == 'object') { + outPut['*注意'] = '(已截断其余部分)' + } + } + // console.log("compressObject use time: " + (new Date().getTime() - t)); + return outPut + } catch (error) { + console.log('compressObject error', error) + return '' + } + }, + /** + * 压缩数组不超过特定大小 + * @param {any[]} [arr=[]] 需要处理的数组 + * @param {string} [delType='start'] 数组超出后删除的开始位置 + * @param {number} [maxSize=1024 * 972] 数组最大字节数 + */ + compressArray(arr = [], delType = 'start', maxSize = 1024 * 972) { + let t = new Date().getTime() + try { + if (!arr || arr.length == 0 || !arr[0]) return [] + let i = 0 + while (true) { + i = i + 1 + if (i > 999999) return arr + if (!arr || arr.length == 0) { + return [] + } + if (this.calculateStringByteSize(arr) <= maxSize) { + // consoleLog("compressArray t=>" + (new Date().getTime() - t) + " i=>" + i) + return arr + } + if (delType == 'start') { + arr.splice(0, 1) + } else { + arr.splice(arr.length - 1, 1) + } + } + } catch (error) { + console.log('compressArray error', error) + return [] + } + }, + /** + * 计算对象或字符串占用的字节大小,传入对象将自动转json + */ + calculateStringByteSize(str) { + try { + let type = typeof str + if (type == 'bigint' || type == 'number' || type == 'boolean') { + return str.toString().length + } else if (type == 'function') { + str = str.toString().length + } else if (str === null || str === undefined) { + return 0 + } else { + try { + str = this.safeJsonStringify(str) + if (str && str.hasOwnProperty) { + return str.length + } else { + return 1024 * 20 + } + } catch (error) { + console.log('calculateStringByteSize error', error) + return 1024 * 20 + } + } + let size = 0 + for (let i = 0; i < str.length; i++) { + const charCode = str.charCodeAt(i) + if (charCode < 0x0080) { + size += 1 + } else if (charCode < 0x0800) { + size += 2 + } else if (charCode >= 0xd800 && charCode <= 0xdfff) { + size += 4 + i++ + } else { + size += 3 + } + } + return size + } catch (error) { + console.log('calculateStringByteSize error', error) + return 1024 * 1024 + } + }, + /** + * 安全的js对象转字符串 + */ + safeJsonStringify(obj, handleValue) { + if (!obj) return '{}' + try { + if (handleValue) { + return JSON.stringify(obj, (key, value) => { + return handleValue(key, value) + }) + } else { + return JSON.stringify(obj, (key, value) => { + return value + }) + } + } catch (error) { + // 尝试解析json失败,可能是变量循环引用的问题,继续尝试增加WeakSet解析 + } + + try { + let seen = new WeakSet() + let jsonStr = JSON.stringify(obj, (key, value) => { + if (typeof value == 'object') { + try { + if (value instanceof File) { + value = 'js:File' + } + if ( + value && + value.constructor && + value.constructor.name && + typeof value.constructor.name == 'string' + ) { + let className = value.constructor.name + if (className == 'VueComponent') { + return 'js:Object:VueComponent' + } + } + } catch (error) {} + } + if (typeof value == 'function') { + try { + value = value.toString() + } catch (error) { + value = 'js:function' + } + } + if (typeof value === 'object' && value !== null) { + // 处理循环引用问题 + if (seen.has(value)) { + return + } + seen.add(value) + } + if (handleValue && typeof handleValue == 'function') { + try { + return handleValue(key, value) + } catch (error) { + console.log('handleValue error', error) + } + return + } + return value + }) + seen = null + return jsonStr + } catch (error) { + return '{}' + } + }, + /** + * 根据限制的字节大小截取字符串 + */ + truncateStrBySize(str = '', size = 20 * 1024) { + try { + if (size < 1) return '' + if (this.calculateStringByteSize(str) <= size) return str + let endStr = '' + if (size > 30) { + endStr = '(已截断多余部分)' + size = size - 30 + } + let low = 0, + high = str.length, + mid + while (low < high) { + mid = Math.floor((low + high) / 2) + let currentSize = this.calculateStringByteSize(str.substring(0, mid)) + if (currentSize > size) { + // 如果大于限制值,减小高边界 + high = mid + } else { + // 如果小于或等于限制值,增加低边界 + low = mid + 1 + } + } + // 返回截断的字符串,注意low-1是因为low是最后一次检查超出大小时的位置 + return str.substring(0, low - 1) + endStr + } catch (error) { + console.log('truncateStrBySize error', error) + return '' + } + }, +} diff --git a/src/devTools/core/libs/logReport.js b/src/devTools/core/libs/logReport.js new file mode 100644 index 0000000..7fa0b6f --- /dev/null +++ b/src/devTools/core/libs/logReport.js @@ -0,0 +1,63 @@ +import devCache from './devCache' +import devOptions from './devOptions' +import jsonCompress from './jsonCompress' +/** + * ! 运行日志提交工具 + */ + +/** + * 日志上报 + */ +function logReport(msg) { + try { + if (!msg) return false + let options = devOptions.getOptions() + if (!options.status) { + console.error('日志上报失败!dev工具未启用 msg:' + msg) + return + } + if (!options.logs.status) { + console.error('日志上报失败!dev logs未启用 msg:' + msg) + return + } + + try { + let pages = getCurrentPages() + if (pages[pages.length - 1].route == options.devRoute) { + // 不记录调试工具报出的日志 + return false + } + } catch (error) {} + + if (typeof msg == 'object') { + try { + msg = JSON.stringify(msg) + } catch (error) { + msg = 'logReport:error' + } + } + + let log = { + t: new Date().getTime(), + m: '', + } + let logSize = jsonCompress.calculateStringByteSize(log) + + msg = String(msg) + msg = jsonCompress.compressObject(msg, options.logs.cache.rowSize - logSize) + log.m = msg + + let logs = devCache.get('logReport') + if (!logs) logs = [] + + logs.unshift(log) + + logs = jsonCompress.compressArray(logs, 'end', options.logs.cache.size) + + devCache.set('logReport', logs) + } catch (error) { + console.log('logReport error', error) + } +} + +export default logReport diff --git a/src/devTools/core/libs/pageLinkList.js b/src/devTools/core/libs/pageLinkList.js new file mode 100644 index 0000000..696ea97 --- /dev/null +++ b/src/devTools/core/libs/pageLinkList.js @@ -0,0 +1,124 @@ +import devCache from './devCache' + +export default { + pageRouteMap: [], + pageRouteKeyMap: {}, + /** + * 安装路径分析插件 + */ + install() { + let allRoutes = this.getAllRoutes() + + let pageRouteKeyMap = devCache.get('pageRouteKeyMap') + if (!pageRouteKeyMap || typeof pageRouteKeyMap != 'object') { + pageRouteKeyMap = {} + } + + let lastNo = 0 + Object.keys(pageRouteKeyMap).forEach((key) => { + let item = Number(pageRouteKeyMap[key]) + if (item > lastNo) { + lastNo = item + } + }) + + allRoutes.forEach((item) => { + if (!pageRouteKeyMap[item.path]) { + pageRouteKeyMap[item.path] = lastNo + 1 + lastNo = lastNo + 1 + } + }) + + devCache.set('pageRouteKeyMap', pageRouteKeyMap) + this.pageRouteKeyMap = pageRouteKeyMap + + let pageRouteMap = devCache.get('pageRouteMap') + if (!pageRouteMap || typeof pageRouteMap != 'object') { + pageRouteMap = {} + } + + Object.keys(pageRouteMap).forEach((key) => { + try { + let n = Number(pageRouteMap[key]) + if (!Number.isInteger(n) || n < 0) { + pageRouteMap[key] = 1 + } + } catch (error) {} + }) + this.pageRouteMap = pageRouteMap + + this.saveData() + }, + /** + * 获取APP注册的所有路由 + * @returns {{path: string}[]} 返回路由列表 + */ + getAllRoutes() { + let pages = [] + // #ifdef H5 || APP-PLUS + try { + __uniRoutes.map((item) => { + let path = item.alias ? item.alias : item.path + pages.push({ path }) + }) + } catch (error) { + pages = [] + } + // #endif + // #ifdef MP-WEIXIN + try { + let wxPages = __wxConfig.pages + wxPages.map((item) => { + pages.push({ + path: '/' + item, + }) + }) + } catch (error) { + pages = [] + } + // #endif + return pages + }, + /** + * 写入路由列表 + */ + pushPageRouteMap(list = []) { + if (!list || list.length == 0) { + list = getCurrentPages() + } + let key = '' + list.forEach((item) => { + let path = item.route.indexOf('/') == 0 ? item.route : '/' + item.route + let keyItem = this.pageRouteKeyMap[path] + if (!keyItem) { + keyItem = path + } + if (key == '') { + key = keyItem + '' + } else { + key = key + ',' + keyItem + } + }) + + if (this.pageRouteMap[key]) { + this.pageRouteMap[key] = this.pageRouteMap[key] + 1 + } else { + this.pageRouteMap[key] = 1 + } + }, + /** + * 保存路由缓存 + */ + saveData() { + let that = this + setTimeout( + () => { + devCache.set('pageRouteMap', that.pageRouteMap) + setTimeout(() => { + that.saveData() + }, 500) + }, + Math.round(Math.random() * 3 * 1000) + 2000, + ) + }, +} diff --git a/src/devTools/core/libs/pageStatistics.js b/src/devTools/core/libs/pageStatistics.js new file mode 100644 index 0000000..ffe59ce --- /dev/null +++ b/src/devTools/core/libs/pageStatistics.js @@ -0,0 +1,71 @@ +/** + * !页面统计:访问次数、停留时长 + */ + +import devCache from './devCache' +import devOptions from './devOptions' +import jsonCompress from './jsonCompress' +import { timeFormat } from './timeFormat' + +/** + * 页面注销时提交 + */ +function pageStatisticsReport(route, activeTime) { + try { + if (!route) return false + let options = devOptions.getOptions() + if (!options.pageStatistics.status) return //! 配置文件关闭页面统计 + let logs = devCache.get('pageCount') + if (!logs) logs = [] + let pageIndex = logs.findIndex((x) => x.route == route) + if (pageIndex == -1) { + logs.push({ + route, + activeTimeCount: activeTime, + }) + } else { + logs[pageIndex].activeTimeCount = activeTime + logs[pageIndex].activeTimeCount + } + logs = jsonCompress.compressArray(logs, 'end', options.pageStatistics.size) + devCache.set('pageCount', logs) + + let now = new Date().getTime() + let date = timeFormat(now, 'yyyy-mm-dd') + let dayOnline = devCache.get('dayOnline') + if (!dayOnline) dayOnline = [] + let i = dayOnline.findIndex((x) => x.date == date) + if (i == -1) { + dayOnline.unshift({ + date, + activeTimeCount: activeTime, + page: [ + { + r: route, + t: activeTime, + }, + ], + }) + } else { + dayOnline[i].activeTimeCount = dayOnline[i].activeTimeCount + activeTime + let pi = dayOnline[i].page.findIndex((x) => x.r == route) + if (pi == -1) { + dayOnline[i].page.push({ + r: route, + t: activeTime, + }) + } else { + dayOnline[i].page[pi].t = dayOnline[i].page[pi].t + activeTime + } + } + if (dayOnline.length > options.pageStatistics.dayOnlineRowMax) { + dayOnline = dayOnline.splice(0, options.pageStatistics.dayOnlineRowMax) + } + + dayOnline = jsonCompress.compressArray(dayOnline, 'end', options.pageStatistics.size) + devCache.set('dayOnline', dayOnline) + } catch (error) { + console.log('pageStatistics error', error) + } +} + +export default pageStatisticsReport diff --git a/src/devTools/core/libs/timeFormat.js b/src/devTools/core/libs/timeFormat.js new file mode 100644 index 0000000..261db5c --- /dev/null +++ b/src/devTools/core/libs/timeFormat.js @@ -0,0 +1,94 @@ +// padStart 的 polyfill,因为某些机型或情况,还无法支持es7的padStart,比如电脑版的微信小程序 +// 所以这里做一个兼容polyfill的兼容处理 +try { + if (!String.prototype.padStart) { + // 为了方便表示这里 fillString 用了ES6 的默认参数,不影响理解 + String.prototype.padStart = function (maxLength, fillString = ' ') { + if (Object.prototype.toString.call(fillString) !== '[object String]') + throw new TypeError('fillString must be String') + let str = this + // 返回 String(str) 这里是为了使返回的值是字符串字面量,在控制台中更符合直觉 + if (str.length >= maxLength) return String(str) + + let fillLength = maxLength - str.length, + times = Math.ceil(fillLength / fillString.length) + while ((times >>= 1)) { + fillString += fillString + if (times === 1) { + fillString += fillString + } + } + return fillString.slice(0, fillLength) + str + } + } +} catch (error) { + console.log('timeFormat fillString error', error) +} + +// 其他更多是格式化有如下: +// yyyy:mm:dd|yyyy:mm|yyyy年mm月dd日|yyyy年mm月dd日 hh时MM分等,可自定义组合 +export function timeFormat(dateTime = null, fmt = 'yyyy-mm-dd hh:MM:ss') { + try { + // 如果为null,则格式化当前时间 + if (!dateTime) dateTime = Number(new Date()) + // 如果dateTime长度为10或者13,则为秒和毫秒的时间戳,如果超过13位,则为其他的时间格式 + if (dateTime.toString().length == 10) dateTime *= 1000 + let date = new Date(dateTime) + let ret + let opt = { + 'y+': date.getFullYear().toString(), // 年 + 'm+': (date.getMonth() + 1).toString(), // 月 + 'd+': date.getDate().toString(), // 日 + 'h+': date.getHours().toString(), // 时 + 'M+': date.getMinutes().toString(), // 分 + 's+': date.getSeconds().toString(), // 秒 + // 有其他格式化字符需求可以继续添加,必须转化成字符串 + } + for (let k in opt) { + ret = new RegExp('(' + k + ')').exec(fmt) + if (ret) { + fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, '0')) + } + } + return fmt + } catch (error) { + console.log('timeFormat error', error) + return 'unknown error' + } +} + +export function timeFromNow(timestamp) { + try { + const now = new Date().getTime() + let diff = timestamp - now + + // 确定是过去还是未来 + const suffix = diff > 0 ? '后' : '前' + diff = Math.abs(diff) + + // 计算时间差异 + const seconds = Math.floor(diff / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + const months = Math.floor(days / 30) + const years = Math.floor(days / 365) + + // 根据时间差异返回相应的字符串 + if (seconds < 60) { + return `${seconds}秒${suffix}` + } else if (minutes < 60) { + return `${minutes}分钟${suffix}` + } else if (hours < 24) { + return `${hours}小时${suffix}` + } else if (days < 30) { + return `${days}天${suffix}` + } else if (months < 12) { + return `${months}个月${suffix}` + } else { + return `${years}年${suffix}` + } + } catch (error) { + console.log('timeFromNow error', error) + } +} diff --git a/src/devTools/core/proxy/console.js b/src/devTools/core/proxy/console.js new file mode 100644 index 0000000..cae443f --- /dev/null +++ b/src/devTools/core/proxy/console.js @@ -0,0 +1,413 @@ +import devCache from '../libs/devCache' +import devOptions from '../libs/devOptions' +import jsonCompress from '../libs/jsonCompress' + +export default { + logList: [], + options: null, + /** + * 挂载打印拦截器 + */ + install() { + let that = this + + this.options = devOptions.getOptions() + if (!this.options.console.status) return + + this.logList = devCache.get('console') + if (!this.logList) this.logList = [] + this.syncReqData() //同步缓存 + + if (uni.__log__) { + // ! VUE3在app端时有这个特殊的方法 + that.mountUniConsole() + } else { + that.mountJsConsole() + } + + //! 删除指定记录 + uni.$on('devTools_delConsoleItem', (item) => { + let i = that.logList.findIndex((x) => { + let t = JSON.stringify(x.list) + return ( + t == JSON.stringify(item.list) && + x.time == item.time && + x.page == item.page && + x.type == item.type + ) + }) + if (i != -1) { + that.logList.splice(i, 1) + } + that.saveData() + }) + + //! 清空console日志 + uni.$on('devTools_delConsoleAll', () => { + that.logList = [] + that.saveData() + }) + }, + /** + * 同步请求信息到缓存数据中 + */ + syncReqData() { + let that = this + setTimeout(() => { + try { + that.saveData() + } catch (error) { + console.log('console.syncReqData error', error) + } + that.syncReqData() + }, 3000) + }, + /** + * 同步数据到缓存 + */ + saveData() { + let that = this + that.logList = jsonCompress.compressArray(that.logList, 'end', that.options.console.cache.size) + devCache.set('console', that.logList) + }, + /** + * 挂载监听js自带的console函数 + */ + mountJsConsole() { + let that = this + try { + let l = console.log + try { + globalThis.consoleLog = function () { + console.log(...arguments) + } + } catch (error) {} + try { + window.consoleLog = function () { + console.log(...arguments) + } + } catch (error) {} + console.log = function () { + replaceConsole('log', arguments) + } + let e = console.error + function _error() { + try { + let args = [...arguments] + if ( + args[0] && + typeof args[0] == 'string' && + (args[0] == '__ignoreReport__' || //! 忽略错误日志上报 + args[0].indexOf('__ignoreReport__') == 0) + ) { + let _args = [] + if (args.length > 1) { + for (let i = 0; i < args.length; i++) { + if (i != 0) { + _args.push(args[i]) + } + } + } else { + _args[0] = args[0] + _args[0] = _args[0].replace('__ignoreReport__', '') + } + if (that.options.console.isOutput) { + e(..._args) + } + return + } + replaceConsole('error', args) + } catch (error) { + e('监听console.error出错', error) + } + } + console.error = _error + let w = console.warn + console.warn = function () { + replaceConsole('warn', arguments) + } + let i = console.info + console.info = function () { + replaceConsole('info', arguments) + } + + /** + * 替换系统打印函数 + */ + function replaceConsole(type, args) { + try { + let data = [] + if (args && args.length > 0) { + let argList = args + + // #ifdef APP-PLUS + if (args.length == 1) { + argList = args[0].split('---COMMA---') + + let endItem = argList[argList.length - 1] + if ( + endItem && + typeof endItem == 'string' && + endItem.indexOf(' at ') > -1 && + endItem.indexOf(':') > -1 + ) { + // 可能包含路径信息 + let endList = endItem.split(' at ') + if (endList.length == 2) { + argList.pop() + argList.push(endList[0]) + argList.push('at ' + endList[1]) + } + } + + argList = argList.map((item, index) => { + try { + if (typeof item == 'string') { + if (item.indexOf('---BEGIN') > -1) { + let isJson = item.indexOf('---BEGIN:JSON---') > -1 + item = item.replace(/---BEGIN:.*?---/g, '') + item = item.replace(/---END:.*?---/g, '') + if (isJson) { + item = JSON.parse(item) + } + } else if (item == '---NULL---') { + item = null + } else if (item == '---UNDEFINED---') { + item = undefined + } + } + } catch (error) { + console.log('replaceConsole 尝试解析对象出错:', error) + } + return item + }) + } + // #endif + + let oneSize = that.options.console.cache.rowSize / argList.length + for (let i = 0; i < argList.length; i++) { + let row = jsonCompress.compressObject(argList[i], oneSize) + data.push(row) + } + } else { + data = [] + } + + let page = '未知' + try { + let pages = getCurrentPages() + let item = pages[pages.length - 1] + if (item && item.route) { + page = item.route + } + } catch (error) {} + that.logList.unshift({ + list: data, + time: new Date().getTime(), + page, + type, + }) + if (that.options.console.isOutput) { + if (type == 'error') { + e(...args) + } else if (type == 'warn') { + w(...args) + } else if (type == 'info') { + i(...args) + } else { + l(...args) + } + } + } catch (error) { + if (that.options.console.isOutput) { + e('监听console出错', error) + } + } + } + } catch (error) { + console.log('console.install error', error) + } + }, + /** + * 挂载监听uni自带的打印函数 + */ + mountUniConsole() { + let that = this + try { + let uniSysConsole = uni.__log__ + try { + globalThis.consoleLog = function () { + uni.__log__('log', '未知来源', ...arguments) + } + } catch (error) {} + try { + window.consoleLog = function () { + uni.__log__('log', '未知来源', ...arguments) + } + } catch (error) {} + + uni.__log__ = function (type, line, ...args) { + try { + // 处理特殊情况 "__ignoreReport__" 忽略错误日志上报 + if (type == 'error') { + if ( + args[0] && + typeof args[0] == 'string' && + (args[0] == '__ignoreReport__' || //! 忽略错误日志上报 + args[0].indexOf('__ignoreReport__') == 0) + ) { + let _args = [] + if (args.length > 1) { + for (let i = 0; i < args.length; i++) { + if (i != 0) { + _args.push(args[i]) + } + } + } else { + _args[0] = args[0] + _args[0] = _args[0].replace('__ignoreReport__', '') + } + if (that.options.console.isOutput) { + uniSysConsole(type, line, ..._args) + } + return + } + } + + let data = [] + if (args && args.length > 0) { + let argList = args + let oneSize = that.options.console.cache.rowSize / argList.length + for (let i = 0; i < argList.length; i++) { + let row = jsonCompress.compressObject(argList[i], oneSize) + data.push(row) + } + } else { + data = [] + } + + let page = '未知' + try { + let pages = getCurrentPages() + let item = pages[pages.length - 1] + if (item && item.route) { + page = item.route + } + } catch (error) {} + + data.push(line) + + that.logList.unshift({ + list: data, + time: new Date().getTime(), + page, + type, + }) + if (that.options.console.isOutput) { + uniSysConsole(type, line, ...data) + } + } catch (error) { + if (that.options.console.isOutput) { + uniSysConsole('error', '监听console出错', error) + } + } + } + + /** + * 替换系统打印函数 + */ + function replaceConsole(type, args) { + try { + let data = [] + if (args && args.length > 0) { + let argList = args + + // #ifdef APP-PLUS + if (args.length == 1) { + argList = args[0].split('---COMMA---') + + let endItem = argList[argList.length - 1] + if ( + endItem && + typeof endItem == 'string' && + endItem.indexOf(' at ') > -1 && + endItem.indexOf(':') > -1 + ) { + // 可能包含路径信息 + let endList = endItem.split(' at ') + if (endList.length == 2) { + argList.pop() + argList.push(endList[0]) + argList.push('at ' + endList[1]) + } + } + + argList = argList.map((item, index) => { + try { + if (typeof item == 'string') { + if (item.indexOf('---BEGIN') > -1) { + let isJson = item.indexOf('---BEGIN:JSON---') > -1 + item = item.replace(/---BEGIN:.*?---/g, '') + item = item.replace(/---END:.*?---/g, '') + if (isJson) { + item = JSON.parse(item) + } + } else if (item == '---NULL---') { + item = null + } else if (item == '---UNDEFINED---') { + item = undefined + } + } + } catch (error) { + console.log('replaceConsole 尝试解析对象出错:', error) + } + return item + }) + } + // #endif + + let oneSize = that.options.console.cache.rowSize / argList.length + for (let i = 0; i < argList.length; i++) { + let row = jsonCompress.compressObject(argList[i], oneSize) + data.push(row) + } + } else { + data = [] + } + + let page = '未知' + try { + let pages = getCurrentPages() + let item = pages[pages.length - 1] + if (item && item.route) { + page = item.route + } + } catch (error) {} + that.logList.unshift({ + list: data, + time: new Date().getTime(), + page, + type, + }) + if (that.options.console.isOutput) { + if (type == 'error') { + e(...args) + } else if (type == 'warn') { + w(...args) + } else if (type == 'info') { + i(...args) + } else { + l(...args) + } + } + } catch (error) { + if (that.options.console.isOutput) { + e('监听console出错', error) + } + } + } + } catch (error) { + console.log('console.install error', error) + } + }, +} diff --git a/src/devTools/core/proxy/index.js b/src/devTools/core/proxy/index.js new file mode 100644 index 0000000..bef54e2 --- /dev/null +++ b/src/devTools/core/proxy/index.js @@ -0,0 +1,33 @@ +import devCache from '../libs/devCache' +import console from './console' +import request from './request' +import storage from './storage' +import uniBus from './uniBus' +import uniListen from './uniListen' + +/** + * dev调试工具初始化 + */ +export default function devToolsProxyInstall(options) { + try { + if (options.network && options.network.status) { + request.install() + } + if (options.console && options.console.status) { + console.install() + } + if (options.logs && options.logs.status) { + uniListen.install() + } + + storage.install() + + if (options.uniBus && options.uniBus.status) { + uniBus.install() + } + + devCache.syncLocalCache() + } catch (error) { + console.log('devToolsProxyInstall error', error) + } +} diff --git a/src/devTools/core/proxy/request.js b/src/devTools/core/proxy/request.js new file mode 100644 index 0000000..5dde9f3 --- /dev/null +++ b/src/devTools/core/proxy/request.js @@ -0,0 +1,246 @@ +import devCache from '../libs/devCache' +import devOptions from '../libs/devOptions' +import jsonCompress from '../libs/jsonCompress' + +export default { + /** + * 请求日志示例 + */ + ajaxLogData: { + id: 0, //请求id + type: 0, // 0发起请求中 1请求成功 2请求失败 + sendTime: 0, //发送请求的时间 + responseTime: 0, //响应时间 + useTime: 0, //请求总耗时 + + url: '', //请求地址 + header: '', //请求头 + method: 'get', //请求方式 + data: '', //请求参数 + + responseBody: '', //响应主体 + responseHeader: '', //响应头 + responseStatus: '', //响应编码 + responseMsg: '', //响应报错信息 + }, + options: null, + /** + * 请求的数据列表 + */ + ajaxData: [], + /** + * 挂载请求拦截器 + */ + install() { + let that = this + + try { + this.options = devOptions.getOptions() + if (!this.options.network.status) return + + this.ajaxData = devCache.get('request') + if (!this.ajaxData) this.ajaxData = [] + this.syncReqData() //同步缓存 + + uni.addInterceptor('request', { + /** + * 入参 + */ + invoke(args) { + try { + args._id_ = + new Date().getTime() + '_' + Number(Math.random().toString().replace('0.', '')) + + let copyData = JSON.parse(JSON.stringify(that.ajaxLogData)) + copyData.id = args._id_ + copyData.sendTime = new Date().getTime() + copyData.url = that.dataCopy(args.url) + copyData.header = that.dataCopy(args.header) + if (!args.method) { + copyData.method = 'get' + } else { + copyData.method = that.dataCopy(args.method) + } + + let cSize = jsonCompress.calculateStringByteSize(copyData) + if (cSize > that.options.network.cache.rowSize) { + copyData = jsonCompress.compressObject(copyData, that.options.network.cache.rowSize) + } else { + let data = jsonCompress.compressObject( + args.data, + that.options.network.cache.rowSize - cSize, + ) + try { + data = JSON.parse(data) + } catch (error) {} + copyData.data = data + } + that.ajaxData.unshift(copyData) + } catch (error) { + console.error('request拦截器invoke出错', error) + } + }, + success(response, request) { + return new Promise(async (yes, err) => { + //! 延迟请求返回,模拟弱网环境 + let speedLimit = uni.getStorageSync('devtools_uniResLimitType') + if (speedLimit) { + let delayDuration = { + '2g': [30, 60], + '3g-': [10, 30], + '3g': [3, 10], + '4g': [0.5, 3], + } + let sleepParam = delayDuration[speedLimit] + if (sleepParam) { + let sleepTime = rNum(sleepParam[0], sleepParam[1]) + await sleep(sleepTime * 1000) + response.errMsg = response.errMsg + ` | [devtools模拟弱网延迟:${sleepTime}s]` + } + } + + // ! 随机响应失败概率 + let resTimeout = uni.getStorageSync('devtools_uniResTimeout') + let isFail = false + if (resTimeout && resTimeout > 1) { + let targetPro = Number(resTimeout) + let randPro = rNum(0, 100) + if (randPro < targetPro) { + // 命中失败 + response.statusCode = rNum(400, 600) //生成随机400 ~ 600之间的状态码 + response.errMsg = + response.errMsg + + ` | [devtools随机超时报错,当前命中的概率阶层为:${targetPro}%,生成的随机数为:${randPro}]` + response.data = '[devTools]模拟请求失败结果!' + isFail = true + } + } + + // ! 记录响应内容 + try { + let item = that.ajaxData.find((x) => x.id == request._id_) + if (!item) return + + item.responseBodySize = jsonCompress.calculateStringByteSize(response.data) + item.responseMsg = response.errMsg + item.responseStatus = response.statusCode + item.responseHeader = response.header + item.type = 1 + item.responseTime = new Date().getTime() + item.useTime = ((item.responseTime - item.sendTime) / 1000).toFixed(3) + + let size = jsonCompress.calculateStringByteSize(item) + if (size > that.options.network.cache.rowSize) { + item.responseBody = '[内容太长已截断多余部分]' + let data = jsonCompress.compressObject(item, that.options.network.cache.rowSize) + that.ajaxData[that.ajaxData.findIndex((x) => x.id == request._id_)] = data + } else { + let json = response.data + try { + json = JSON.parse(JSON.stringify(json)) + } catch (error) {} + item.responseBody = jsonCompress.compressObject( + json, + that.options.network.cache.rowSize - size, + ) + } + } catch (error) { + console.error('request拦截器success出错', error) + } + + if (isFail) { + err(response.data) + } else { + yes(response) + } + }) + }, + fail(err, request) { + try { + let item = that.ajaxData.find((x) => x.id == request._id_) + if (!item) return + + item.type = 2 + item.responseTime = new Date().getTime() + item.useTime = ((item.responseTime - item.sendTime) / 1000).toFixed(3) + + item.responseMsg = err.errMsg + } catch (error) { + console.error('request拦截器fail出错', error) + } + }, + complete(res) {}, + }) + + // ! 删除指定请求记录 + uni.$on('devTools_delNetworkItemById', (id) => { + let i = this.ajaxData.findIndex((x) => x.id == id) + if (i != -1) { + this.ajaxData.splice(i, 1) + } + this.saveData() + }) + + // ! 清空请求记录 + uni.$on('devTools_delNetworkAll', () => { + this.ajaxData = [] + this.saveData() + }) + } catch (error) { + console.log('request.install error', error) + } + }, + /** + * 同步请求信息到缓存数据中 + */ + syncReqData() { + let that = this + setTimeout(() => { + try { + that.saveData() + } catch (error) { + console.log('request.syncReqData', error) + } + that.syncReqData() + }, 4000) + }, + /** + * 保存数据到缓存中 + */ + saveData() { + let that = this + that.ajaxData = jsonCompress.compressArray(that.ajaxData, that.options.network.cache.size) + devCache.set('request', that.ajaxData) + }, + /** + * 复制对象 + */ + dataCopy(data) { + try { + if (typeof data == 'object') { + return JSON.parse(JSON.stringify([data]))[0] + } else { + return data + } + } catch (error) { + console.log('request.dataCopy', error) + return '' + } + }, +} + +/** + * 随机生成n~m的数,支持两位小数 + */ +function rNum(n, m) { + return Number((Math.random() * (m - n) + n).toFixed(2)) +} + +/** + * 休眠指定时长 + */ +function sleep(t) { + return new Promise((y) => { + setTimeout(y, t) + }) +} diff --git a/src/devTools/core/proxy/storage.js b/src/devTools/core/proxy/storage.js new file mode 100644 index 0000000..99934cc --- /dev/null +++ b/src/devTools/core/proxy/storage.js @@ -0,0 +1,96 @@ +import devCache from '../libs/devCache' + +export default { + /** + * 挂载缓存监听 + */ + install() { + try { + // #ifdef MP + let that = this + + let _setStorage = uni.setStorage + uni.setStorage = setStorage + function setStorage() { + try { + if (arguments[0] && arguments[0].key && arguments[0].key.indexOf('devTools_') != 0) { + that.addCacheKey(arguments[0].key) + } + } catch (error) {} + return _setStorage(...arguments) + } + + let _setStorageSync = uni.setStorageSync + uni.setStorageSync = setStorageSync + function setStorageSync() { + try { + if (arguments[0] && arguments[0].indexOf('devTools_') != 0) { + that.addCacheKey(arguments[0]) + } + } catch (error) {} + return _setStorageSync(...arguments) + } + + let _removeStorage = uni.removeStorage + uni.removeStorage = removeStorage + function removeStorage() { + try { + if (arguments[0] && arguments[0].key && arguments[0].key.indexOf('devTools_') != 0) { + that.delCacheKey(arguments[0].key) + } + } catch (error) {} + return _removeStorage(...arguments) + } + + let _removeStorageSync = uni.removeStorageSync + uni.removeStorageSync = removeStorageSync + function removeStorageSync() { + try { + if (arguments[0] && arguments[0].indexOf('devTools_') != 0) { + that.delCacheKey(arguments[0]) + } + } catch (error) {} + return _removeStorageSync(...arguments) + } + + // #endif + } catch (error) { + console.log('devTools storage.install error', error) + } + }, + /** + * 添加缓存key + */ + addCacheKey(key) { + try { + if (key && typeof key == 'string') { + let storageList = devCache.get('storage') + if (!storageList) storageList = [] + if (storageList.indexOf(key) == -1) { + storageList.push(key) + devCache.set('storage', storageList) + } + } + } catch (error) { + console.log('devTools storage.addCacheKey error', error) + } + }, + /** + * 删除指定缓存key + */ + delCacheKey(key) { + try { + if (key && typeof key == 'string') { + let storageList = devCache.get('storage') + if (!storageList) storageList = [] + let index = storageList.indexOf(key) + if (index > -1) { + storageList.splice(index, 1) + devCache.set('storage', storageList) + } + } + } catch (error) { + console.log('devTools storage.delCacheKey error', error) + } + }, +} diff --git a/src/devTools/core/proxy/uniBus.js b/src/devTools/core/proxy/uniBus.js new file mode 100644 index 0000000..12b5f45 --- /dev/null +++ b/src/devTools/core/proxy/uniBus.js @@ -0,0 +1,160 @@ +import devCache from '../libs/devCache' +import devOptions from '../libs/devOptions' +import jsonCompress from '../libs/jsonCompress' + +export default { + logList: [], + busCount: [], + + options: null, + /** + * 挂载打印拦截器 + */ + install() { + try { + let that = this + + this.options = devOptions.getOptions() + if (!this.options.uniBus.status) return + + this.logList = devCache.get('uniBus') + if (!this.logList) this.logList = [] + + this.busCount = devCache.get('busCount') + if (!this.busCount) this.busCount = [] + + this.syncReqData() //同步缓存 + + let now = () => new Date().getTime() + + const _on = uni.$on + uni.$on = function () { + try { + let n = arguments[0] + if (n && typeof n == 'string' && n.length < 200 && n.indexOf('devTools_') == -1) { + that.logList.unshift({ + t: now(), + e: jsonCompress.compressObject(`on>${n}`, that.options.uniBus.cache.rowMax), + }) + addCount(n, 'on') + } + } catch (error) { + console.error('uni.$on出错', error) + } + _on(...arguments) + } + + const _once = uni.$once + uni.$once = function () { + try { + let n = arguments[0] + if (n && typeof n == 'string' && n.length < 200 && n.indexOf('devTools_') == -1) { + that.logList.unshift({ + t: now(), + e: jsonCompress.compressObject(`once>${n}`, that.options.uniBus.cache.rowMax), + }) + addCount(n, 'once') + } + } catch (error) { + console.error('uni.$once出错', error) + } + _once(...arguments) + } + + const _emit = uni.$emit + uni.$emit = function () { + try { + let n = arguments[0] + let p = arguments[1] + if (n && typeof n == 'string' && n.length < 200 && n.indexOf('devTools_') == -1) { + that.logList.unshift({ + t: now(), + e: jsonCompress.compressObject( + `emit>${n}` + (p ? '>' + JSON.stringify(p) : ''), + that.options.uniBus.cache.rowMax, + ), + }) + addCount(n, 'emit') + } + } catch (error) { + console.error('uni.$emit出错', error) + } + _emit(...arguments) + } + + const _off = uni.$off + uni.$off = function () { + try { + let n = arguments[0] + if (n && typeof n == 'string' && n.length < 200 && n.indexOf('devTools_') == -1) { + that.logList.unshift({ + t: now(), + e: jsonCompress.compressObject( + `off>${n}` + arguments[0], + that.options.uniBus.cache.rowMax, + ), + }) + addCount(n, 'off') + } + } catch (error) { + console.error('uni.$off出错', error) + } + _off(...arguments) + } + + /** + * 统计总次数 + */ + function addCount(name, type = 'on') { + let i = that.busCount.findIndex((x) => x.e == name) + if (i == -1) { + let item = { + e: name, + on: 0, + off: 0, + emit: 0, + once: 0, + } + item[type] = item[type] + 1 + that.busCount.push(item) + } else { + that.busCount[i][type] = that.busCount[i][type] + 1 + } + } + + // ! 清空全部记录 + uni.$on('devTools_delUniBusAll', () => { + that.logList = [] + that.busCount = [] + }) + } catch (error) { + console.log('devTools uniBus.install error', error) + } + }, + /** + * 同步请求信息到缓存数据中 + */ + syncReqData() { + let that = this + setTimeout(() => { + try { + that.logList = jsonCompress.compressArray( + that.logList, + 'end', + that.options.uniBus.cache.rowMax, + ) + devCache.set('uniBus', that.logList) + + that.busCount = jsonCompress.compressArray( + that.busCount, + 'end', + that.options.uniBus.cache.countMaxSize, + ) + devCache.set('busCount', that.busCount) + } catch (error) { + console.log('devTools uniBus.syncReqData error', error) + } + that.syncReqData() + }, 5000) + }, +} diff --git a/src/devTools/core/proxy/uniListen.js b/src/devTools/core/proxy/uniListen.js new file mode 100644 index 0000000..03348e0 --- /dev/null +++ b/src/devTools/core/proxy/uniListen.js @@ -0,0 +1,195 @@ +import logReport from '../libs/logReport' + +export default { + /** + * 挂载uni大部分api监听器 + */ + install() { + try { + this.addDefUniApiListen() + this.onNetworkStatusChange() + this.scanCodeListen() + this.onLocaleChange() + } catch (error) { + console.log('uniListen error', error) + } + }, + /** + * 批量挂载api调用日志 + */ + addDefUniApiListen() { + /** + * 需要挂载监听的api列表 + */ + let diyListenApi = { + downloadFile(args) { + logReport('downloadFile>' + (args && args.url ? args.url : '')) + }, + connectSocket(args) { + logReport('connectSocket>' + args.url) + }, + makePhoneCall(args) { + logReport('makePhoneCall>' + args.phoneNumber) + }, + addPhoneContact(args) { + logReport('addPhoneContact>' + args.name) + }, + showToast(args) { + logReport('showToast>' + args.title) + }, + showModal(args) { + logReport('showModal>' + args.title + '>' + args.content) + }, + setLocale(args) { + logReport('setLocale>' + args) + }, + saveFile(args) { + logReport('saveFile>' + args.tempFilePath) + }, + login(args) { + logReport('login>' + JSON.stringify(args)) + }, + share(args) { + logReport('share>' + JSON.stringify(args)) + }, + shareWithSystem(args) { + logReport('shareWithSystem>' + JSON.stringify(args)) + }, + requestPayment(args) { + logReport('requestPayment>' + JSON.stringify(args)) + }, + authorize(args) { + logReport('requestPayment>' + args.scope) + }, + navigateToMiniProgram(args) { + logReport('navigateToMiniProgram>' + args.appId + '>' + args.path) + }, + openDocument(args) { + logReport('openDocument>' + args.filePath) + }, + } + /** + * 需要监听打印日志的api名称列表 + */ + let waitListenApiNames = [ + 'uploadFile', + 'closeSocket', + 'getLocation', + 'chooseLocation', + 'openLocation', + 'chooseImage', + 'previewImage', + 'saveImageToPhotosAlbum', + 'chooseFile', + 'chooseVideo', + 'chooseMedia', + 'saveVideoToPhotosAlbum', + 'openVideoEditor', + 'openAppAuthorizeSetting', + 'startAccelerometer', + 'startCompass', + 'startGyroscope', + 'setScreenBrightness', + 'vibrate', + 'vibrateLong', + 'vibrateShort', + 'openBluetoothAdapter', + 'startBeaconDiscovery', + 'startSoterAuthentication', + 'hideKeyboard', + 'showActionSheet', + 'startPullDownRefresh', + 'showShareMenu', + 'startFacialRecognitionVerify', + 'openSetting', + 'chooseAddress', + 'chooseInvoiceTitle', + 'openEmbeddedMiniProgram', + ] + + for (const key in diyListenApi) { + uni.addInterceptor(key, { + invoke(_args) { + try { + diyListenApi[key](_args) + } catch (error) { + console.error('addInterceptor=>' + key, error) + } + }, + }) + } + + waitListenApiNames.map((key) => { + uni.addInterceptor(key, { + invoke(args) { + try { + logReport(key) + } catch (error) { + console.error('addInterceptor>' + key, error) + } + }, + }) + }) + }, + /** + * 添加网络状态监听 + */ + onNetworkStatusChange() { + uni.onNetworkStatusChange((res) => { + try { + logReport( + 'onNetworkStatusChange>isConnected:' + + (res.isConnected ? 'true' : 'false') + + '>networkType:' + + res.networkType, + ) + } catch (error) { + console.log('onNetworkStatusChange', error) + } + }) + }, + /** + * 添加系统主题切换监听 + */ + onThemeChange() { + uni.onThemeChange((res) => { + try { + logReport('onThemeChange>' + res.theme) + } catch (error) { + console.log('onThemeChange', error) + } + }) + }, + /** + * 监听扫码结果 + */ + scanCodeListen() { + uni.addInterceptor('scanCode', { + success(res) { + try { + logReport( + 'scanCodeSuccess>' + + JSON.stringify({ + scanType: res.scanType, + result: res.result, + }), + ) + } catch (error) { + console.log('scanCode', error) + } + }, + }) + }, + /** + * 监听系统语言切换 + */ + onLocaleChange() { + uni.onLocaleChange((locale) => { + try { + logReport('onLocaleChange>' + locale) + } catch (error) { + console.log('onLocaleChange', error) + } + }) + }, +} diff --git a/src/devTools/core/proxy/vueMixin.js b/src/devTools/core/proxy/vueMixin.js new file mode 100644 index 0000000..3c69718 --- /dev/null +++ b/src/devTools/core/proxy/vueMixin.js @@ -0,0 +1,160 @@ +import devOptions from '../libs/devOptions' +import logReport from '../libs/logReport' +import pageLinkList from '../libs/pageLinkList' +import pageStatisticsReport from '../libs/pageStatistics' + +/** + * ! Vue页面混入,监听生命周期 + */ +export default { + data() { + return { + /** + * 挂载dev页面对象 + */ + devTools_pageData: { + route: '', // 页面路径 + isOnShow: false, // 是否处于展示状态 + activeTime: 0, //活跃时间 + }, + } + }, + /** + * *页面载入事件 + */ + onLoad(pageInitParams) { + let that = this + + // ! 注入 Eruda + let isInjectEruda = uni.getStorageSync('devTools_isInjectEruda') == 'yes' + if (isInjectEruda) { + let ErudaCode = ` + if(!window.isInjectEruda){ + window.isInjectEruda = true; + var script = document.createElement('script'); + script.src="https://cdn.jsdelivr.net/npm/eruda"; + document.body.append(script); + script.onload = function () { + eruda.init(); + } + } + ` + let fun = 'e' + ['v'][0] + 'a' + ['l'][0] + try { + // #ifdef H5 + window[fun](ErudaCode) + // #endif + // #ifdef APP-PLUS + let endPageWebView = getCurrentPages().pop() + if (endPageWebView) { + let nowPageWebview = endPageWebView.$getAppWebview() + if (nowPageWebview && !nowPageWebview.nvue) { + nowPageWebview[fun + 'JS'](ErudaCode) + } + } + // #endif + } catch (error) { + console.log('devTools mixin onLoad injectEruda error ', error) + } + } + + // ! 注入 vConsole + let isInjectVConsole = uni.getStorageSync('devTools_isInjectVConsole') == 'yes' + if (isInjectVConsole) { + let vConsoleCode = ` + if(!window.isInjectVConsole){ + window.isInjectVConsole = true; + var script = document.createElement('script'); + script.src="https://cdn.jsdelivr.net/npm/vconsole@latest/dist/vconsole.min.js"; + document.body.append(script); + script.onload = function () { + let vConsoleObj = new window.VConsole(); + } + } + ` + let fun = 'e' + ['v'][0] + 'a' + ['l'][0] + try { + // #ifdef H5 + window[fun](vConsoleCode) + // #endif + // #ifdef APP-PLUS + let endPageWebView = getCurrentPages().pop() + if (endPageWebView) { + let nowPageWebview = endPageWebView.$getAppWebview() + if (nowPageWebview && !nowPageWebview.nvue) { + nowPageWebview[fun + 'JS'](vConsoleCode) + } + } + // #endif + } catch (error) { + console.log('devTools mixin onLoad injectVConsole error ', error) + } + } + + try { + let pages = getCurrentPages() + let pageItem = pages && pages.length > 0 ? pages[pages.length - 1] : null + if (pageItem) { + let devSetting = devOptions.getOptions() + if (pageItem.route == devSetting.devRoute) { + that.devTools_pageData = false + } else { + that.devTools_pageData.route = pageItem.route + logReport( + `onLoad>${pageItem.route}>` + (pageInitParams ? JSON.stringify(pageInitParams) : ''), + ) + setInterval(() => { + if (that.devTools_pageData && that.devTools_pageData.isOnShow) { + that.devTools_pageData.activeTime = that.devTools_pageData.activeTime + 1 + } + }, 1000) + } + } + + pageLinkList.pushPageRouteMap(pages) + } catch (error) { + console.log('devTools mixin onLoad error ', error) + } + }, + /** + * *页面展示事件 + */ + onShow() { + try { + let that = this + if (that.devTools_pageData) { + that.devTools_pageData.isOnShow = true + that.devTools_pageData.activeTime = 0 + } + } catch (error) { + console.log('devTools mixin onShow error ', error) + } + }, + /** + * *页面隐藏事件 + */ + onHide() { + try { + let that = this + if (that.devTools_pageData) { + that.devTools_pageData.isOnShow = false + pageStatisticsReport(that.devTools_pageData.route, that.devTools_pageData.activeTime) + that.devTools_pageData.activeTime = 0 + } + } catch (error) { + console.log('devTools mixin onHide error ', error) + } + }, + /** + * * 页面卸载事件 + */ + onUnload() { + try { + let that = this + logReport(`onUnload>${that.devTools_pageData.route}`) + that.devTools_pageData = null + } catch (error) { + console.log('devTools mixin onUnload error ', error) + } + }, +} diff --git a/src/devTools/index.js b/src/devTools/index.js new file mode 100644 index 0000000..76d2f5c --- /dev/null +++ b/src/devTools/index.js @@ -0,0 +1,177 @@ +import drawView from './core/libs/drawView' +import logReport from './core/libs/logReport' +import errorReport from './core/libs/errorReport' +import devOptions from './core/libs/devOptions' +import createH5Bubble from './core/libs/createH5Bubble' +import vueMixin from './core/proxy/vueMixin' +import devToolsProxyInstall from './core/proxy/index' +import pageLinkList from './core/libs/pageLinkList' + +/** + * @type {Vue} + */ +let that + +const devTools = { + options: null, + /** + * 挂载安装APP页面 + */ + install(vm, options) { + try { + that = vm + let _this = this + + if (vm && vm.config && vm.config.globalProperties) { + vm.config.globalProperties.$logReport = logReport + } else { + vm.prototype.$logReport = logReport + } + + //! 初始化配置项 + devOptions.setOptions(options) + options = devOptions.getOptions() + _this.options = options + + if (!options || !options.status) { + return console.log( + '%c devTools 调试工具未运行!', + 'padding: 4px;background-color: red;color: #fff;font-size: 15px;', + ) + } + + //! 挂载dev工具 + if (vm && vm.config && vm.config.globalProperties) { + vm.config.globalProperties.$devTools = devTools + } else { + vm.prototype.$devTools = devTools + } + + if (options.error.status) { + //! 挂载vue报错 + vm.config.errorHandler = (err, vm, trace) => { + errorReport(err, trace, 've') + } + + //! 挂载vue警告 + vm.config.warnHandler = (err, vm, trace) => { + errorReport(err, trace, 'vw') + } + } + + //!混入生命周期监听器 + vm.mixin(vueMixin) + + //!绘制环境变量小标签 + // #ifdef APP-PLUS + drawView(options, devTools) + // #endif + // #ifdef H5 + createH5Bubble(options, devTools) + // #endif + + //!调试工具全局拦截器挂载 + devToolsProxyInstall(options) + + //! 注册dev弹窗打开事件 + uni.$on('devTools_showDialog', () => { + _this.show() + }) + + //! 注册dev弹窗关闭事件 + uni.$on('devTools_closeDialog', (options) => { + _this.hide(options) + }) + + //! 挂载uni对象 + uni.$dev = { + show() { + _this.show() + }, + hide() { + _this.hide() + }, + errorReport, + logReport, + } + + //! 注册jsRunner执行事件 + uni.$on('devTools_jsRunner', (code) => { + let result = undefined + try { + let fun = ('ev' + '__混淆__' + 'al').replace('__混淆__', '') + result = globalThis[fun](code) + // result = eval(code); + } catch (error) { + if (error && error.message) { + result = error.message + } + } + + uni.$emit('devTools_jsRunnerCallback', result) + }) + + // ! 页面路由列表 + pageLinkList.install() + } catch (error) { + console.log('devTools install error', error) + } + }, + /** + * 打开调试弹窗 + */ + show() { + let pages = getCurrentPages() + + //! 已经打开了调试工具,不要重复显示 + if (pages[pages.length - 1].route == this.options.devRoute) { + return false + } + + uni.navigateTo({ + url: this.options.route, + animationType: 'none', + animationDuration: 0, + }) + }, + /** + * 隐藏调试弹窗 + */ + hide(options) { + // #ifdef APP-PLUS + uni.$emit('devTools_closeDevToolsPanel') + let isBack = false + uni.$once('devTools_panelHideSuccess', () => { + if (!isBack) { + isBack = true + uni.navigateBack() + } + }) + setTimeout(() => { + if (!isBack) { + isBack = true + uni.navigateBack() + } + }, 500) + // #endif + // #ifndef APP-PLUS + uni.navigateBack() + // #endif + + if (options && options.navigateToUrl) { + let t = 600 + // #ifndef APP-PLUS + t = 200 + // #endif + setTimeout(() => { + uni.navigateTo({ + url: options.navigateToUrl, + }) + }, t) + } + }, + errorReport, + logReport, +} + +export default devTools diff --git a/src/devTools/page/components/bottomTools.vue b/src/devTools/page/components/bottomTools.vue new file mode 100644 index 0000000..10028f7 --- /dev/null +++ b/src/devTools/page/components/bottomTools.vue @@ -0,0 +1,914 @@ +