feat: amis Debug 辅助工具 (#3370)

* feat: debug 草稿

* 补充文档及增加 inspect 功能

* 增加 data 查看

* 增加 log 分类

* 增加 resize 功能

* 修复报错

* 去掉无用依赖

* 避免报错

Co-authored-by: wuduoyi <nwind@iMac-Pro.local>
This commit is contained in:
吴多益 2022-01-12 13:04:05 +08:00 committed by GitHub
parent 7a8b11dcb0
commit afaa9384d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 658 additions and 3 deletions

View File

@ -0,0 +1,23 @@
---
title: Debug 工具
---
> 1.6.1 及以上版本
amis 内置了 Debug 功能,可以查看组件内部运行日志,方便分析问题,目前在文档右侧就有显示。
## 开启方法
默认不会开启这个功能,可以通过下面两种方式开启:
1. 配置全局变量 `enableAMISDebug` 的值为 `true`,比如 `window.enableAMISDebug = true`
2. 在页面 URL 参数中加上 `amisDebug=1`,比如 `http://xxx.com/?amisDebug=1`
开启之后,在页面右侧就会显示。
## 目前功能
目前 Debug 工具提供了两个功能:
1. 运行日志,主要是 api 及数据转换的日志
2. 查看组件数据链Debug 工具展开后,点击任意组件就能看到这个组件的数据链

View File

@ -103,6 +103,8 @@
})();
}
window.enableAMISDebug = true;
/* @require ./index.jsx 标记为同步依赖,提前加载 */
amis.require(['./index.jsx'], function (app) {
var initialState = {};

View File

@ -409,6 +409,7 @@ $zindex-contextmenu: 1500 !default;
$zindex-tooltip: 1600 !default;
$zindex-toast: 2000 !default;
$zindex-top: 3000 !default;
$zindex-debug: 4000 !default;
$Form--horizontal-columns: 12;
$Table-strip-bg: lighten(#f6f8f8, 1%) !default;

167
scss/components/_debug.scss Normal file
View File

@ -0,0 +1,167 @@
/**
* Debug 模块的 UI由于没法使用任何主题所以这里使用独立配色风格
*/
.AMISDebug {
position: fixed;
z-index: $zindex-debug;
top: 0;
right: 0;
height: 100vh;
width: 24px;
h3 {
color: inherit;
}
.primary {
color: #009fff;
}
&-header {
padding: var(--Drawer-header-padding);
background: var(--Drawer-header-bg);
border-bottom: var(--Drawer-content-borderWidth) solid
var(--Drawer-header-borderColor);
}
&-hoverBox {
pointer-events: none;
position: absolute;
outline: 1px dashed #1c76c4;
}
&-activeBox {
pointer-events: none;
position: absolute;
outline: 1px #1c76c4;
}
&-tab {
overflow: hidden;
}
&-tab > button {
color: inherit;
background: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
padding: var(--gap-sm) var(--gap-md);
transition: 0.3s;
border-bottom: 1px solid transparent;
}
&-tab > button:hover {
color: #e7e7e7;
}
&-tab > button.active {
color: #e7e7e7;
border-bottom-color: #e7e7e7;
}
&-toggle {
background: var(--body-bg);
position: fixed;
top: 50%;
right: 0;
width: 24px;
height: 48px;
box-shadow: rgba(0, 0, 0, 0.24) -2px 0px 4px 0px;
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
padding-top: 14px;
padding-left: 6px;
cursor: pointer;
i {
color: var(--text-color);
}
&:hover {
i {
color: var(--primary);
}
}
}
&-content {
display: none;
}
&-resize {
position: absolute;
width: 4px;
top: 0;
left: 0;
bottom: 0;
cursor: col-resize;
&:hover {
background: #75715e;
}
}
&-changePosition {
position: absolute;
font-size: 18px;
right: 40px;
top: var(--gap-sm);
cursor: pointer;
}
&-close {
position: absolute;
font-size: 18px;
right: var(--gap-sm);
top: var(--gap-sm);
cursor: pointer;
}
&.is-expanded {
width: 420px;
overflow: auto;
background: #272821;
color: #cccccc;
box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
.AMISDebug-toggle {
display: none;
}
.AMISDebug-content {
display: block;
}
}
&.is-left {
left: 0;
.AMISDebug-resize {
left: unset;
right: 0;
}
}
&-log {
padding: var(--gap-sm);
button {
cursor: pointer;
background: #0e639c;
flex-grow: 1;
box-sizing: border-box;
display: inline-flex;
justify-content: center;
align-items: center;
padding: 6px 11px;
outline: none;
text-decoration: none;
color: inherit;
max-width: 300px;
border: none;
}
button:hover {
background: #1177bb;
}
}
&-inspect {
padding: var(--gap-sm);
}
}

View File

@ -123,4 +123,6 @@
@import '../components/formula';
@import '../components/timeline';
@import '../components/debug';
@import '../utilities';

View File

@ -14,6 +14,7 @@ import {
import {asFormItem} from './renderers/Form/Item';
import {renderChild, renderChildren} from './Root';
import {Schema, SchemaNode} from './types';
import {DebugWrapper, enableAMISDebug} from './utils/debug';
import getExprProperties from './utils/filter-schema';
import {anyChanged, chainEvents, autobind} from './utils/helper';
import {RendererEvent} from './utils/renderer-event';
@ -370,7 +371,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
return null;
}
return (
const component = (
<BroadcastCmpt
{...theme.getRendererConfig(renderer.name)}
{...restSchema}
@ -387,6 +388,12 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
component={Component}
/>
);
return enableAMISDebug ? (
<DebugWrapper renderer={renderer}>{component}</DebugWrapper>
) : (
component
);
}
}

View File

@ -41,6 +41,8 @@ import {
import './locale/zh-CN';
import './utils/debug';
import animation from './utils/Animation';
export * from './Schema';

View File

@ -15,6 +15,7 @@ import {
uuid
} from './helper';
import isPlainObject from 'lodash/isPlainObject';
import {debug} from './debug';
const rSchema = /(?:^|raw\:)(get|post|put|delete|patch|options|head|jsonp):/i;
@ -265,7 +266,10 @@ export function responseAdaptor(ret: fetcherResult, api: ApiObject) {
payload.errors = data.errors;
}
debug('api', 'response', payload);
if (payload.ok && api.responseData) {
debug('api', 'before dataMapping', payload.data);
const responseData = dataMapping(
api.responseData,
@ -280,7 +284,7 @@ export function responseAdaptor(ret: fetcherResult, api: ApiObject) {
undefined,
api.convertKeyToPath
);
console.debug('responseData', responseData);
debug('api', 'after dataMapping', responseData);
payload.data = responseData;
}
@ -294,7 +298,11 @@ export function wrapFetcher(
return function (api, data, options) {
api = buildApi(api, data, options) as ApiObject;
api.requestAdaptor && (api = api.requestAdaptor(api) || api);
if (api.requestAdaptor) {
debug('api', 'before requestAdaptor', api);
api = api.requestAdaptor(api) || api;
debug('api', 'after requestAdaptor', api);
}
if (api.data && (hasFile(api.data) || api.dataType === 'form-data')) {
api.data =
@ -319,6 +327,8 @@ export function wrapFetcher(
api.headers['Content-Type'] = 'application/json';
}
debug('api', 'request api', api);
tracker?.(
{eventType: 'api', eventData: omit(api, ['config', 'data', 'body'])},
api.data
@ -355,12 +365,15 @@ export function wrapAdaptor(promise: Promise<fetcherResult>, api: ApiObject) {
return adaptor
? promise
.then(async response => {
debug('api', 'before adaptor data', (response as any).data);
let result = adaptor((response as any).data, response, api);
if (result?.then) {
result = await result;
}
debug('api', 'after adaptor data', result);
return {
...response,
data: result

438
src/utils/debug.tsx Normal file
View File

@ -0,0 +1,438 @@
/**
* amis amis
*/
import React, {Component, useEffect, useRef, useState} from 'react';
import cx from 'classnames';
import {findDOMNode, render} from 'react-dom';
import JsonView from 'react-json-view';
import {autorun, observable} from 'mobx';
import {observer} from 'mobx-react';
import {uuidv4} from './helper';
import position from './position';
class Log {
@observable cat = '';
@observable level = '';
@observable msg = '';
@observable ext? = '';
}
class AMISDebugStore {
/**
* tab
*/
@observable tab: 'log' | 'inspect' = 'log';
/**
*
*/
@observable position: 'left' | 'right' = 'right';
/**
*
*/
@observable logs: Log[] = [];
/**
* Debug
*/
@observable isExpanded = false;
/**
* inspect
*/
@observable inspectMode = false;
/**
* id
*/
@observable hoverId: string;
/**
* id
*/
@observable activeId: string;
}
const store = new AMISDebugStore();
interface ComponentInspect {
name: string;
component: any;
}
// 存储组件信息用于 debug
const ComponentInfo = {} as {[propName: string]: ComponentInspect};
const LogView = observer(({store}: {store: AMISDebugStore}) => {
const logs = store.logs;
return (
<>
{logs.map((log, index) => {
return (
<div className="AMISDebug-logLine" key={`log-${index}`}>
<div className="AMISDebug-logLineMsg">
[{log.cat}] {log.msg}
</div>
{log.ext ? (
<JsonView
name={null}
theme="monokai"
src={JSON.parse(log.ext)}
collapsed={true}
enableClipboard={false}
displayDataTypes={false}
iconStyle="square"
/>
) : null}
</div>
);
})}
</>
);
});
const AMISDebug = observer(({store}: {store: AMISDebugStore}) => {
const activeId = store.activeId;
const activeComponentInspect = ComponentInfo[activeId];
// 收集数据域里的数据
let start = activeComponentInspect?.component?.props?.data || {};
const stacks = [start];
while (Object.getPrototypeOf(start) !== Object.prototype) {
const superData = Object.getPrototypeOf(start);
if (Object.prototype.toString.call(superData) !== '[object Object]') {
break;
}
stacks.push(superData);
start = superData;
}
const stackDataView = [];
if (Object.keys(stacks[0]).length || stacks.length > 1) {
let level = 0;
for (const stack of stacks) {
stackDataView.push(
<div key={`data-${level}`}>
<h3>Data Level-{level}</h3>
<JsonView
key={`dataview-${stack}`}
name={null}
theme="monokai"
src={stack}
collapsed={level === 0 ? false : true}
enableClipboard={false}
displayDataTypes={false}
iconStyle="square"
/>
</div>
);
level += 1;
}
}
const panelRef = useRef(null);
const [isResizing, setResizing] = useState(false);
const [startX, setStartX] = useState(0);
const [panelWidth, setPanelWidth] = useState(0);
useEffect(() => {
const handleMouseUp = () => {
setResizing(false);
};
const handleMouseMove = (e: MouseEvent) => {
if (!isResizing) {
return;
}
const xOffset =
store.position === 'right' ? e.clientX - startX : startX - e.clientX;
const panel = panelRef.current! as HTMLElement;
const targetWidth = Math.max(200, panelWidth - xOffset);
panel.style.width = targetWidth + 'px';
if (e.stopPropagation) e.stopPropagation();
if (e.preventDefault) e.preventDefault();
e.cancelBubble = true;
return false;
};
if (isResizing) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
return () => {
if (isResizing) {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
}
};
}, [isResizing]);
return (
<div
className={cx('AMISDebug', {
'is-expanded': store.isExpanded,
'is-left': store.position === 'left'
})}
ref={panelRef}
>
<div
className="AMISDebug-toggle"
onClick={() => {
store.isExpanded = true;
}}
>
{store.isExpanded ? (
<i className="fas fa-times"></i>
) : (
<i className="fas fa-bug"></i>
)}
</div>
<div className={cx('AMISDebug-content')}>
<div
className="AMISDebug-close"
title="Close"
onClick={() => {
store.isExpanded = false;
}}
>
<i className="fas fa-times" />
</div>
<div
className="AMISDebug-resize"
onMouseDown={event => {
setStartX(event.clientX);
setPanelWidth(
parseInt(
getComputedStyle(panelRef.current!).getPropertyValue('width'),
10
)
);
setResizing(true);
}}
></div>
<div className="AMISDebug-tab">
<button
className={cx({active: store.tab === 'log'})}
onClick={() => {
store.tab = 'log';
}}
>
Log
</button>
<button
className={cx({active: store.tab === 'inspect'})}
onClick={() => {
store.tab = 'inspect';
}}
>
Inspect
</button>
</div>
<div className="AMISDebug-changePosition">
{store.position === 'right' ? (
<i
className="fas fa-chevron-left"
title="move to left"
onClick={() => {
store.position = 'left';
}}
/>
) : (
<i
className="fas fa-chevron-right"
title="move to right"
onClick={() => {
store.position = 'right';
}}
/>
)}
</div>
{store.tab === 'log' ? (
<div className="AMISDebug-log">
<button
onClick={() => {
store.logs = [];
}}
>
Clear Log
</button>
<LogView store={store} />
</div>
) : null}
{store.tab === 'inspect' ? (
<div className="AMISDebug-inspect">
{activeId ? (
<>
<h3>
Component:{' '}
<span className="primary">{activeComponentInspect.name}</span>
</h3>
{stackDataView}
</>
) : (
'Click component to display inspect'
)}
</div>
) : null}
</div>
</div>
);
});
export let enableAMISDebug = false;
// 开启 debug 有两种方法,一个是设置 enableAMISDebug 全局变量,另一个是通过 amisDebug=1 query
if (
(window as any).enableAMISDebug ||
location.search.indexOf('amisDebug=1') !== -1
) {
enableAMISDebug = true;
// 页面只有一个
if (!(window as any).amisDebugElement) {
const amisDebugElement = document.createElement('div');
document.body.appendChild(amisDebugElement);
const element = <AMISDebug store={store} />;
render(element, amisDebugElement);
(window as any).amisDebugElement = true;
}
}
/**
*
*/
function handleMouseMove(e: MouseEvent) {
if (!store.isExpanded) {
return;
}
const dom = e.target as HTMLElement;
const target = dom.closest(`[data-debug-id]`);
if (target) {
store.hoverId = target.getAttribute('data-debug-id')!;
}
}
/**
*
*/
function handleMouseclick(e: MouseEvent) {
if (!store.isExpanded) {
return;
}
const dom = e.target as HTMLElement;
const target = dom.closest(`[data-debug-id]`);
if (target) {
store.activeId = target.getAttribute('data-debug-id')!;
store.tab = 'inspect';
}
}
// hover 及点击后的高亮
const amisHoverBox = document.createElement('div');
amisHoverBox.className = 'AMISDebug-hoverBox';
const amisActiveBox = document.createElement('div');
amisActiveBox.className = 'AMISDebug-activeBox';
autorun(() => {
const hoverId = store.hoverId;
const hoverElement = document.querySelector(
`[data-debug-id="${hoverId}"]`
) as HTMLElement;
if (hoverElement) {
const offset = position(hoverElement, document.body);
amisHoverBox.style.top = `${offset.top}px`;
amisHoverBox.style.left = `${offset.left}px`;
amisHoverBox.style.width = `${offset.width}px`;
amisHoverBox.style.height = `${offset.height}px`;
}
});
autorun(() => {
const activeId = store.activeId;
const activeElement = document.querySelector(
`[data-debug-id="${activeId}"]`
) as HTMLElement;
if (activeElement) {
const offset = position(activeElement, document.body);
amisActiveBox.style.top = `${offset.top}px`;
amisActiveBox.style.left = `${offset.left}px`;
amisActiveBox.style.width = `${offset.width}px`;
amisActiveBox.style.height = `${offset.height}px`;
}
});
if (enableAMISDebug) {
document.body.appendChild(amisHoverBox);
document.body.appendChild(amisActiveBox);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('click', handleMouseclick);
}
interface DebugWrapperProps {
renderer: any;
}
export class DebugWrapper extends Component<DebugWrapperProps> {
componentDidMount() {
if (!enableAMISDebug) {
return;
}
const root = findDOMNode(this) as HTMLElement;
if (!root) {
return;
}
const {renderer} = this.props;
const debugId = uuidv4();
root.setAttribute('data-debug-id', debugId);
ComponentInfo[debugId] = {
name: renderer.name,
component: this.props.children
};
}
render() {
return this.props.children;
}
}
type Category = 'api' | 'event';
/**
*
* @param msg
* @param ext
*/
export function debug(cat: Category, msg: string, ext?: object) {
if (!enableAMISDebug) {
return;
}
const log = {
cat,
level: 'debug',
msg: msg,
ext: JSON.stringify(ext)
};
console.debug(log);
store.logs.push(log);
}
/**
*
* @param msg
* @param ext
*/
export function warning(cat: Category, msg: string, ext?: object) {
if (!enableAMISDebug) {
return;
}
store.logs.push({
cat,
level: 'warn',
msg: msg,
ext: JSON.stringify(ext)
});
}