const CONSOLE_TYPES = ['log', 'warn', 'error', 'info', 'debug'];
|
let sendConsole = null;
|
const messageQueue = [];
|
function sendConsoleMessages(messages) {
|
if (sendConsole == null) {
|
messageQueue.push(...messages);
|
return;
|
}
|
sendConsole(JSON.stringify({
|
type: 'console',
|
data: messages,
|
}));
|
}
|
function setSendConsole(value) {
|
sendConsole = value;
|
if (value != null && messageQueue.length > 0) {
|
const messages = messageQueue.slice();
|
messageQueue.length = 0;
|
sendConsoleMessages(messages);
|
}
|
}
|
const originalConsole = /*@__PURE__*/ CONSOLE_TYPES.reduce((methods, type) => {
|
methods[type] = console[type].bind(console);
|
return methods;
|
}, {});
|
const atFileRegex = /^\s*at\s+[\w/./-]+:\d+$/;
|
function rewriteConsole() {
|
function wrapConsole(type) {
|
return function (...args) {
|
const originalArgs = [...args];
|
if (originalArgs.length) {
|
const maybeAtFile = originalArgs[originalArgs.length - 1];
|
// 移除最后的 at pages/index/index.uvue:6
|
if (typeof maybeAtFile === 'string' && atFileRegex.test(maybeAtFile)) {
|
originalArgs.pop();
|
}
|
}
|
if (__UNI_CONSOLE_KEEP_ORIGINAL__) {
|
originalConsole[type](...originalArgs);
|
}
|
sendConsoleMessages([formatMessage(type, args)]);
|
};
|
}
|
// 百度小程序不允许赋值,所以需要判断是否可写
|
if (isConsoleWritable()) {
|
CONSOLE_TYPES.forEach((type) => {
|
console[type] = wrapConsole(type);
|
});
|
return function restoreConsole() {
|
CONSOLE_TYPES.forEach((type) => {
|
console[type] = originalConsole[type];
|
});
|
};
|
}
|
else {
|
// @ts-expect-error
|
const oldLog = uni.__f__;
|
if (oldLog) {
|
// 重写 uni.__f__ 方法,这样的话,仅能打印开发者代码里的日志,其他没有被重写为__f__的日志将无法打印(比如uni-app框架、小程序框架等)
|
// @ts-expect-error
|
uni.__f__ = function (...args) {
|
const [type, filename, ...rest] = args;
|
// 原始日志移除 filename
|
oldLog(type, '', ...rest);
|
sendConsoleMessages([formatMessage(type, [...rest, filename])]);
|
};
|
return function restoreConsole() {
|
// @ts-expect-error
|
uni.__f__ = oldLog;
|
};
|
}
|
}
|
return function restoreConsole() { };
|
}
|
function isConsoleWritable() {
|
const value = console.log;
|
const sym = Symbol();
|
try {
|
// @ts-expect-error
|
console.log = sym;
|
}
|
catch (ex) {
|
return false;
|
}
|
// @ts-expect-error
|
const isWritable = console.log === sym;
|
console.log = value;
|
return isWritable;
|
}
|
function formatMessage(type, args) {
|
try {
|
return {
|
type,
|
args: formatArgs(args),
|
};
|
}
|
catch (e) {
|
originalConsole.error(e);
|
}
|
return {
|
type,
|
args: [],
|
};
|
}
|
function formatArgs(args) {
|
return args.map((arg) => formatArg(arg));
|
}
|
function formatArg(arg, depth = 0) {
|
if (depth >= 7) {
|
return {
|
type: 'object',
|
value: '[Maximum depth reached]',
|
};
|
}
|
return ARG_FORMATTERS[typeof arg](arg, depth);
|
}
|
function formatObject(value, depth) {
|
if (value === null) {
|
return {
|
type: 'null',
|
};
|
}
|
if (isComponentPublicInstance(value)) {
|
return formatComponentPublicInstance(value, depth);
|
}
|
if (isComponentInternalInstance(value)) {
|
return formatComponentInternalInstance(value, depth);
|
}
|
if (isUniElement(value)) {
|
return formatUniElement(value, depth);
|
}
|
if (isCSSStyleDeclaration(value)) {
|
return formatCSSStyleDeclaration(value, depth);
|
}
|
if (Array.isArray(value)) {
|
return {
|
type: 'object',
|
subType: 'array',
|
value: {
|
properties: value.map((v, i) => formatArrayElement(v, i, depth + 1)),
|
},
|
};
|
}
|
if (value instanceof Set) {
|
return {
|
type: 'object',
|
subType: 'set',
|
className: 'Set',
|
description: `Set(${value.size})`,
|
value: {
|
entries: Array.from(value).map((v) => formatSetEntry(v, depth + 1)),
|
},
|
};
|
}
|
if (value instanceof Map) {
|
return {
|
type: 'object',
|
subType: 'map',
|
className: 'Map',
|
description: `Map(${value.size})`,
|
value: {
|
entries: Array.from(value.entries()).map((v) => formatMapEntry(v, depth + 1)),
|
},
|
};
|
}
|
if (value instanceof Promise) {
|
return {
|
type: 'object',
|
subType: 'promise',
|
value: {
|
properties: [],
|
},
|
};
|
}
|
if (value instanceof RegExp) {
|
return {
|
type: 'object',
|
subType: 'regexp',
|
value: String(value),
|
className: 'Regexp',
|
};
|
}
|
if (value instanceof Date) {
|
return {
|
type: 'object',
|
subType: 'date',
|
value: String(value),
|
className: 'Date',
|
};
|
}
|
if (value instanceof Error) {
|
return {
|
type: 'object',
|
subType: 'error',
|
value: value.message || String(value),
|
className: value.name || 'Error',
|
};
|
}
|
return {
|
type: 'object',
|
value: {
|
properties: Object.entries(value).map(([name, value]) => formatObjectProperty(name, value, depth + 1)),
|
},
|
};
|
}
|
function isComponentPublicInstance(value) {
|
return value.$ && isComponentInternalInstance(value.$);
|
}
|
function isComponentInternalInstance(value) {
|
return value.type && value.uid != null && value.appContext;
|
}
|
function formatComponentPublicInstance(value, depth) {
|
return {
|
type: 'object',
|
className: 'ComponentPublicInstance',
|
value: {
|
properties: Object.entries(value.$.type).map(([name, value]) => formatObjectProperty(name, value, depth + 1)),
|
},
|
};
|
}
|
function formatComponentInternalInstance(value, depth) {
|
return {
|
type: 'object',
|
className: 'ComponentInternalInstance',
|
value: {
|
properties: Object.entries(value.type).map(([name, value]) => formatObjectProperty(name, value, depth + 1)),
|
},
|
};
|
}
|
function isUniElement(value) {
|
return value.style && value.tagName != null && value.nodeName != null;
|
}
|
function formatUniElement(value, depth) {
|
return {
|
type: 'object',
|
// 非 x 没有 UniElement 的概念
|
// className: 'UniElement',
|
value: {
|
properties: Object.entries(value)
|
.filter(([name]) => [
|
'id',
|
'tagName',
|
'nodeName',
|
'dataset',
|
'offsetTop',
|
'offsetLeft',
|
'style',
|
].includes(name))
|
.map(([name, value]) => formatObjectProperty(name, value, depth + 1)),
|
},
|
};
|
}
|
function isCSSStyleDeclaration(value) {
|
return (typeof value.getPropertyValue === 'function' &&
|
typeof value.setProperty === 'function' &&
|
value.$styles);
|
}
|
function formatCSSStyleDeclaration(style, depth) {
|
return {
|
type: 'object',
|
value: {
|
properties: Object.entries(style.$styles).map(([name, value]) => formatObjectProperty(name, value, depth + 1)),
|
},
|
};
|
}
|
function formatObjectProperty(name, value, depth) {
|
return Object.assign(formatArg(value, depth), {
|
name,
|
});
|
}
|
function formatArrayElement(value, index, depth) {
|
return Object.assign(formatArg(value, depth), {
|
name: `${index}`,
|
});
|
}
|
function formatSetEntry(value, depth) {
|
return {
|
value: formatArg(value, depth),
|
};
|
}
|
function formatMapEntry(value, depth) {
|
return {
|
key: formatArg(value[0], depth),
|
value: formatArg(value[1], depth),
|
};
|
}
|
const ARG_FORMATTERS = {
|
function(value) {
|
return {
|
type: 'function',
|
value: `function ${value.name}() {}`,
|
};
|
},
|
undefined() {
|
return {
|
type: 'undefined',
|
};
|
},
|
object(value, depth) {
|
return formatObject(value, depth);
|
},
|
boolean(value) {
|
return {
|
type: 'boolean',
|
value: String(value),
|
};
|
},
|
number(value) {
|
return {
|
type: 'number',
|
value: String(value),
|
};
|
},
|
bigint(value) {
|
return {
|
type: 'bigint',
|
value: String(value),
|
};
|
},
|
string(value) {
|
return {
|
type: 'string',
|
value,
|
};
|
},
|
symbol(value) {
|
return {
|
type: 'symbol',
|
value: value.description,
|
};
|
},
|
};
|
|
function initRuntimeSocket(hosts, port, id) {
|
if (!hosts || !port || !id)
|
return Promise.resolve(null);
|
return hosts
|
.split(',')
|
.reduce((promise, host) => {
|
return promise.then((socket) => {
|
if (socket)
|
return socket;
|
return tryConnectSocket(host, port, id);
|
});
|
}, Promise.resolve(null));
|
}
|
const SOCKET_TIMEOUT = 500;
|
function tryConnectSocket(host, port, id) {
|
return new Promise((resolve, reject) => {
|
const socket = uni.connectSocket({
|
url: `ws://${host}:${port}/${id}`,
|
// 支付宝小程序 是否开启多实例
|
multiple: true,
|
fail() {
|
resolve(null);
|
},
|
});
|
const timer = setTimeout(() => {
|
if (process.env.UNI_DEBUG) {
|
originalConsole.log(`uni-app:[${Date.now()}][socket]`, `connect timeout: ${host}`);
|
}
|
socket.close({
|
code: 1006,
|
reason: 'connect timeout',
|
});
|
resolve(null);
|
}, SOCKET_TIMEOUT);
|
socket.onOpen((e) => {
|
if (process.env.UNI_DEBUG) {
|
originalConsole.log(`uni-app:[${Date.now()}][socket]`, `connect success: ${host}`, e);
|
}
|
clearTimeout(timer);
|
resolve(socket);
|
});
|
socket.onClose((e) => {
|
if (process.env.UNI_DEBUG) {
|
originalConsole.log(`uni-app:[${Date.now()}][socket]`, `connect close: ${host}`, e);
|
}
|
clearTimeout(timer);
|
resolve(null);
|
});
|
socket.onError((e) => {
|
if (process.env.UNI_DEBUG) {
|
originalConsole.log(`uni-app:[${Date.now()}][socket]`, `connect error: ${host}`, e);
|
}
|
clearTimeout(timer);
|
resolve(null);
|
});
|
});
|
}
|
|
let sendError = null;
|
// App.onError会监听到两类错误,一类是小程序自身抛出的,一类是 vue 的 errorHandler 触发的
|
// uni.onError 和 App.onError 会同时监听到错误(主要是App.onError监听之前的错误),所以需要用 Set 来去重
|
// uni.onError 会在 App.onError 上边同时增加监听,因为要监听 vue 的errorHandler
|
// 目前 vue 的 errorHandler 仅会callHook('onError'),所以需要把uni.onError的也挂在 App.onError 上
|
const errorQueue = new Set();
|
function sendErrorMessages(errors) {
|
if (sendError == null) {
|
errors.forEach((error) => {
|
errorQueue.add(error);
|
});
|
return;
|
}
|
sendError(JSON.stringify({
|
type: 'error',
|
data: errors.map((err) => {
|
const isPromiseRejection = err && 'promise' in err && 'reason' in err;
|
const prefix = isPromiseRejection ? 'UnhandledPromiseRejection: ' : '';
|
if (isPromiseRejection) {
|
err = err.reason;
|
}
|
if (err instanceof Error && err.stack) {
|
return prefix + err.stack;
|
}
|
if (typeof err === 'object' && err !== null) {
|
try {
|
return prefix + JSON.stringify(err);
|
}
|
catch (err) {
|
return prefix + String(err);
|
}
|
}
|
return prefix + String(err);
|
}),
|
}));
|
}
|
function setSendError(value) {
|
sendError = value;
|
if (value != null && errorQueue.size > 0) {
|
const errors = Array.from(errorQueue);
|
errorQueue.clear();
|
sendErrorMessages(errors);
|
}
|
}
|
function initOnError() {
|
function onError(error) {
|
try {
|
// 小红书小程序 socket.send 时,会报错,onError错误信息为:
|
// Cannot create property 'errMsg' on string 'taskId'
|
// 导致陷入死循环
|
if (typeof PromiseRejectionEvent !== 'undefined' &&
|
error instanceof PromiseRejectionEvent &&
|
error.reason instanceof Error &&
|
error.reason.message &&
|
error.reason.message.includes(`Cannot create property 'errMsg' on string 'taskId`)) {
|
return;
|
}
|
if (__UNI_CONSOLE_KEEP_ORIGINAL__) {
|
originalConsole.error(error);
|
}
|
sendErrorMessages([error]);
|
}
|
catch (err) {
|
originalConsole.error(err);
|
}
|
}
|
if (typeof uni.onError === 'function') {
|
uni.onError(onError);
|
}
|
if (typeof uni.onUnhandledRejection === 'function') {
|
uni.onUnhandledRejection(onError);
|
}
|
return function offError() {
|
if (typeof uni.offError === 'function') {
|
uni.offError(onError);
|
}
|
if (typeof uni.offUnhandledRejection === 'function') {
|
uni.offUnhandledRejection(onError);
|
}
|
};
|
}
|
|
function initRuntimeSocketService() {
|
const hosts = __UNI_SOCKET_HOSTS__;
|
const port = __UNI_SOCKET_PORT__;
|
const id = __UNI_SOCKET_ID__;
|
if (!hosts || !port || !id)
|
return Promise.resolve(false);
|
// 百度小程序需要延迟初始化,不然会存在循环引用问题vendor.js
|
const lazy = typeof swan !== 'undefined';
|
// 重写需要同步,避免丢失早期日志信息
|
let restoreError = lazy ? () => { } : initOnError();
|
let restoreConsole = lazy ? () => { } : rewriteConsole();
|
// 百度小程序需要异步初始化,不然调用 uni.connectSocket 会循环引入vendor.js
|
return Promise.resolve().then(() => {
|
if (lazy) {
|
restoreError = initOnError();
|
restoreConsole = rewriteConsole();
|
}
|
return initRuntimeSocket(hosts, port, id).then((socket) => {
|
if (!socket) {
|
restoreError();
|
restoreConsole();
|
originalConsole.error(`开发模式下日志通道建立 socket 连接失败。
|
如果是小程序平台,请勾选不校验合法域名配置。
|
如果是运行到真机,请确认手机与电脑处于同一网络。`);
|
return false;
|
}
|
socket.onClose(() => {
|
if (process.env.UNI_DEBUG) {
|
originalConsole.log(`uni-app:[${Date.now()}][socket]`, 'connect close and restore');
|
}
|
originalConsole.error('开发模式下日志通道 socket 连接关闭,请在 HBuilderX 中重新运行。');
|
restoreError();
|
restoreConsole();
|
});
|
setSendConsole((data) => {
|
if (process.env.UNI_DEBUG) {
|
originalConsole.log(`uni-app:[${Date.now()}][console]`, data);
|
}
|
socket.send({
|
data,
|
});
|
});
|
setSendError((data) => {
|
if (process.env.UNI_DEBUG) {
|
originalConsole.log(`uni-app:[${Date.now()}][error]`, data);
|
}
|
socket.send({
|
data,
|
});
|
});
|
return true;
|
});
|
});
|
}
|
initRuntimeSocketService();
|
|
export { initRuntimeSocketService };
|