UNPKG

hc-web-log-mon

Version:

基于 JS 跨平台插件,为前端项目提供【 行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段

510 lines (465 loc) 12.7 kB
import { AnyFun, AnyObj } from '../types' import { logError } from './debug' import { isRegExp, isArray, isFunction, isNumber } from './is' import { isInit } from '../utils/global' /** * 添加事件监听器 * @param target 对象 * @param eventName 事件名称 * @param handler 回调函数 * @param opitons */ export function on( target: Window | Document, eventName: string, handler: AnyFun, opitons = false ): void { target.addEventListener(eventName, handler, opitons) } /** * 重写对象上面的某个属性 * @param source 需要被重写的对象 * @param name 需要被重写对象的key * @param replacement 以原有的函数作为参数,执行并重写原有函数 * @param isForced 是否强制重写(可能原先没有该属性) */ export function replaceAop( source: AnyObj, name: string, replacement: AnyFun, isForced = false ): void { if (source === undefined) return if (name in source || isForced) { const original = source[name] const wrapped = replacement(original) if (isFunction(wrapped)) { source[name] = wrapped } } } /** * 格式化对象(针对数字类型属性) * 小数位数保留最多两位、空值赋 undefined * @param source 源对象 */ export function normalizeObj(source: AnyObj) { Object.keys(source).forEach(p => { const v = source[p] if (isNumber(v)) { source[p] = v === 0 ? undefined : parseFloat(v.toFixed(2)) } }) return source } /** * 获取当前页面的url * @returns 当前页面的url */ export function getLocationHref(): string { if (typeof document === 'undefined' || document.location == null) return '' return document.location.href } /** * 获取当前的时间戳 * @returns 当前的时间戳 */ export function getTimestamp(): number { return Date.now() } /** * 函数节流 * @param fn 需要节流的函数 * @param delay 节流的时间间隔 * @param runFirst 是否需要第一个函数立即执行 (每次) * @returns 返回一个包含节流功能的函数 */ export function throttle(func: AnyFun, wait: number, runFirst = false) { let timer: NodeJS.Timeout | null = null let lastArgs: any[] return function (this: any, ...args: any[]) { lastArgs = args if (timer === null) { if (runFirst) { func.apply(this, lastArgs) } timer = setTimeout(() => { timer = null func.apply(this, lastArgs) }, wait) } } } /** * 函数防抖 * @param func 需要防抖的函数 * @param wait 防抖的时间间隔 * @param runFirst 是否需要第一个函数立即执行 * @returns 返回一个包含防抖功能的函数 */ export function debounce(func: AnyFun, wait: number, runFirst = false) { let timer: NodeJS.Timeout | null = null return function (this: any, ...arg: any[]) { if (runFirst) { func.call(this, ...arg) runFirst = false } if (timer) clearTimeout(timer) timer = setTimeout(() => { func.call(this, ...arg) }, wait) } } /** * 将数组内对象以对象内的属性分类 * @param arr 数组源 - 格式为 [{}, {}...] * @param pop 是否需要在遍历后清除源数组内的数据 * @param keys 需要匹配的属性名 */ export function groupArray<T, K extends keyof T>( arr: T[], ...keys: K[] ): T[][] { const groups = new Map<string, T[]>() for (const obj of arr) { const key = keys .filter(k => obj[k]) .map(k => obj[k]) .join(':') if (!groups.has(key)) { groups.set(key, []) } groups.get(key)!.push(obj) } return Array.from(groups.values()) } /** * 深度合并对象 */ export function deepAssign<T>(target: AnyObj, ...sources: AnyObj[]) { sources.forEach(source => { for (const key in source) { if (source[key] !== null && isRegExp(source[key])) { target[key] = source[key] } else if (source[key] !== null && typeof source[key] === 'object') { // 如果当前 key 对应的值是一个对象或数组,则进行递归 target[key] = deepAssign( target[key] || (isArray(source[key]) ? [] : {}), source[key] ) } else { // 如果当前 key 对应的值是基本类型数据,则直接赋值 target[key] = source[key] } } }) return target as T } /** * 验证调用sdk暴露的方法之前是否初始化 * @param methodsName 方法名 * @returns 是否通过验证 */ export function validateMethods(methodsName: string): boolean { if (!isInit()) { logError(`${methodsName} 需要在SDK初始化之后使用`) return false } return true } /** * 判断入参类型 * @param target 任意入参 * @returns 类型 */ export function typeofAny(target: any): string { return Object.prototype.toString.call(target).slice(8, -1).toLowerCase() } /** * 判断对象中是否包含该属性 * @param key 键 * @param object 对象 * @returns 是否包含 */ export function isValidKey( key: string | number | symbol, object: object ): key is keyof typeof object { return key in object } /** * 随机概率通过 * @param randow 设定比例,例如 0.7 代表 70%的概率通过 * @returns 是否通过 */ export function randomBoolean(randow: number) { return Math.random() <= randow } /** * 补全字符 * @param {*} num 初始值 * @param {*} len 需要补全的位数 * @param {*} placeholder 补全的值 * @returns 补全后的值 */ export function pad(num: number, len: number, placeholder = '0') { const str = String(num) if (str.length < len) { let result = str for (let i = 0; i < len - str.length; i += 1) { result = placeholder + result } return result } return str } /** * 获取一个随机字符串 */ export function uuid() { const date = new Date() // yyyy-MM-dd的16进制表示,7位数字 const hexDate = parseInt( `${date.getFullYear()}${pad(date.getMonth() + 1, 2)}${pad( date.getDate(), 2 )}`, 10 ).toString(16) // hh-mm-ss-ms的16进制表示,最大也是7位 const hexTime = parseInt( `${pad(date.getHours(), 2)}${pad(date.getMinutes(), 2)}${pad( date.getSeconds(), 2 )}${pad(date.getMilliseconds(), 3)}`, 10 ).toString(16) // 第8位数字表示后面的time字符串的长度 let guid = hexDate + hexTime.length + hexTime // 补充随机数,补足32位的16进制数 while (guid.length < 32) { guid += Math.floor(Math.random() * 16).toString(16) } // 分为三段,前两段包含时间戳信息 return `${guid.slice(0, 8)}-${guid.slice(8, 16)}-${guid.slice(16)}` } /** * 获取cookie中目标name的值 * @param name cookie名 * @returns */ export function getCookieByName(name: string) { const result = document.cookie.match(new RegExp(`${name}=([^;]+)(;|$)`)) return result ? result[1] : undefined } /** * 发送数据方式 - navigator.sendBeacon */ export function sendByBeacon(url: string, data: any) { return navigator.sendBeacon(url, JSON.stringify(data)) } export const sendReaconImageList: any[] = [] /** * 发送数据方式 - image */ export function sendByImage(url: string, data: any): Promise<void> { return new Promise(resolve => { const beacon = new Image() beacon.src = `${url}?v=${encodeURIComponent(JSON.stringify(data))}` sendReaconImageList.push(beacon) beacon.onload = () => { // 发送成功 resolve() } beacon.onerror = function () { // 发送失败 resolve() } }) } /** * 发送数据方式 - xml */ export function sendByXML(url: string, data: any): Promise<void> { return new Promise(resolve => { const xhr = new XMLHttpRequest() xhr.open('post', url) xhr.setRequestHeader('content-type', 'application/json') xhr.send(JSON.stringify(data)) xhr.onreadystatechange = function () { if (xhr.readyState === 4) { resolve() } } }) } /** * 批量执行方法 * @param funList 方法数组 * @param through 是否将第一次参数贯穿全部方法 * @param args 额外参数 * @returns */ export function executeFunctions( funList: AnyFun[], through: boolean, args: any ): any { if (funList.length === 0) return args let result: any = undefined for (let i = 0; i < funList.length; i++) { const func = funList[i] if (i === 0 || through) { result = func(args) } else { result = func(result) } } return result } /** * 将未知参数转换为数组格式 * @param target */ export function unKnowToArray<T>(target: T[] | T): T[] { return (isArray(target) ? target : [target]) as T[] } const arrayMap = Array.prototype.map || function polyfillMap(this: any, fn) { const result = [] for (let i = 0; i < this.length; i += 1) { result.push(fn(this[i], i, this)) } return result } /** * map方法 * @param arr 源数组 * @param fn 条件函数 * @returns */ export function map(arr: any[], fn: AnyFun) { return arrayMap.call(arr, fn) } const arrayFilter = Array.prototype.filter || function filterPolyfill(this: any, fn: AnyFun) { const result = [] for (let i = 0; i < this.length; i += 1) { if (fn(this[i], i, this)) { result.push(this[i]) } } return result } /** * filter方法 * @param arr 源数组 * @param fn 条件函数 */ export function filter(arr: any[], fn: AnyFun) { return arrayFilter.call(arr, fn) } const arrayFind = Array.prototype.find || function findPolyfill(this: any, fn: AnyFun) { for (let i = 0; i < this.length; i += 1) { if (fn(this[i], i, this)) { return this[i] } } return undefined } /** * find方法 * @param arr 源数组 * @param fn 条件函数 */ export function find(arr: any[], fn: AnyFun) { return arrayFind.call(arr, fn) } /** * 去除头部或者尾部的空格 * @param str 需要去除的字符串 * @returns 去除后的字符串 */ export function trim(str = '') { return str.replace(/(^\s+)|(\s+$)/, '') } /** * 可以理解为异步执行 * requestIdleCallback 是浏览器空闲时会自动执行内部函数 * requestAnimationFrame 是浏览器必须执行的 * 关于 requestIdleCallback 和 requestAnimationFrame 可以参考 https://www.cnblogs.com/cangqinglang/p/13877078.html */ export const nextTime = window.requestIdleCallback || window.requestAnimationFrame || (callback => setTimeout(callback, 17)) /** * 取消异步执行 */ export const cancelNextTime = window.cancelIdleCallback || window.cancelAnimationFrame || clearTimeout /** * 判断对象是否超过指定kb大小 * @param object 源对象 * @param limitInKB 最大kb */ export function isObjectOverSizeLimit( object: object, limitInKB: number ): boolean { const serializedObject = JSON.stringify(object) const sizeInBytes = new TextEncoder().encode(serializedObject).length const sizeInKB = sizeInBytes / 1024 return sizeInKB > limitInKB } /** * 获取url地址上的参数 * @param url 请求url * @returns 参数对象 */ export function parseGetParams(url: string): AnyObj<string> { const params: AnyObj<string> = {} const query = url.split('?')[1] if (query) { const pairs = query.split('&') for (const pair of pairs) { const [key, value] = pair.split('=') params[decodeURIComponent(key)] = decodeURIComponent(value) } } return params } /** * 深拷贝 * 兼容函数,对象,相互引用场景 * @param target 需要深拷贝的原对象 * @return 深拷贝后的对象 */ export function deepCopy<T>(target: T, map = new Map()) { if (target !== null && typeof target === 'object') { let res = map.get(target) if (res) return res if (target instanceof Array) { res = [] map.set(target, res) target.forEach((item, index) => { res[index] = deepCopy(item, map) }) } else { res = {} map.set(target, res) Object.keys(target).forEach(key => { if (isValidKey(key, target)) { res[key] = deepCopy(target[key], map) } }) } return res } return target }