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 GlobalErrormain.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端读取指定设备的日志,这样功能才算完整。











发表评论