更新
This commit is contained in:
+113
@@ -0,0 +1,113 @@
|
||||
import type { Plugin } from 'vite'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import {
|
||||
defineUniMainJsPlugin,
|
||||
isEnableConsole,
|
||||
normalizePath,
|
||||
resolveBuiltIn,
|
||||
} from '@dcloudio/uni-cli-shared'
|
||||
|
||||
const uniConsoleRuntimePlugin = (): Plugin => {
|
||||
return {
|
||||
name: 'uni:console:runtime',
|
||||
config() {
|
||||
const isX = process.env.UNI_APP_X === 'true'
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
let keepOriginal = true
|
||||
if (
|
||||
process.env.UNI_PLATFORM == 'mp-harmony' ||
|
||||
process.env.UNI_PLATFORM === 'app-harmony'
|
||||
) {
|
||||
keepOriginal = false
|
||||
}
|
||||
const webviewEvalJsCode =
|
||||
isX && process.env.UNI_UTS_PLATFORM === 'app-android'
|
||||
? fs.readFileSync(
|
||||
path.join(__dirname, '../dist/__uniwebview.js'),
|
||||
'utf-8'
|
||||
)
|
||||
: ''
|
||||
return {
|
||||
define: {
|
||||
'process.env.UNI_CONSOLE_KEEP_ORIGINAL': process.env
|
||||
.UNI_CONSOLE_KEEP_ORIGINAL
|
||||
? process.env.UNI_CONSOLE_KEEP_ORIGINAL === 'true'
|
||||
: keepOriginal,
|
||||
'process.env.UNI_SOCKET_HOSTS': JSON.stringify(
|
||||
isProd ? '' : process.env.UNI_SOCKET_HOSTS
|
||||
),
|
||||
'process.env.UNI_SOCKET_PORT': JSON.stringify(
|
||||
isProd ? '' : process.env.UNI_SOCKET_PORT
|
||||
),
|
||||
'process.env.UNI_SOCKET_ID': JSON.stringify(
|
||||
isProd ? '' : process.env.UNI_SOCKET_ID
|
||||
),
|
||||
'process.env.UNI_CONSOLE_WEBVIEW_EVAL_JS_CODE':
|
||||
JSON.stringify(webviewEvalJsCode),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default () => {
|
||||
return [
|
||||
uniConsoleRuntimePlugin(),
|
||||
defineUniMainJsPlugin((opts) => {
|
||||
let hasRuntimeSocket = isEnableConsole()
|
||||
const isX = process.env.UNI_APP_X === 'true'
|
||||
// 基座类型为custom时,不启用运行时socket
|
||||
// 需要判断自定义基座是否包含socket模块,有的话才可以启用
|
||||
if (isX && process.env.UNI_PLATFORM === 'app') {
|
||||
if (process.env.HX_USE_BASE_TYPE === 'custom') {
|
||||
hasRuntimeSocket = false
|
||||
}
|
||||
}
|
||||
let uniConsolePath = resolveBuiltIn(
|
||||
path.join(
|
||||
'@dcloudio/uni-console',
|
||||
`dist/${
|
||||
(process.env.UNI_PLATFORM || '').startsWith('mp-') ? 'mp' : 'index'
|
||||
}.esm.js`
|
||||
)
|
||||
)
|
||||
if (isX) {
|
||||
if (process.env.UNI_UTS_PLATFORM === 'app-android') {
|
||||
uniConsolePath = resolveBuiltIn(
|
||||
path.join('@dcloudio/uni-console', 'src/runtime/app/index.ts')
|
||||
)
|
||||
} else if (process.env.UNI_UTS_PLATFORM === 'app-ios') {
|
||||
uniConsolePath = resolveBuiltIn(
|
||||
path.join('@dcloudio/uni-console', 'dist/app.esm.js')
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (process.env.UNI_PLATFORM === 'app-harmony') {
|
||||
uniConsolePath = resolveBuiltIn(
|
||||
path.join('@dcloudio/uni-console', 'dist/harmony.jsvm.esm.js')
|
||||
)
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: 'uni:console-main-js',
|
||||
enforce:
|
||||
// android需要提前,不然拿到的code是解析后的仅保留import语句的
|
||||
process.env.UNI_UTS_PLATFORM === 'app-android' ? 'pre' : 'post',
|
||||
transform(code: string, id: string) {
|
||||
if (!hasRuntimeSocket) {
|
||||
return
|
||||
}
|
||||
if (!opts.filter(id)) {
|
||||
return
|
||||
}
|
||||
return {
|
||||
// 采用绝对路径引入,此时,tsc失效,代码里需要自己处理好各种类型问题
|
||||
code: `import '${normalizePath(uniConsolePath)}';${code}`,
|
||||
map: null,
|
||||
}
|
||||
},
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { initRuntimeSocket } from './socket'
|
||||
|
||||
export function initRuntimeSocketService(): Promise<boolean> {
|
||||
const hosts: string = process.env.UNI_SOCKET_HOSTS
|
||||
const port: string = process.env.UNI_SOCKET_PORT
|
||||
const id: string = process.env.UNI_SOCKET_ID
|
||||
if (hosts == '' || port == '' || id == '') return Promise.resolve(false)
|
||||
let socketTask: SocketTask | null = null
|
||||
__registerWebViewUniConsole(
|
||||
(): string => {
|
||||
return process.env.UNI_CONSOLE_WEBVIEW_EVAL_JS_CODE
|
||||
},
|
||||
(data: string) => {
|
||||
socketTask?.send({
|
||||
data,
|
||||
} as SendSocketMessageOptions)
|
||||
}
|
||||
)
|
||||
return Promise.resolve()
|
||||
.then((): Promise<boolean> => {
|
||||
return initRuntimeSocket(hosts, port, id).then((socket): boolean => {
|
||||
if (socket == null) {
|
||||
return false
|
||||
}
|
||||
socketTask = socket
|
||||
return true
|
||||
})
|
||||
})
|
||||
.catch((): boolean => {
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
initRuntimeSocketService()
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/// <reference types="@dcloudio/uni-app-x/types/uni/global" />
|
||||
// 之所以又写了一份,是因为外层的socket,connectSocket的时候必须传入multiple:true
|
||||
// 但是android又不能传入,目前代码里又不能写条件编译之类的。
|
||||
export function initRuntimeSocket(
|
||||
hosts: string,
|
||||
port: string,
|
||||
id: string
|
||||
): Promise<SocketTask | null> {
|
||||
if (hosts == '' || port == '' || id == '') return Promise.resolve(null)
|
||||
return hosts
|
||||
.split(',')
|
||||
.reduce<Promise<SocketTask | null>>(
|
||||
(
|
||||
promise: Promise<SocketTask | null>,
|
||||
host: string
|
||||
): Promise<SocketTask | null> => {
|
||||
return promise.then((socket): Promise<SocketTask | null> => {
|
||||
if (socket != null) return Promise.resolve(socket)
|
||||
return tryConnectSocket(host, port, id)
|
||||
})
|
||||
},
|
||||
Promise.resolve(null)
|
||||
)
|
||||
}
|
||||
|
||||
const SOCKET_TIMEOUT = 500
|
||||
function tryConnectSocket(
|
||||
host: string,
|
||||
port: string,
|
||||
id: string
|
||||
): Promise<SocketTask | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = uni.connectSocket({
|
||||
url: `ws://${host}:${port}/${id}`,
|
||||
fail() {
|
||||
resolve(null)
|
||||
},
|
||||
})
|
||||
const timer = setTimeout(() => {
|
||||
// @ts-expect-error
|
||||
socket.close({
|
||||
code: 1006,
|
||||
reason: 'connect timeout',
|
||||
} as CloseSocketOptions)
|
||||
resolve(null)
|
||||
}, SOCKET_TIMEOUT)
|
||||
|
||||
socket.onOpen((e) => {
|
||||
clearTimeout(timer)
|
||||
resolve(socket)
|
||||
})
|
||||
socket.onClose((e) => {
|
||||
clearTimeout(timer)
|
||||
resolve(null)
|
||||
})
|
||||
socket.onError((e) => {
|
||||
clearTimeout(timer)
|
||||
resolve(null)
|
||||
})
|
||||
})
|
||||
}
|
||||
+426
@@ -0,0 +1,426 @@
|
||||
import type { ComponentInternalInstance, ComponentPublicInstance } from 'vue'
|
||||
import type { MessageType } from './utils'
|
||||
|
||||
interface NormalizeResult {
|
||||
name?: string
|
||||
type: string
|
||||
subType?: string
|
||||
className?: string
|
||||
description?: string
|
||||
value?: any
|
||||
}
|
||||
|
||||
interface ObjectResultValue {
|
||||
properties: Array<NormalizeResult>
|
||||
}
|
||||
interface ObjectResult extends NormalizeResult {
|
||||
value: ObjectResultValue
|
||||
}
|
||||
interface ArrayResultValue {
|
||||
properties: Array<NormalizeResult>
|
||||
}
|
||||
interface ArrayResult extends NormalizeResult {
|
||||
value: ArrayResultValue
|
||||
}
|
||||
interface SetResultEntry {
|
||||
value: NormalizeResult
|
||||
}
|
||||
interface SetResultValue {
|
||||
entries: Array<SetResultEntry>
|
||||
}
|
||||
interface SetResult extends NormalizeResult {
|
||||
value: SetResultValue
|
||||
}
|
||||
interface MapResultEntry {
|
||||
key: NormalizeResult
|
||||
value: NormalizeResult
|
||||
}
|
||||
interface MapResultValue {
|
||||
entries: Array<MapResultEntry>
|
||||
}
|
||||
interface MapResult extends NormalizeResult {
|
||||
value: MapResultValue
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
type: MessageType
|
||||
args: Array<any>
|
||||
}
|
||||
|
||||
export function formatMessage(
|
||||
type: MessageType,
|
||||
args: Array<any | null>
|
||||
): Message {
|
||||
try {
|
||||
return {
|
||||
type,
|
||||
args: formatArgs(args),
|
||||
}
|
||||
} catch (e) {
|
||||
// originalConsole.error(e)
|
||||
}
|
||||
return {
|
||||
type,
|
||||
args: [],
|
||||
}
|
||||
}
|
||||
|
||||
export function formatArgs(args: Array<any | null>) {
|
||||
return args.map((arg) => formatArg(arg))
|
||||
}
|
||||
|
||||
export function formatArg(arg: any | null, depth: number = 0): NormalizeResult {
|
||||
if (depth >= 7) {
|
||||
return {
|
||||
type: 'object',
|
||||
value: '[Maximum depth reached]',
|
||||
}
|
||||
}
|
||||
const type = typeof arg
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return formatString(arg as string)
|
||||
case 'number':
|
||||
return formatNumber(arg as number)
|
||||
case 'boolean':
|
||||
return formatBoolean(arg as boolean)
|
||||
case 'object':
|
||||
try {
|
||||
// 鸿蒙里边 object 可能包含 nativePtr 指针,该指针 typeof 是 object
|
||||
// 但是又不能访问上边的任意属性,否则会报:TypeError: Can not get Prototype on non ECMA Object
|
||||
// 所以这里需要捕获异常,防止报错
|
||||
return formatObject(arg as object, depth)
|
||||
} catch (e) {
|
||||
return {
|
||||
type: 'object',
|
||||
value: {
|
||||
properties: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
case 'undefined':
|
||||
return formatUndefined()
|
||||
case 'function':
|
||||
return formatFunction(arg as Function)
|
||||
case 'symbol':
|
||||
if (__HARMONY__) {
|
||||
return formatUnknown('symbol', arg as unknown)
|
||||
} else {
|
||||
return formatSymbol(arg as symbol)
|
||||
}
|
||||
|
||||
case 'bigint':
|
||||
return formatBigInt(arg as unknown)
|
||||
}
|
||||
}
|
||||
|
||||
function formatFunction(value: Function): NormalizeResult {
|
||||
return {
|
||||
type: 'function',
|
||||
value: `function ${value.name}() {}`,
|
||||
}
|
||||
}
|
||||
|
||||
function formatUndefined(): NormalizeResult {
|
||||
return {
|
||||
type: 'undefined',
|
||||
}
|
||||
}
|
||||
|
||||
function formatBoolean(value: boolean): NormalizeResult {
|
||||
return {
|
||||
type: 'boolean',
|
||||
value: String(value),
|
||||
}
|
||||
}
|
||||
|
||||
function formatNumber(value: number): NormalizeResult {
|
||||
return {
|
||||
type: 'number',
|
||||
value: String(value),
|
||||
}
|
||||
}
|
||||
|
||||
function formatBigInt(value: unknown): NormalizeResult {
|
||||
return {
|
||||
type: 'bigint',
|
||||
value: String(value),
|
||||
}
|
||||
}
|
||||
|
||||
function formatString(value: string): NormalizeResult {
|
||||
return {
|
||||
type: 'string',
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
function formatSymbol(value: symbol): NormalizeResult {
|
||||
return {
|
||||
type: 'symbol',
|
||||
value: value.description,
|
||||
}
|
||||
}
|
||||
|
||||
function formatUnknown(type: string, value: unknown): NormalizeResult {
|
||||
return {
|
||||
type,
|
||||
value: String(value),
|
||||
}
|
||||
}
|
||||
|
||||
function formatObject(value: object, depth: number): NormalizeResult {
|
||||
if (value === null) {
|
||||
return {
|
||||
type: 'null',
|
||||
}
|
||||
}
|
||||
|
||||
if (!__HARMONY__) {
|
||||
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: any | null, i: number): NormalizeResult =>
|
||||
formatArrayElement(v, i, depth + 1)
|
||||
),
|
||||
},
|
||||
} as ArrayResult
|
||||
}
|
||||
if (value instanceof Set) {
|
||||
return {
|
||||
type: 'object',
|
||||
subType: 'set',
|
||||
className: 'Set',
|
||||
description: `Set(${value.size})`,
|
||||
value: {
|
||||
entries: Array.from(value).map(
|
||||
(v: any | null): SetResultEntry => formatSetEntry(v, depth + 1)
|
||||
),
|
||||
},
|
||||
} as SetResult
|
||||
}
|
||||
if (value instanceof Map) {
|
||||
return {
|
||||
type: 'object',
|
||||
subType: 'map',
|
||||
className: 'Map',
|
||||
description: `Map(${value.size})`,
|
||||
value: {
|
||||
entries: Array.from(value.entries()).map(
|
||||
(v: Array<any | null>): MapResultEntry => formatMapEntry(v, depth + 1)
|
||||
),
|
||||
},
|
||||
} as MapResult
|
||||
}
|
||||
|
||||
if (value instanceof Promise) {
|
||||
return {
|
||||
type: 'object',
|
||||
subType: 'promise',
|
||||
value: {
|
||||
properties: [],
|
||||
},
|
||||
} as ObjectResult
|
||||
}
|
||||
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',
|
||||
}
|
||||
}
|
||||
let className: string | undefined = undefined
|
||||
if (!__HARMONY__) {
|
||||
const constructor = value.constructor
|
||||
if (constructor) {
|
||||
// @ts-expect-error
|
||||
if (constructor.get$UTSMetadata$) {
|
||||
// @ts-expect-error
|
||||
className = constructor.get$UTSMetadata$().name
|
||||
}
|
||||
}
|
||||
}
|
||||
let entries = Object.entries(value)
|
||||
if (isHarmonyBuilderParams(value)) {
|
||||
entries = entries.filter(
|
||||
([key]) => key !== 'modifier' && key !== 'nodeContent'
|
||||
)
|
||||
}
|
||||
return {
|
||||
type: 'object',
|
||||
className,
|
||||
value: {
|
||||
properties: entries.map(
|
||||
(entry: [string, any | null]): NormalizeResult =>
|
||||
formatObjectProperty(entry[0], entry[1], depth + 1)
|
||||
),
|
||||
},
|
||||
} as ObjectResult
|
||||
}
|
||||
|
||||
function isHarmonyBuilderParams(value: any) {
|
||||
return value.modifier && value.modifier._attribute && value.nodeContent
|
||||
}
|
||||
|
||||
function isComponentPublicInstance(
|
||||
value: any
|
||||
): value is ComponentPublicInstance {
|
||||
return value.$ && isComponentInternalInstance(value.$)
|
||||
}
|
||||
|
||||
function isComponentInternalInstance(
|
||||
value: any
|
||||
): value is ComponentInternalInstance {
|
||||
return value.type && value.uid != null && value.appContext
|
||||
}
|
||||
|
||||
function formatComponentPublicInstance(
|
||||
value: ComponentPublicInstance,
|
||||
depth: number
|
||||
) {
|
||||
return {
|
||||
type: 'object',
|
||||
className: 'ComponentPublicInstance',
|
||||
value: {
|
||||
properties: Object.entries(value.$.type).map(
|
||||
([name, value]): NormalizeResult =>
|
||||
formatObjectProperty(name, value, depth + 1)
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function formatComponentInternalInstance(
|
||||
value: ComponentInternalInstance,
|
||||
depth: number
|
||||
) {
|
||||
return {
|
||||
type: 'object',
|
||||
className: 'ComponentInternalInstance',
|
||||
value: {
|
||||
properties: Object.entries(value.type).map(
|
||||
([name, value]): NormalizeResult =>
|
||||
formatObjectProperty(name, value, depth + 1)
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function isUniElement(value: any): value is UniElement {
|
||||
return value.style && value.tagName != null && value.nodeName != null
|
||||
}
|
||||
|
||||
function formatUniElement(value: UniElement, depth: number) {
|
||||
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]): NormalizeResult =>
|
||||
formatObjectProperty(name, value, depth + 1)
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function isCSSStyleDeclaration(
|
||||
value: any
|
||||
): value is CSSStyleDeclaration & { $styles: Record<string, string | null> } {
|
||||
return (
|
||||
typeof value.getPropertyValue === 'function' &&
|
||||
typeof value.setProperty === 'function' &&
|
||||
value.$styles
|
||||
)
|
||||
}
|
||||
|
||||
function formatCSSStyleDeclaration(
|
||||
style: CSSStyleDeclaration & { $styles: Record<string, string | null> },
|
||||
depth: number
|
||||
) {
|
||||
return {
|
||||
type: 'object',
|
||||
value: {
|
||||
properties: Object.entries(style.$styles).map(([name, value]) =>
|
||||
formatObjectProperty(name, value, depth + 1)
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function formatObjectProperty(name: string, value: any | null, depth: number) {
|
||||
const result = formatArg(value, depth)
|
||||
result.name = name
|
||||
return result
|
||||
}
|
||||
|
||||
function formatArrayElement(value: any | null, index: number, depth: number) {
|
||||
const result = formatArg(value, depth)
|
||||
result.name = `${index}`
|
||||
return result
|
||||
}
|
||||
|
||||
function formatSetEntry(value: any | null, depth: number): SetResultEntry {
|
||||
return {
|
||||
value: formatArg(value, depth),
|
||||
}
|
||||
}
|
||||
|
||||
function formatMapEntry(
|
||||
value: Array<any | null>,
|
||||
depth: number
|
||||
): MapResultEntry {
|
||||
return {
|
||||
key: formatArg(value[0], depth),
|
||||
value: formatArg(value[1], depth),
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
import { sendErrorMessages } from '../error'
|
||||
import type { SendFn } from '../utils'
|
||||
import { type Message, formatMessage } from './format'
|
||||
import { CONSOLE_TYPES, type MessageType, originalConsole } from './utils'
|
||||
|
||||
let sendConsole: SendFn = null
|
||||
|
||||
const messageQueue: Message[] = []
|
||||
|
||||
const messageExtra: Record<string, any> = {}
|
||||
|
||||
const EXCEPTION_BEGIN_MARK = '---BEGIN:EXCEPTION---'
|
||||
const EXCEPTION_END_MARK = '---END:EXCEPTION---'
|
||||
|
||||
function sendConsoleMessages(messages: Message[]) {
|
||||
if (sendConsole == null) {
|
||||
messageQueue.push(...messages)
|
||||
return
|
||||
}
|
||||
sendConsole(
|
||||
JSON.stringify(
|
||||
Object.assign(
|
||||
{
|
||||
type: 'console',
|
||||
data: messages,
|
||||
},
|
||||
messageExtra
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function setSendConsole(value: SendFn, extra: Record<string, any> = {}) {
|
||||
sendConsole = value
|
||||
Object.assign(messageExtra, extra)
|
||||
if (value != null && messageQueue.length > 0) {
|
||||
const messages = messageQueue.slice()
|
||||
messageQueue.length = 0
|
||||
sendConsoleMessages(messages)
|
||||
}
|
||||
}
|
||||
|
||||
const atFileRegex = /^\s*at\s+[\w/./-]+:\d+$/
|
||||
|
||||
export function rewriteConsole() {
|
||||
if (__HARMONY_JSVM__) {
|
||||
if (
|
||||
typeof UTSProxyObject === 'object' &&
|
||||
UTSProxyObject !== null &&
|
||||
typeof UTSProxyObject.invokeSync === 'function'
|
||||
) {
|
||||
UTSProxyObject.invokeSync('__UniConsole', 'setSendConsoleMessages', [
|
||||
sendConsoleMessages,
|
||||
])
|
||||
}
|
||||
}
|
||||
function wrapConsole(type: MessageType) {
|
||||
return function (...args: any[]) {
|
||||
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 (process.env.UNI_CONSOLE_KEEP_ORIGINAL) {
|
||||
originalConsole[type](...originalArgs)
|
||||
}
|
||||
if (type === 'error' && args.length === 1) {
|
||||
const arg = args[0]
|
||||
if (typeof arg === 'string' && arg.startsWith(EXCEPTION_BEGIN_MARK)) {
|
||||
const startIndex = EXCEPTION_BEGIN_MARK.length
|
||||
const endIndex = arg.length - EXCEPTION_END_MARK.length
|
||||
sendErrorMessages([arg.slice(startIndex, endIndex)])
|
||||
return
|
||||
} else if (arg instanceof Error) {
|
||||
sendErrorMessages([arg])
|
||||
return
|
||||
}
|
||||
}
|
||||
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 {
|
||||
if (!process.env.UNI_CONSOLE_WEBVIEW) {
|
||||
if (typeof uni !== 'undefined' && uni.__f__) {
|
||||
const oldLog = uni.__f__
|
||||
if (oldLog) {
|
||||
// 重写 uni.__f__ 方法,这样的话,仅能打印开发者代码里的日志,其他没有被重写为__f__的日志将无法打印(比如uni-app框架、小程序框架等)
|
||||
uni.__f__ = function (...args: any[]) {
|
||||
const [type, filename, ...rest] = args
|
||||
// 原始日志移除 filename
|
||||
oldLog(type, '', ...rest)
|
||||
sendConsoleMessages([formatMessage(type, [...rest, filename])])
|
||||
}
|
||||
return function restoreConsole() {
|
||||
uni.__f__ = oldLog
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return function restoreConsole() {
|
||||
if (__HARMONY_JSVM__) {
|
||||
if (
|
||||
typeof UTSProxyObject === 'object' &&
|
||||
UTSProxyObject !== null &&
|
||||
typeof UTSProxyObject.invokeSync === 'function'
|
||||
) {
|
||||
UTSProxyObject.invokeSync('__UniConsole', '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
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
export const CONSOLE_TYPES = ['log', 'warn', 'error', 'info', 'debug'] as const
|
||||
|
||||
export type MessageType = 'log' | 'warn' | 'error' | 'info' | 'debug'
|
||||
|
||||
export const originalConsole = /*@__PURE__*/ CONSOLE_TYPES.reduce(
|
||||
(methods, type) => {
|
||||
methods[type] = console[type].bind(console)
|
||||
return methods
|
||||
},
|
||||
{} as Record<MessageType, typeof console.log>
|
||||
)
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import { originalConsole } from './console/utils'
|
||||
import type { SendFn } from './utils'
|
||||
|
||||
let sendError: SendFn = 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: Set<any> = new Set()
|
||||
|
||||
const errorExtra: Record<string, any> = {}
|
||||
|
||||
export function sendErrorMessages(errors: any[]) {
|
||||
if (sendError == null) {
|
||||
errors.forEach((error) => {
|
||||
errorQueue.add(error)
|
||||
})
|
||||
return
|
||||
}
|
||||
const data = errors
|
||||
.map((err) => {
|
||||
if (typeof err === 'string') {
|
||||
return 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) {
|
||||
if (err.message && !err.stack.includes(err.message)) {
|
||||
return `${prefix}${err.message}
|
||||
${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)
|
||||
})
|
||||
.filter(Boolean)
|
||||
if (data.length > 0) {
|
||||
sendError(
|
||||
JSON.stringify(
|
||||
Object.assign(
|
||||
{
|
||||
type: 'error',
|
||||
data,
|
||||
},
|
||||
errorExtra
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function setSendError(value: SendFn, extra: Record<string, any> = {}) {
|
||||
sendError = value
|
||||
Object.assign(errorExtra, extra)
|
||||
if (value != null && errorQueue.size > 0) {
|
||||
const errors = Array.from(errorQueue)
|
||||
errorQueue.clear()
|
||||
sendErrorMessages(errors)
|
||||
}
|
||||
}
|
||||
|
||||
export function initOnError() {
|
||||
function onError(error: any) {
|
||||
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 (process.env.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { formatMessage } from './console/format'
|
||||
import type { MessageType } from './console/utils'
|
||||
|
||||
export function __f__(type: MessageType, filename: string, ...args: any[]) {
|
||||
const message = formatMessage(type, [...args, filename])
|
||||
return message
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { initRuntimeSocket } from './socket'
|
||||
import { rewriteConsole, setSendConsole } from './console'
|
||||
import { initOnError, setSendError } from './error'
|
||||
import { originalConsole } from './console/utils'
|
||||
|
||||
export function initRuntimeSocketService(): Promise<boolean> {
|
||||
const hosts: string = process.env.UNI_SOCKET_HOSTS
|
||||
const port: string = process.env.UNI_SOCKET_PORT
|
||||
const id: string = process.env.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(
|
||||
wrapError('开发模式下日志通道建立 socket 连接失败。')
|
||||
)
|
||||
// @ts-expect-error
|
||||
if (__PLATFORM__ === 'mp') {
|
||||
originalConsole.error(
|
||||
wrapError('小程序平台,请勾选不校验合法域名配置。')
|
||||
)
|
||||
}
|
||||
originalConsole.error(
|
||||
wrapError('如果是运行到真机,请确认手机与电脑处于同一网络。')
|
||||
)
|
||||
return false
|
||||
}
|
||||
// @ts-expect-error
|
||||
if (__PLATFORM__ === 'mp') {
|
||||
initMiniProgramGlobalFlag()
|
||||
}
|
||||
socket.onClose(() => {
|
||||
if (process.env.UNI_DEBUG) {
|
||||
originalConsole.log(
|
||||
`uni-app:[${Date.now()}][socket]`,
|
||||
'connect close and restore'
|
||||
)
|
||||
}
|
||||
// @ts-expect-error
|
||||
if (__PLATFORM__ === 'mp') {
|
||||
originalConsole.error(
|
||||
wrapError(
|
||||
'开发模式下日志通道 socket 连接关闭,请在 HBuilderX 中重新运行。'
|
||||
)
|
||||
)
|
||||
} else {
|
||||
originalConsole.error(
|
||||
wrapError(
|
||||
'手机端日志通道 socket 连接已断开,请重启基座应用或重新运行。'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
restoreError()
|
||||
restoreConsole()
|
||||
})
|
||||
setSendConsole((data: string) => {
|
||||
if (process.env.UNI_DEBUG) {
|
||||
originalConsole.log(`uni-app:[${Date.now()}][console]`, data)
|
||||
}
|
||||
socket!.send({
|
||||
data,
|
||||
})
|
||||
})
|
||||
setSendError((data: string) => {
|
||||
if (process.env.UNI_DEBUG) {
|
||||
originalConsole.log(`uni-app:[${Date.now()}][error]`, data)
|
||||
}
|
||||
socket!.send({
|
||||
data,
|
||||
})
|
||||
})
|
||||
return true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const ERROR_CHAR = '\u200C'
|
||||
|
||||
function wrapError(error: string) {
|
||||
return `${ERROR_CHAR}${error}${ERROR_CHAR}`
|
||||
}
|
||||
|
||||
function initMiniProgramGlobalFlag() {
|
||||
if (typeof wx !== 'undefined') {
|
||||
// @ts-expect-error
|
||||
wx.__uni_console__ = true
|
||||
// @ts-expect-error
|
||||
} else if (typeof my !== 'undefined') {
|
||||
// @ts-expect-error
|
||||
my.__uni_console__ = true
|
||||
} else if (typeof tt !== 'undefined') {
|
||||
tt.__uni_console__ = true
|
||||
} else if (typeof swan !== 'undefined') {
|
||||
swan.__uni_console__ = true
|
||||
} else if (typeof qq !== 'undefined') {
|
||||
qq.__uni_console__ = true
|
||||
} else if (typeof ks !== 'undefined') {
|
||||
ks.__uni_console__ = true
|
||||
} else if (typeof jd !== 'undefined') {
|
||||
jd.__uni_console__ = true
|
||||
} else if (typeof xhs !== 'undefined') {
|
||||
xhs.__uni_console__ = true
|
||||
} else if (typeof has !== 'undefined') {
|
||||
has.__uni_console__ = true
|
||||
} else if (typeof qa !== 'undefined') {
|
||||
qa.__uni_console__ = true
|
||||
}
|
||||
}
|
||||
|
||||
initRuntimeSocketService()
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/// <reference types="@dcloudio/uni-app-x/types/uni/global" />
|
||||
|
||||
export function initRuntimeSocket(
|
||||
hosts: string,
|
||||
port: string,
|
||||
id: string
|
||||
): Promise<SocketTask | null> {
|
||||
if (hosts == '' || port == '' || id == '') return Promise.resolve(null)
|
||||
return hosts
|
||||
.split(',')
|
||||
.reduce<Promise<SocketTask | null>>(
|
||||
(
|
||||
promise: Promise<SocketTask | null>,
|
||||
host: string
|
||||
): Promise<SocketTask | null> => {
|
||||
return promise.then((socket): Promise<SocketTask | null> => {
|
||||
if (socket != null) return Promise.resolve(socket)
|
||||
return tryConnectSocket(host, port, id)
|
||||
})
|
||||
},
|
||||
Promise.resolve(null)
|
||||
)
|
||||
}
|
||||
|
||||
const SOCKET_TIMEOUT = 500
|
||||
function tryConnectSocket(
|
||||
host: string,
|
||||
port: string,
|
||||
id: string
|
||||
): Promise<SocketTask | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = uni.connectSocket({
|
||||
url: `ws://${host}:${port}/${id}`,
|
||||
multiple: true, // 支付宝小程序 是否开启多实例
|
||||
fail() {
|
||||
resolve(null)
|
||||
},
|
||||
})
|
||||
const timer = setTimeout(() => {
|
||||
// @ts-expect-error
|
||||
socket.close({
|
||||
code: 1006,
|
||||
reason: 'connect timeout',
|
||||
} as CloseSocketOptions)
|
||||
resolve(null)
|
||||
}, SOCKET_TIMEOUT)
|
||||
|
||||
socket.onOpen((e) => {
|
||||
clearTimeout(timer)
|
||||
resolve(socket)
|
||||
})
|
||||
socket.onClose((e) => {
|
||||
clearTimeout(timer)
|
||||
resolve(null)
|
||||
})
|
||||
socket.onError((e) => {
|
||||
clearTimeout(timer)
|
||||
resolve(null)
|
||||
})
|
||||
})
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export type SendFn = ((msg: string) => void) | null
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { rewriteConsole, setSendConsole } from '../console'
|
||||
import { sendErrorMessages, setSendError } from '../error'
|
||||
declare global {
|
||||
interface Window {
|
||||
__UNI_CONSOLE_WEBVIEW__: boolean
|
||||
__UNI_PAGE_ROUTE__: string
|
||||
}
|
||||
}
|
||||
|
||||
function initUniWebviewRuntimeService() {
|
||||
if (window.__UNI_CONSOLE_WEBVIEW__) return
|
||||
window.__UNI_CONSOLE_WEBVIEW__ = true
|
||||
const channel = `[web-view]${
|
||||
window.__UNI_PAGE_ROUTE__ ? `[${window.__UNI_PAGE_ROUTE__}]` : ''
|
||||
}`
|
||||
rewriteConsole()
|
||||
setSendConsole(
|
||||
(data: string) => {
|
||||
sendToService(data)
|
||||
},
|
||||
{
|
||||
channel,
|
||||
}
|
||||
)
|
||||
setSendError(
|
||||
(data: string) => {
|
||||
sendToService(data)
|
||||
},
|
||||
{
|
||||
channel,
|
||||
}
|
||||
)
|
||||
// 监听同步错误
|
||||
window.addEventListener('error', (event) => {
|
||||
sendErrorMessages([event.error])
|
||||
})
|
||||
// 监听Promise未处理的异步错误
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
sendErrorMessages([event])
|
||||
})
|
||||
}
|
||||
|
||||
function sendToService(data: string) {
|
||||
// 发送数据到 service 层
|
||||
const serviceMessage = {
|
||||
type: 'WEB_INVOKE_APPSERVICE',
|
||||
args: {
|
||||
data: {
|
||||
name: 'console',
|
||||
arg: data,
|
||||
},
|
||||
},
|
||||
}
|
||||
// @ts-expect-error
|
||||
if (window.__uniapp_x_postMessageToService) {
|
||||
// @ts-expect-error
|
||||
return window.__uniapp_x_postMessageToService(serviceMessage)
|
||||
} else {
|
||||
// @ts-expect-error
|
||||
return window.__uniapp_x_.postMessageToService(
|
||||
JSON.stringify(serviceMessage)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
initUniWebviewRuntimeService()
|
||||
Reference in New Issue
Block a user