一、背景:
爲了使系統更加穩定,在用戶使用期間,若發現異常,可及時應對,採取了“報警機制”。
通常“報警機制”分爲2種,一種是後端對api監控及自定義監控,出現異常,通過釘釘或郵件的形式通知,第二種是前端對js語法,vue語法,自定義報錯進行監控,以此來規範代碼質量,保證系統預警
二、流程步驟
1. 收集錯誤(錯誤類型包含 vue錯誤 + js 錯誤 + Promise 錯誤 + 自定義錯誤)
2. 關聯釘釘
3. 錯誤信息發送
三、前期準備內容
1. 釘釘軟件,自定義機器人接入,文檔鏈接
2. 簽名計算(前後端聯調,統一加密方式)
四、參考代碼
1. 收集錯誤階段(main.js)
1 import { createApp } from 'vue' 2 import { errorHandler, detectOS, digits, getBrowserInfo, format } from '@/assets/scripts/errorPlugin' // 收集錯誤信息 3 4 const app = createApp(App) 5 // 1. 用於組件生命週期中的錯誤、自定義事件處理函數內部錯誤、v-on DOM 監聽器內部拋出的錯誤、處理返回 Promise 鏈的錯誤 6 app.config.errorHandler = errorHandler 7 // 2. 處理 JS 的額外錯誤 8 // eslint-disable-next-line max-params 9 window.onerror = function (message, source, line, column, error) { 10 // 瞭解文檔: https://juejin.cn/post/6844904093979246606 11 if (message === 'ResizeObserver loop limit exceeded') { 12 console.warn('Ignored: ResizeObserver loop limit exceeded') 13 return false 14 } 15 if (message == 'cancel') return false 16 let errMsg = null 17 if (message == 'Script error.') { 18 // 跨域 19 errMsg = ` 20 --infoType: JS 無法訪問, 請在控制檯查看具體錯誤 21 --apName : 用戶端-${process.env.NODE_ENV === 'development' ? '測試環境' : '生產環境'} 22 --url: ${window.location.href} 23 --browser:${detectOS()}-${digits()} ${getBrowserInfo()} 24 --time: ${format('yyyy-MM-dd hh:mm:ss')} 25 --userInfo: ${sessionStorage.getItem('AI_INFO')} 26 --info: 瀏覽器跨域請求一個腳本執行出錯 27 ` 28 return false 29 } 30 // ------排除這兩個文件錯誤信息的檢查開始----- 31 let noNeedFile = ['app', 'contextMenuFilter'] 32 let noContinue = false 33 noNeedFile.map((res) => { 34 if (source.indexOf(res) != -1) noContinue = true 35 }) 36 console.log('排除文件了') 37 if (noContinue) return false 38 console.log('沒排除文件') 39 // ------排除這兩個文件錯誤信息的檢查結束----- 40 errMsg = ` 41 --infoType: JS 錯誤 42 --apName : 用戶端-${process.env.NODE_ENV === 'development' ? '測試環境' : '生產環境'} 43 --url: ${window.location.href} 44 --browser:${detectOS()}-${digits()} ${getBrowserInfo()} 45 --time: ${format('yyyy-MM-dd hh:mm:ss')} 46 --userInfo: ${sessionStorage.getItem('AI_INFO')} 47 --info: ${message}-${source}-${JSON.stringify(error)} 48 ` 49 errorHandler(errMsg, null, 'JS錯誤') 50 } 51 // 3. 處理 Promise 錯誤 52 window.addEventListener('unhandledrejection', (event) => { 53 console.log('event', event.reason) 54 // 全局存在的未處理的 Promise 異常,比如: Promise.reject() 55 // 場景: 接口異常 56 if (event.reason == 'cancel') return false 57 let errMsg = ` 58 --infoType: 捕獲Promise異常 59 --apName : 用戶端-${process.env.NODE_ENV === 'development' ? '測試環境' : '生產環境'} 60 --url: ${window.location.href} 61 --browser:${detectOS()}-${digits()} ${getBrowserInfo()} 62 --time: ${format('yyyy-MM-dd hh:mm:ss')} 63 --userInfo: ${sessionStorage.getItem('AI_INFO')} 64 --errorInfo: ${JSON.stringify(event.reason)} 65 ` 66 67 errorHandler(errMsg, null, 'Promise錯誤') 68 }) 69 app.mount('#app')
2. 關聯釘釘(src\assets\scripts\robot.js)
3. 發送錯誤信息到釘釘軟件(src\assets\scripts\errorPlugin.js)
import ChatBot from './robot.js' export const errorHandler = (err, vm, info) => { let token = sessionStorage.getItem('AI_TOKEN') || null if (!token) return let errInfo = null if (info !== 'JS錯誤' || info !== 'Promise錯誤') { errInfo = ` --infoType: vue異常錯誤 --apName : 用戶端-${process.env.NODE_ENV === 'development' ? '測試環境' : '生產環境'} --url: ${window.location.href} --browser:${detectOS()}-${digits()} ${getBrowserInfo()} --time: ${format('yyyy-MM-dd hh:mm:ss')} --userInfo: ${sessionStorage.getItem('AI_INFO')} --errorInfo: ${err}-${JSON.stringify(info)} ` } else { errInfo = err } // 將捕獲的錯誤, 通過釘釘報警 robotDD(errInfo) } const robotDD = (errMsg) => { const robot = new ChatBot({ webhook: 'https://oapi.dingtalk.com/robot/send?access_token=***', secret: '***' }) // 規定發送的消息的類型和參數 let textContent = { msgtype: 'text', text: { content: errMsg // 注意了,字符串裏面的錯誤漢字,其實就是你在釘釘報警設置的自定義字段,兩個地方需要相同,否則不會發送到羣裏 } } // 機器人發送消息 robot .send(textContent) .then((res) => { console.error(res) }) .catch(() => { console.log('釘釘報警錯誤') // ElMessage.error({ // message: `釘釘報警錯誤` // }) }) } export const format = (fmt) => { //author: meizz var o = { 'M+': new Date().getMonth() + 1, //月份 'd+': new Date().getDate(), //日 'h+': new Date().getHours(), //小時 'm+': new Date().getMinutes(), //分 's+': new Date().getSeconds(), //秒 'q+': Math.floor((new Date().getMonth() + 3) / 3), //季度 S: new Date().getMilliseconds() //毫秒 } if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (new Date().getFullYear() + '').substr(4 - RegExp.$1.length)) for (var k in o) if (new RegExp('(' + k + ')').test(fmt)) fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)) return fmt } /** * 初始化設備信息 */ export const initDeviceInfo = () => { let _deviceInfo = '' //設備信息 console.log(navigator, 'navigator') if (navigator == null) { _deviceInfo = 'PC' } if (navigator.userAgent != null) { var su = navigator.userAgent.toLowerCase(), mb = ['ipad', 'iphone os', 'midp', 'rv:1.2.3.4', 'ucweb', 'android', 'windows ce', 'windows mobile'] // 開始遍歷提前設定好的設備關鍵字,如果設備信息中包含關鍵字,則說明是該設備 for (var i in mb) { if (su.indexOf(mb[i]) > 0) { _deviceInfo = mb[i] break } } } return _deviceInfo } /** * 獲取瀏覽器的信息 */ export const getBrowserInfo = () => { var output = 'other' // Opera 8.0+ var isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0 if (isOpera) { output = 'Opera' } // Firefox 1.0+ var isFirefox = typeof InstallTrigger !== 'undefined' if (isFirefox) { output = 'Firefox' } // Safari 3.0+ "[object HTMLElementConstructor]" var isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === '[object SafariRemoteNotification]' })(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification)) if (isSafari) { output = 'Safari' } // Internet Explorer 6-11 var isIE = /*@cc_on!@*/ false || !!document.documentMode if (isIE) { output = 'IE' } // Edge 20+ var isEdge = !isIE && !!window.StyleMedia if (isEdge) { output = 'Edge' } // Chrome 1 - 79 var isChrome = !!window.chrome && navigator.userAgent.indexOf('Chrome') !== -1 if (isChrome) { output = 'Chrome' } // Edge (based on chromium) detection var isEdgeChromium = isChrome && navigator.userAgent.indexOf('Edg') !== -1 if (isEdgeChromium) { output = 'EdgeChromium' } return output } export const detectOS = () => { var userAgent = window.navigator.userAgent, platform = window.navigator.platform, macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'], iosPlatforms = ['iPhone', 'iPad', 'iPod'], os = null if (macosPlatforms.indexOf(platform) !== -1) { os = 'Mac OS' } else if (iosPlatforms.indexOf(platform) !== -1) { os = 'iOS' } else if (windowsPlatforms.indexOf(platform) !== -1) { os = 'Windows' } else if (/Android/.test(userAgent)) { os = 'Android' } else if (!os && /Linux/.test(platform)) { os = 'Linux' } return os } export const digits = () => { var sUserAgent = navigator.userAgent var is64 = sUserAgent.indexOf('WOW64') > -1 if (is64) { return '64bit' } else { return '32bit' } }
五、跨域解決
import { defineConfig } from 'vite' export default defineConfig(({ command, mode, ssrBuild }) => { return { base: '/', server: { host: '0.0.0.0', port: 8080, proxy: { '/api': { target: 'https://oapi.dingtalk.com', // 代理地址 changeOrigin: true, // 是否允許跨域,爲true代表允許 rewrite: (path) => path.replace(/^\/api/, '') } } } } })
六、最終實現效果