1
0
mirror of synced 2025-12-08 14:54:02 +08:00
Files
eruda/src/Console/Logger.js
2020-04-29 13:29:09 +08:00

726 lines
16 KiB
JavaScript

import Log from './Log'
import {
Emitter,
isNum,
isUndef,
perfNow,
isStr,
extend,
uniqId,
isRegExp,
isFn,
$,
Stack,
isEmpty,
contain,
copy,
each,
toArr,
keys,
last,
throttle,
raf,
xpath,
isHidden,
lowerCase,
dateFormat
} from '../lib/util'
import evalCss from '../lib/evalCss'
let id = 0
export default class Logger extends Emitter {
constructor($container) {
super()
this._style = evalCss(require('./Logger.scss'))
this._$container = $container
this._container = $container.get(0)
this._$el = $container.find('ul.eruda-logs')
this._el = this._$el.get(0)
this._$fakeEl = $container.find('ul.eruda-fake-logs')
this._fakeEl = this._$fakeEl.get(0)
this._$topSpace = $container.find('.eruda-top-space')
this._topSpace = this._$topSpace.get(0)
this._$bottomSpace = $container.find('.eruda-bottom-space')
this._bottomSpace = this._$bottomSpace.get(0)
this._topSpaceHeight = 0
this._bottomSpaceHeight = 0
this._logs = []
this._displayLogs = []
this._timer = {}
this._count = {}
this._lastLog = {}
this._filter = 'all'
this._maxNum = 'infinite'
this._displayHeader = false
this._asyncRender = false
this._asyncList = []
this._asyncTimer = null
this._isAtBottom = true
this._groupStack = new Stack()
this.renderViewport = throttle(() => {
this._renderViewport()
}, 16)
// https://developers.google.cn/web/tools/chrome-devtools/console/utilities
this._global = {
copy(value) {
if (!isStr(value)) value = JSON.stringify(value, null, 2)
copy(value)
},
$() {
return document.querySelector.apply(document, arguments)
},
$$() {
return toArr(document.querySelectorAll.apply(document, arguments))
},
$x(path) {
return xpath(path)
},
clear: () => {
this.clear()
},
dir: value => {
this.dir(value)
},
table: (data, columns) => {
this.table(data, columns)
},
keys
}
this._bindEvent()
}
renderAsync(flag) {
this._asyncRender = flag
}
setGlobal(name, val) {
this._global[name] = val
}
displayHeader(flag) {
this._displayHeader = flag
}
maxNum(val) {
const logs = this._logs
this._maxNum = val
if (isNum(val) && logs.length > val) {
this._logs = logs.slice(logs.length - val)
this.render()
}
}
displayUnenumerable(flag) {
Log.showUnenumerable = flag
}
displayGetterVal(flag) {
Log.showGetterVal = flag
}
lazyEvaluation(flag) {
Log.lazyEvaluation = flag
}
viewLogInSources(flag) {
Log.showSrcInSources = flag
}
destroy() {
if (this._style) {
evalCss.remove(this._style)
}
}
filter(val) {
this._filter = val
this.emit('filter', val)
return this.render()
}
count(label = 'default') {
const count = this._count
!isUndef(count[label]) ? count[label]++ : (count[label] = 1)
return this.info(`${label}: ${count[label]}`)
}
countReset(label = 'default') {
this._count[label] = 0
return this
}
assert(...args) {
if (isEmpty(args)) return
const exp = args.shift()
if (!exp) {
if (args.length === 0) args.unshift('console.assert')
args.unshift('Assertion failed: ')
return this.insert('error', args)
}
}
log(...args) {
if (isEmpty(args)) return
return this.insert('log', args)
}
debug(...args) {
if (isEmpty(args)) return
return this.insert('debug', args)
}
dir(obj) {
if (isUndef(obj)) return
return this.insert('dir', [obj])
}
table(...args) {
if (isEmpty(args)) return
return this.insert('table', args)
}
time(name = 'default') {
if (this._timer[name]) {
return this.insert('warn', [`Timer '${name}' already exists`])
}
this._timer[name] = perfNow()
return this
}
timeLog(name = 'default') {
const startTime = this._timer[name]
if (!startTime) {
return this.insert('warn', [`Timer '${name}' does not exist`])
}
return this.info(`${name}: ${perfNow() - startTime}ms`)
}
timeEnd(name = 'default') {
this.timeLog(name)
delete this._timer[name]
return this
}
clear() {
this.silentClear()
return this.insert('log', [
'%cConsole was cleared',
'color:#808080;font-style:italic;'
])
}
silentClear() {
this._logs = []
this._displayLogs = []
this._lastLog = {}
this._count = {}
this._timer = {}
this._groupStack = new Stack()
this._asyncList = []
if (this._asyncTimer) {
clearTimeout(this._asyncTimer)
this._asyncTimer = null
}
return this.render()
}
info(...args) {
if (isEmpty(args)) return
return this.insert('info', args)
}
error(...args) {
if (isEmpty(args)) return
return this.insert('error', args)
}
warn(...args) {
if (isEmpty(args)) return
return this.insert('warn', args)
}
group(...args) {
return this.insert({
type: 'group',
args,
ignoreFilter: true
})
}
groupCollapsed(...args) {
return this.insert({
type: 'groupCollapsed',
args,
ignoreFilter: true
})
}
groupEnd() {
this.insert('groupEnd')
}
input(jsCode) {
this.insert({
type: 'input',
args: [jsCode],
ignoreFilter: true
})
try {
this.output(this._evalJs(jsCode))
} catch (e) {
this.insert({
type: 'error',
ignoreFilter: true,
args: [e]
})
}
return this
}
output(val) {
return this.insert({
type: 'output',
args: [val],
ignoreFilter: true
})
}
html(...args) {
return this.insert('html', args)
}
render() {
const logs = this._logs
this._$el.html('')
this._isAtBottom = true
this._updateBottomSpace(0)
this._updateTopSpace(0)
this._displayLogs = []
for (let i = 0, len = logs.length; i < len; i++) {
this._attachLog(logs[i])
}
return this
}
insert(type, args) {
let headers
if (this._displayHeader) {
headers = {
time: getCurTime(),
from: getFrom()
}
}
this._asyncRender
? this.insertAsync(type, args, headers)
: this.insertSync(type, args, headers)
}
insertAsync(type, args, headers) {
this._asyncList.push([type, args, headers])
this._handleAsyncList()
}
insertSync(type, args, headers) {
const logs = this._logs
const groupStack = this._groupStack
// Because asynchronous rendering, groupEnd must be put here.
if (type === 'groupEnd') {
const lastLog = this._lastLog
lastLog.groupEnd()
this._groupStack.pop()
return this
}
const options = isStr(type) ? { type, args } : type
if (groupStack.size > 0) {
options.group = groupStack.peek()
}
extend(options, {
id: ++id,
headers
})
if (options.type === 'group' || options.type === 'groupCollapsed') {
const group = {
id: uniqId('group'),
collapsed: false,
parent: groupStack.peek(),
indentLevel: groupStack.size + 1
}
if (options.type === 'groupCollapsed') group.collapsed = true
options.targetGroup = group
groupStack.push(group)
}
let log = new Log(options)
log.on('updateSize', () => {
this._isAtBottom = false
this.renderViewport()
})
const lastLog = this._lastLog
if (
!contain(['html', 'group', 'groupCollapsed'], log.type) &&
lastLog.type === log.type &&
!log.src &&
!log.args &&
lastLog.text() === log.text()
) {
lastLog.addCount()
if (log.time) lastLog.updateTime(log.time)
log = lastLog
this._detachLog(lastLog)
} else {
logs.push(log)
this._lastLog = log
}
if (this._maxNum !== 'infinite' && logs.length > this._maxNum) {
const firstLog = logs[0]
this._detachLog(firstLog)
logs.shift()
}
this._attachLog(log)
this.emit('insert', log)
return this
}
toggleGroup(log) {
const { targetGroup } = log
targetGroup.collapsed ? this._openGroup(log) : this._collapseGroup(log)
}
_updateTopSpace(height) {
this._topSpaceHeight = height
this._topSpace.style.height = height + 'px'
}
_updateBottomSpace(height) {
this._bottomSpaceHeight = height
this._bottomSpace.style.height = height + 'px'
}
_updateLogSize(log) {
const fakeEl = this._fakeEl
if (isHidden(this._fakeEl)) return
if (!log.isAttached()) {
fakeEl.appendChild(log.el)
log.updateSize()
if (fakeEl.children > 100) {
fakeEl.innerHTML = ''
}
return
}
log.updateSize()
}
_detachLog(log) {
const displayLogs = this._displayLogs
const idx = displayLogs.indexOf(log)
if (idx > -1) {
displayLogs.splice(idx, 1)
this.renderViewport()
}
}
// Binary search
_attachLog(log) {
if (!this._filterLog(log) || log.collapsed) return
const displayLogs = this._displayLogs
if (displayLogs.length === 0) {
displayLogs.push(log)
this.renderViewport()
return
}
const lastDisplayLog = last(displayLogs)
if (log.id > lastDisplayLog.id) {
displayLogs.push(log)
this.renderViewport()
return
}
let startIdx = 0
let endIdx = displayLogs.length - 1
let middleLog
let middleIdx
while (startIdx <= endIdx) {
middleIdx = startIdx + Math.floor((endIdx - startIdx) / 2)
middleLog = displayLogs[middleIdx]
if (middleLog.id === log.id) {
return
}
if (middleLog.id < log.id) {
startIdx = middleIdx + 1
} else {
endIdx = middleIdx - 1
}
}
if (middleLog.id < log.id) {
displayLogs.splice(middleIdx + 1, 0, log)
} else {
displayLogs.splice(middleIdx, 0, log)
}
this.renderViewport()
}
_handleAsyncList(timeout = 20) {
const asyncList = this._asyncList
if (this._asyncTimer) return
this._asyncTimer = setTimeout(() => {
this._asyncTimer = null
let done = false
const len = asyncList.length
// insert faster if logs is huge, thus takes more time to render.
let timeout, num
if (len < 1000) {
num = 200
timeout = 400
} else if (len < 5000) {
num = 500
timeout = 800
} else if (len < 10000) {
num = 800
timeout = 1000
} else if (len < 25000) {
num = 1000
timeout = 1200
} else if (len < 50000) {
num = 1500
timeout = 1500
} else {
num = 2000
timeout = 2500
}
if (num > len) {
num = len
done = true
}
for (let i = 0; i < num; i++) {
const [type, args, headers] = asyncList.shift()
this.insertSync(type, args, headers)
}
if (!done) raf(() => this._handleAsyncList(timeout))
}, timeout)
}
_injectGlobal() {
each(this._global, (val, name) => {
if (window[name]) return
window[name] = val
})
}
_clearGlobal() {
each(this._global, (val, name) => {
if (window[name] && window[name] === val) {
delete window[name]
}
})
}
_evalJs(jsInput) {
let ret
this._injectGlobal()
try {
ret = eval.call(window, `(${jsInput})`)
} catch (e) {
ret = eval.call(window, jsInput)
}
this.setGlobal('$_', ret)
this._clearGlobal()
return ret
}
_filterLog(log) {
const filter = this._filter
if (filter === 'all') return true
const isFilterRegExp = isRegExp(filter)
const isFilterFn = isFn(filter)
if (log.ignoreFilter) return true
if (isFilterFn) return filter(log)
if (isFilterRegExp) return filter.test(lowerCase(log.text()))
return log.type === filter
}
_getLog(id) {
const logs = this._logs
let log
for (let i = 0, len = logs.length; i < len; i++) {
log = logs[i]
if (log.id === id) break
}
return log
}
_collapseGroup(log) {
const { targetGroup } = log
targetGroup.collapsed = true
log.updateIcon('caret-right')
this._updateGroup(log)
}
_openGroup(log) {
const { targetGroup } = log
targetGroup.collapsed = false
log.updateIcon('caret-down')
this._updateGroup(log)
}
_updateGroup(log) {
const { targetGroup } = log
const logs = this._logs
const len = logs.length
let i = logs.indexOf(log) + 1
while (i < len) {
const log = logs[i]
if (!log.checkGroup() && log.group === targetGroup) {
break
}
log.collapsed ? this._detachLog(log) : this._attachLog(log)
i++
}
}
_bindEvent() {
const self = this
const $el = this._$el
$el
.on('click', '.eruda-log-container', function() {
this.log.click(self)
})
.on('click', '.eruda-icon-caret-down', function() {
const $el = $(this)
.parent()
.parent()
.parent()
self._collapseGroup($el.get(0).log)
})
.on('click', '.eruda-icon-caret-right', function() {
const $el = $(this)
.parent()
.parent()
.parent()
self._openGroup($el.get(0).log)
})
this._$container.on('scroll', () => {
const { scrollHeight, offsetHeight, scrollTop } = this._container
// safari bounce effect
if (scrollTop < 0) return
if (offsetHeight + scrollTop > scrollHeight) return
let isAtBottom = false
if (scrollHeight === offsetHeight) {
isAtBottom = true
} else if (scrollTop === scrollHeight - offsetHeight) {
isAtBottom = true
}
this._isAtBottom = isAtBottom
if (
this._topSpaceHeight < scrollTop &&
this._topSpaceHeight + this._el.offsetHeight > scrollTop + offsetHeight
) {
return
}
this.renderViewport()
})
}
_renderViewport() {
const container = this._container
if (isHidden(container)) return
const { scrollTop, clientWidth, offsetHeight } = container
let top = scrollTop
let bottom = scrollTop + offsetHeight
const displayLogs = this._displayLogs
const tolerance = 1000
top -= tolerance
bottom += tolerance
let topSpaceHeight = 0
let bottomSpaceHeight = 0
let currentHeight = 0
this._el.innerHTML = ''
const len = displayLogs.length
const fakeEl = this._fakeEl
const fakeFrag = document.createDocumentFragment()
const logs = []
for (let i = 0; i < len; i++) {
const log = displayLogs[i]
const { width, height } = log
if (height === 0 || width !== clientWidth) {
fakeFrag.appendChild(log.el)
logs.push(log)
}
}
if (logs.length > 0) {
fakeEl.appendChild(fakeFrag)
for (let i = 0, len = logs.length; i < len; i++) {
logs[i].updateSize()
}
fakeEl.innerHTML = ''
}
const frag = document.createDocumentFragment()
for (let i = 0; i < len; i++) {
const log = displayLogs[i]
const { el, height } = log
if (currentHeight > bottom) {
bottomSpaceHeight += height
} else if (currentHeight + height > top) {
frag.appendChild(el)
} else if (currentHeight < top) {
topSpaceHeight += height
}
currentHeight += height
}
this._el.appendChild(frag)
this._updateTopSpace(topSpaceHeight)
this._updateBottomSpace(bottomSpaceHeight)
const { scrollHeight } = container
if (this._isAtBottom && scrollTop !== scrollHeight - offsetHeight) {
container.scrollTop = scrollHeight - offsetHeight
this.renderViewport()
} else {
container.scrollTop = scrollTop
}
}
}
const getCurTime = () => dateFormat('HH:MM:ss ')
function getFrom() {
const e = new Error()
let ret = ''
const lines = e.stack ? e.stack.split('\n') : ''
for (let i = 0, len = lines.length; i < len; i++) {
ret = lines[i]
if (ret.indexOf('winConsole') > -1 && i < len - 1) {
ret = lines[i + 1]
break
}
}
return ret
}