uniapp项目制作app外壳,h5应用嵌入其中,开发电视机App。相比pc的h5项目,错误调试以及问题排查就比较费劲了。常规的办法就是通过toast将信息输出到屏幕上,这种方式也只适用于开发阶段,并且还有局限性,只能作为调试的手段,针对一些详细信息的查看,就显得无能为力了。pc的h5项目,无论是开发阶段,还是生产环境,即使出现问题,也可以通过查看控制台的方式排查问题。所以app项目也需要一个类似的功能,方便调试和排查问题,计划采用本地文件记录日志的方案。
一、实现思路
1、按照日期,一天记录一个文件,最多保留7个文件。
2、规定日志格式,日期、日志级别、日志类型、日志内容
3、实现写入内容到指定文件的方法
4、全局错误信息捕获,以及日志记录
5、全局接口响应错误信息监听,以及日志记录
6、app与h5交互指令监听,以及日志记录
7、h5接收服务端指令监听,以及日志记录
8、关键业务节点日志记录
二、具体实现
1、依赖基础api
/** * 返回当前日期 YYYY-MM-DD */ export function getToday(date) { // const date = new Date() const seperator1 = '-' const year = date.getFullYear() let month = date.getMonth() + 1 let strDate = date.getDate() if (month >= 1 && month <= 9) { month = '0' + month } if (strDate >= 0 && strDate <= 9) { strDate = '0' + strDate } const currentdate = year + seperator1 + month + seperator1 + strDate return currentdate } function supplyZero(number) { return number >= 10 ? number : '0' + number } // 获取当前时间 export function getNowTime() { const date = new Date() // 年 getFullYear():四位数字返回年份 const year = date.getFullYear() // getFullYear()代替getYear() // 月 getMonth():0 ~ 11 const month = date.getMonth() + 1 // 日 getDate():(1 ~ 31) const day = date.getDate() // 时 getHours():(0 ~ 23) const hour = date.getHours() // 分 getMinutes(): (0 ~ 59) const minute = date.getMinutes() // 秒 getSeconds():(0 ~ 59) const second = date.getSeconds() const time = year + '-' + supplyZero(month) + '-' + supplyZero(day) + ' ' + supplyZero(hour) + ':' + supplyZero(minute) + ':' + supplyZero(second) return time } // 获取给定日期至前七天的日期数组 export function getDay7(data) { // 传入 yyyy-MM-dd 格式 const datas = [] for (let i = 0; i < 7; i++) { datas.push(getBeforeDate(data, -i)) } return datas } /** * @param {Object} value Date对象 * @param {Object} fmt */ export function formatDate(value, fmt = 'yyyy-MM-dd') { let getDate if (value) { getDate = new Date(value) } else { getDate = new Date() } const o = { 'M+': getDate.getMonth() + 1, 'd+': getDate.getDate(), 'h+': getDate.getHours(), 'm+': getDate.getMinutes(), 's+': getDate.getSeconds(), 'q+': Math.floor((getDate.getMonth() + 3) / 3), S: getDate.getMilliseconds() } if (/(y+)/.test(fmt)) { fmt = fmt.replace(RegExp.$1, (getDate.getFullYear() + '').substr(4 - RegExp.$1.length)) } for (const 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 } // 获取xx天后的日期 传入负数 则是多少天前的日期 /** * @param {Object} date 日期字符串 yyyy-MM-dd * @param {Object} num 多少天 数字 */ export function getBeforeDate(date, num) { date = date.replace(/-/g, '/') var timestamp = new Date(date).getTime() return new Date(timestamp + num * 1000 * 60 * 60 * 24) }
2、清理超过范围的日志文件
/** * 清除超过范围的日志文件 */ function clearOverLog() { const day7Arr = getDay7(getToday(new Date())) const logNameArr = [] for (let i = 0; i < day7Arr.length; i++) { logNameArr.push('log_' + formatDate(day7Arr[i], 'yyyyMMdd') + '.txt') } try { const File = window.plus.android.importClass('java.io.File') const logFolder = new File('/sdcard/wisdomApp/log/') if (!logFolder.exists()) { logFolder.mkdirs() return } // 文件列表 const files = logFolder.listFiles() if (files && files.length > 7) { for (let i = 0; i < files.length; i++) { if (!logNameArr.includes(files[i].getName())) { files[i].delete() } } } } catch (e) { console.error(e) } }
3、将文本写入文件
/** * 将文本写入文件 * @param filePath 文件路径 * @param text 文本 * @param isAppend 是否追加 * @returns {boolean} */ function writeFileFun(filePath, res, isAppend) { try { // 只能用于安卓 导入java类 const File = window.plus.android.importClass('java.io.File') const FileWriter = window.plus.android.importClass('java.io.FileWriter') // 不加根目录创建文件(即用相对地址)的话directory.exists()这个判断一值都是false const n = filePath.lastIndexOf('/') if (n !== -1) { const fileDirs = filePath.substring(0, n) const directory = new File(fileDirs) if (!directory.exists()) { directory.mkdirs() // 不存在创建目录 } } const file = new File(filePath) if (!file.exists()) { file.createNewFile() // 创建文件 } const fos = new FileWriter(filePath, !!isAppend) fos.write(res) fos.close() return true } catch (e) { return false } }
4、记录日志
/** * 记录日志 * @param logLevel 日志级别 * @param logType 日志类型 * @param message 日志内容 */ function logInfo(logLevel, logType, message) { // 非App模式 或者不支持H5+ 跳过 if (window.localStorage.getItem('showModel') !== 'app' || !window.plus) { return } // 清除超过范围的日志文件 clearOverLog() // 今天的日期 const today = getToday(new Date()) // 拼接日志名称 const logName = 'log_' + today.replace(/-/g, '') + '.txt' // 处理日志内容 const messageLine = getNowTime() + '[' + logLevel + '][' + logType + ']:' + message + '\r\n' // 日志路径 const logFilePath = '/sdcard/wisdomApp/log/' + logName // 写入日志 writeFileFun(logFilePath, messageLine, true) }
5、全局错误日志处理
创建errorPlugin.js文件,以下为文件内容
import * as common from '@/common/common' // eslint-disable-next-line handle-callback-err function errorHandler(err, vm, info) { const messageObj = { info: info } if (err instanceof TypeError) { messageObj.message = err.message messageObj.name = err.name messageObj.fileName = err.fileName messageObj.lineNumber = err.lineNumber messageObj.columnNumber = err.columnNumber messageObj.stack = err.stack } else if (err instanceof String) { messageObj.error = err } common.logInfo('error', '全局代码错误', JSON.stringify(messageObj)) } const handleMethods = (instance) => { if (instance.$options.methods) { const actions = instance.$options.methods || {} for (const key in actions) { if (Object.hasOwnProperty.call(actions, key)) { const fn = actions[key] actions[key] = function(...args) { const ret = args.length > 0 ? fn.apply(this, args) : fn.call(this) if (isPromise(ret) && !ret._handled) { ret._handled = true return ret.catch((e) => errorHandler(e, this, `捕获到了未处理的Promise异常: (Promise/async)`)) } } } } } } function isPromise(ret) { return ret && typeof ret.then === 'function' && typeof ret.catch === 'function' } const GlobalError = { install: (Vue, options) => { Vue.config.errorHandler = errorHandler // eslint-disable-next-line max-params window.onerror = function(message, source, line, column, error) { errorHandler(error, null, message) // console.log('全局捕获错误', message, source, line, column, error) } window.addEventListener('unhandledrejection', (event) => { errorHandler(event, null, '全局捕获未处理的Promise异常') }) Vue.mixin({ beforeCreate() { handleMethods(this) } }) } } export default GlobalError
main.js中引入文件
// 引入错误处理插件 import ErrorPlugin from './common/errorPlugin' Vue.use(ErrorPlugin)
6、接口响应错误监听
在axios配置中处理
import axios from 'axios' import * as stringUtils from '@/utils/string' import * as common from '@/common/common' // 创建一个 axios 实例 const service = axios.create({ baseURL: window.g.ApiUrl, // url = base url + request url timeout: window.g.AXIOS_TIMEOUT // request timeout }) // response interceptor 响应拦截器 service.interceptors.response.use( response => { const res = response // JSON.stringify(response) if (res.config.responseType === 'blob') { return res } return res }, error => { common.logInfo('error', '接口响应错误', JSON.stringify(error)) return Promise.reject(error) } )
7、记录h5与app的通信指令
8、记录服务端的websocket指令
9、关键节点业务日志记录
三、实现效果
四、注意事项
全局监听错误日志一共有几种处理手段,具体参考《Vue项目处理错误上报如此简单》
vue的errorHandler配置
window.onerror监听
未处理的promise异常
以下几点需要特别注意
1、项目使用npm run dev启动,window.onerror监听的错误全部会被重写为Script error,如图
原因如下,参考《window.onerror()的用法与实例分析》
vue项目dev启动出现Script error的并未找到解决方案,值得高兴的是,npm run build打包后的日志是正常的
2、手动抛出异常测试效果时,发现setTimeout宏任务的异步错误是window.onerror捕获的,而直接在create钩子中的错误是被errorHandler捕获的
3、error参数是TypeError类型时,无法使用JSON.stringIfy获取有效信息,vm是vue组件对象,无法使用JSON.stringify格式化,只能手动获取err的几个属性。
五、后续
本地记录日志信息只是第一步,后面还需要在web端读取指定设备的日志,这样功能才算完整。
发表评论