mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-24 04:16:00 +08:00
RN: A wild YellowBox has appeared!
Summary: Replaces the existing `YellowBox` with a modern one. Here are the notable changes: - Sort warnings by recency (with most recent on top). - Group warnings by format string if present. - Present stack traces similar to RedBox. - Show status of loading source maps. - Support inspecting each occurrence of a warning. - Fixed a bunch of edge cases and race conditions. Reviewed By: TheSavior Differential Revision: D8345180 fbshipit-source-id: b9e10d526b262c3985bbea639ba2ea0e7cad5081
This commit is contained in:
committed by
Facebook Github Bot
parent
f8b4850425
commit
d0219a0301
@@ -127,4 +127,11 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
if (__DEV__) {
|
||||
if (!global.__RCTProfileIsProfiling) {
|
||||
const YellowBox = require('YellowBox');
|
||||
YellowBox.install();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AppContainer;
|
||||
|
||||
@@ -1,554 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('EventEmitter');
|
||||
const Platform = require('Platform');
|
||||
const React = require('React');
|
||||
const SafeAreaView = require('SafeAreaView');
|
||||
const StyleSheet = require('StyleSheet');
|
||||
const RCTLog = require('RCTLog');
|
||||
|
||||
const infoLog = require('infoLog');
|
||||
const openFileInEditor = require('openFileInEditor');
|
||||
const parseErrorStack = require('parseErrorStack');
|
||||
const stringifySafe = require('stringifySafe');
|
||||
const symbolicateStackTrace = require('symbolicateStackTrace');
|
||||
|
||||
import type EmitterSubscription from 'EmitterSubscription';
|
||||
import type {StackFrame} from 'parseErrorStack';
|
||||
|
||||
type WarningInfo = {
|
||||
count: number,
|
||||
stacktrace: Array<StackFrame>,
|
||||
symbolicated: boolean,
|
||||
};
|
||||
|
||||
const _warningEmitter = new EventEmitter();
|
||||
const _warningMap: Map<string, WarningInfo> = new Map();
|
||||
const IGNORED_WARNINGS: Array<string> = [];
|
||||
|
||||
/**
|
||||
* YellowBox renders warnings at the bottom of the app being developed.
|
||||
*
|
||||
* Warnings help guard against subtle yet significant issues that can impact the
|
||||
* quality of the app. This "in your face" style of warning allows developers to
|
||||
* notice and correct these issues as quickly as possible.
|
||||
*
|
||||
* By default, the warning box is enabled in `__DEV__`. Set the following flag
|
||||
* to disable it (and call `console.warn` to update any rendered <YellowBox>):
|
||||
*
|
||||
* console.disableYellowBox = true;
|
||||
* console.warn('YellowBox is disabled.');
|
||||
*
|
||||
* Ignore specific warnings by calling:
|
||||
*
|
||||
* YellowBox.ignoreWarnings(['Warning: ...']);
|
||||
*
|
||||
* (DEPRECATED) Warnings can be ignored programmatically by setting the array:
|
||||
*
|
||||
* console.ignoredYellowBox = ['Warning: ...'];
|
||||
*
|
||||
* Strings in `console.ignoredYellowBox` can be a prefix of the warning that
|
||||
* should be ignored.
|
||||
*/
|
||||
|
||||
if (__DEV__) {
|
||||
const {error, warn} = console;
|
||||
|
||||
(console: any).error = function() {
|
||||
error.apply(console, arguments);
|
||||
// Show yellow box for the `warning` module.
|
||||
if (
|
||||
typeof arguments[0] === 'string' &&
|
||||
arguments[0].startsWith('Warning: ')
|
||||
) {
|
||||
updateWarningMap.apply(null, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
(console: any).warn = function() {
|
||||
warn.apply(console, arguments);
|
||||
updateWarningMap.apply(null, arguments);
|
||||
};
|
||||
|
||||
if (Platform.isTesting) {
|
||||
(console: any).disableYellowBox = true;
|
||||
}
|
||||
|
||||
RCTLog.setWarningHandler((...args) => {
|
||||
updateWarningMap.apply(null, args);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple function for formatting strings.
|
||||
*
|
||||
* Replaces placeholders with values passed as extra arguments
|
||||
*
|
||||
* @param {string} format the base string
|
||||
* @param ...args the values to insert
|
||||
* @return {string} the replaced string
|
||||
*/
|
||||
function sprintf(format, ...args) {
|
||||
let index = 0;
|
||||
return format.replace(/%s/g, match => args[index++]);
|
||||
}
|
||||
|
||||
function updateWarningMap(...args): void {
|
||||
/* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an error
|
||||
* found when Flow v0.68 was deployed. To see the error delete this comment
|
||||
* and run Flow. */
|
||||
if (console.disableYellowBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
let warning;
|
||||
if (typeof args[0] === 'string') {
|
||||
const [format, ...formatArgs] = args;
|
||||
const argCount = (format.match(/%s/g) || []).length;
|
||||
warning = [
|
||||
sprintf(format, ...formatArgs.slice(0, argCount).map(stringifySafe)),
|
||||
...formatArgs.slice(argCount).map(stringifySafe),
|
||||
].join(' ');
|
||||
} else {
|
||||
warning = args.map(stringifySafe).join(' ');
|
||||
}
|
||||
|
||||
if (warning.startsWith('(ADVICE)')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const warningInfo = _warningMap.get(warning);
|
||||
if (warningInfo) {
|
||||
warningInfo.count += 1;
|
||||
} else {
|
||||
const error: any = new Error();
|
||||
error.framesToPop = 2;
|
||||
|
||||
_warningMap.set(warning, {
|
||||
count: 1,
|
||||
stacktrace: parseErrorStack(error),
|
||||
symbolicated: false,
|
||||
});
|
||||
}
|
||||
|
||||
_warningEmitter.emit('warning', _warningMap);
|
||||
}
|
||||
|
||||
function ensureSymbolicatedWarning(warning: string): void {
|
||||
const prevWarningInfo = _warningMap.get(warning);
|
||||
if (!prevWarningInfo || prevWarningInfo.symbolicated) {
|
||||
return;
|
||||
}
|
||||
prevWarningInfo.symbolicated = true;
|
||||
|
||||
symbolicateStackTrace(prevWarningInfo.stacktrace).then(
|
||||
stack => {
|
||||
const nextWarningInfo = _warningMap.get(warning);
|
||||
if (nextWarningInfo) {
|
||||
nextWarningInfo.stacktrace = stack;
|
||||
_warningEmitter.emit('warning', _warningMap);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
const nextWarningInfo = _warningMap.get(warning);
|
||||
if (nextWarningInfo) {
|
||||
infoLog('Failed to symbolicate warning, "%s":', warning, error);
|
||||
_warningEmitter.emit('warning', _warningMap);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function isWarningIgnored(warning: string): boolean {
|
||||
const isIgnored = IGNORED_WARNINGS.some((ignoredWarning: string) =>
|
||||
warning.includes(ignoredWarning),
|
||||
);
|
||||
|
||||
if (isIgnored) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// DEPRECATED
|
||||
return (
|
||||
/* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an
|
||||
* error found when Flow v0.68 was deployed. To see the error delete this
|
||||
* comment and run Flow. */
|
||||
Array.isArray(console.ignoredYellowBox) &&
|
||||
console.ignoredYellowBox.some(ignorePrefix =>
|
||||
warning.startsWith(String(ignorePrefix)),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const WarningRow = ({count, warning, onPress}) => {
|
||||
const Text = require('Text');
|
||||
const TouchableHighlight = require('TouchableHighlight');
|
||||
const View = require('View');
|
||||
|
||||
const countText =
|
||||
count > 1 ? (
|
||||
<Text style={styles.listRowCount}>{'(' + count + ') '}</Text>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<View style={styles.listRow}>
|
||||
<TouchableHighlight
|
||||
activeOpacity={0.5}
|
||||
onPress={onPress}
|
||||
style={styles.listRowContent}
|
||||
underlayColor="transparent">
|
||||
<Text style={styles.listRowText} numberOfLines={2}>
|
||||
{countText}
|
||||
{warning}
|
||||
</Text>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
type StackRowProps = {frame: StackFrame};
|
||||
const StackRow = ({frame}: StackRowProps) => {
|
||||
const Text = require('Text');
|
||||
const TouchableHighlight = require('TouchableHighlight');
|
||||
const {file, lineNumber} = frame;
|
||||
let fileName;
|
||||
if (file) {
|
||||
const fileParts = file.split('/');
|
||||
fileName = fileParts[fileParts.length - 1];
|
||||
} else {
|
||||
fileName = '<unknown file>';
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
activeOpacity={0.5}
|
||||
style={styles.openInEditorButton}
|
||||
underlayColor="transparent"
|
||||
onPress={openFileInEditor.bind(null, file, lineNumber)}>
|
||||
<Text style={styles.inspectorCountText}>
|
||||
{fileName}:{lineNumber}
|
||||
</Text>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
const WarningInspector = ({
|
||||
warningInfo,
|
||||
warning,
|
||||
stacktraceVisible,
|
||||
onDismiss,
|
||||
onMinimize,
|
||||
toggleStacktrace,
|
||||
}) => {
|
||||
const ScrollView = require('ScrollView');
|
||||
const Text = require('Text');
|
||||
const TouchableHighlight = require('TouchableHighlight');
|
||||
const View = require('View');
|
||||
const {count, stacktrace} = warningInfo || {};
|
||||
|
||||
const countSentence =
|
||||
'Warning encountered ' + count + ' time' + (count - 1 ? 's' : '') + '.';
|
||||
|
||||
let stacktraceList;
|
||||
if (stacktraceVisible && stacktrace) {
|
||||
stacktraceList = (
|
||||
<View style={styles.stacktraceList}>
|
||||
{stacktrace.map((frame, ii) => <StackRow frame={frame} key={ii} />)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.inspector}>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<View style={styles.inspectorCount}>
|
||||
<Text style={styles.inspectorCountText}>{countSentence}</Text>
|
||||
<TouchableHighlight
|
||||
onPress={toggleStacktrace}
|
||||
underlayColor="transparent">
|
||||
<Text style={styles.inspectorButtonText}>
|
||||
{stacktraceVisible ? '\u{25BC}' : '\u{25B6}'} Stacktrace
|
||||
</Text>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
<ScrollView style={styles.inspectorWarning}>
|
||||
{stacktraceList}
|
||||
<Text style={styles.inspectorWarningText}>{warning}</Text>
|
||||
</ScrollView>
|
||||
<View style={styles.inspectorButtons}>
|
||||
<TouchableHighlight
|
||||
activeOpacity={0.5}
|
||||
onPress={onMinimize}
|
||||
style={styles.inspectorButton}
|
||||
underlayColor="transparent">
|
||||
<Text style={styles.inspectorButtonText}>Minimize</Text>
|
||||
</TouchableHighlight>
|
||||
<TouchableHighlight
|
||||
activeOpacity={0.5}
|
||||
onPress={onDismiss}
|
||||
style={styles.inspectorButton}
|
||||
underlayColor="transparent">
|
||||
<Text style={styles.inspectorButtonText}>Dismiss</Text>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
class YellowBox extends React.Component<
|
||||
mixed,
|
||||
{
|
||||
stacktraceVisible: boolean,
|
||||
inspecting: ?string,
|
||||
warningMap: Map<any, any>,
|
||||
},
|
||||
> {
|
||||
_listener: ?EmitterSubscription;
|
||||
dismissWarning: (warning: ?string) => void;
|
||||
|
||||
constructor(props: mixed, context: mixed) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
inspecting: null,
|
||||
stacktraceVisible: false,
|
||||
warningMap: _warningMap,
|
||||
};
|
||||
this.dismissWarning = warning => {
|
||||
const {inspecting, warningMap} = this.state;
|
||||
if (warning) {
|
||||
warningMap.delete(warning);
|
||||
} else {
|
||||
warningMap.clear();
|
||||
}
|
||||
this.setState({
|
||||
inspecting: warning && inspecting !== warning ? inspecting : null,
|
||||
warningMap,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
static ignoreWarnings(warnings: Array<string>): void {
|
||||
warnings.forEach((warning: string) => {
|
||||
if (IGNORED_WARNINGS.indexOf(warning) === -1) {
|
||||
IGNORED_WARNINGS.push(warning);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let scheduled = null;
|
||||
this._listener = _warningEmitter.addListener('warning', warningMap => {
|
||||
// Use `setImmediate` because warnings often happen during render, but
|
||||
// state cannot be set while rendering.
|
||||
scheduled =
|
||||
scheduled ||
|
||||
setImmediate(() => {
|
||||
scheduled = null;
|
||||
this.setState({
|
||||
warningMap,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const {inspecting} = this.state;
|
||||
if (inspecting != null) {
|
||||
ensureSymbolicatedWarning(inspecting);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._listener) {
|
||||
this._listener.remove();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
/* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an
|
||||
* error found when Flow v0.68 was deployed. To see the error delete this
|
||||
* comment and run Flow. */
|
||||
if (console.disableYellowBox || this.state.warningMap.size === 0) {
|
||||
return null;
|
||||
}
|
||||
const ScrollView = require('ScrollView');
|
||||
const View = require('View');
|
||||
const Text = require('Text');
|
||||
const TouchableHighlight = require('TouchableHighlight');
|
||||
|
||||
const {inspecting, stacktraceVisible} = this.state;
|
||||
const inspector =
|
||||
inspecting !== null ? (
|
||||
<WarningInspector
|
||||
warningInfo={this.state.warningMap.get(inspecting)}
|
||||
warning={inspecting}
|
||||
stacktraceVisible={stacktraceVisible}
|
||||
onDismiss={() => this.dismissWarning(inspecting)}
|
||||
onMinimize={() => this.setState({inspecting: null})}
|
||||
toggleStacktrace={() =>
|
||||
this.setState({stacktraceVisible: !stacktraceVisible})
|
||||
}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const rows = [];
|
||||
this.state.warningMap.forEach((warningInfo, warning) => {
|
||||
if (!isWarningIgnored(warning)) {
|
||||
rows.push(
|
||||
<WarningRow
|
||||
key={warning}
|
||||
count={warningInfo.count}
|
||||
warning={warning}
|
||||
onPress={() => this.setState({inspecting: warning})}
|
||||
onDismiss={() => this.dismissWarning(warning)}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const listStyle = [
|
||||
styles.list,
|
||||
// Additional `0.4` so the 5th row can peek into view.
|
||||
{height: Math.min(rows.length, 4.4) * (rowGutter + rowHeight)},
|
||||
];
|
||||
return (
|
||||
<View style={inspector ? styles.fullScreen : listStyle}>
|
||||
{!inspector &&
|
||||
rows.length > 0 && (
|
||||
<TouchableHighlight
|
||||
style={styles.dismissAllContainer}
|
||||
onPress={() => this.dismissWarning(null)}>
|
||||
<Text style={styles.dismissAll}>Dismiss All</Text>
|
||||
</TouchableHighlight>
|
||||
)}
|
||||
<ScrollView style={listStyle} scrollsToTop={false}>
|
||||
{rows}
|
||||
</ScrollView>
|
||||
{inspector}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const backgroundColor = opacity => 'rgba(250, 186, 48, ' + opacity + ')';
|
||||
const textColor = 'white';
|
||||
const rowGutter = 1;
|
||||
const rowHeight = 46;
|
||||
|
||||
// For unknown reasons, setting elevation: Number.MAX_VALUE causes remote debugging to
|
||||
// hang on iOS (some sort of overflow maybe). Setting it to Number.MAX_SAFE_INTEGER fixes the iOS issue, but since
|
||||
// elevation is an android-only style property we might as well remove it altogether for iOS.
|
||||
// See: https://github.com/facebook/react-native/issues/12223
|
||||
const elevation =
|
||||
Platform.OS === 'android' ? Number.MAX_SAFE_INTEGER : undefined;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
fullScreen: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
elevation: elevation,
|
||||
position: 'absolute',
|
||||
},
|
||||
inspector: {
|
||||
backgroundColor: backgroundColor(0.95),
|
||||
height: '100%',
|
||||
paddingTop: 5,
|
||||
elevation: elevation,
|
||||
},
|
||||
inspectorButtons: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
inspectorButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 22,
|
||||
backgroundColor: backgroundColor(1),
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
stacktraceList: {
|
||||
paddingBottom: 5,
|
||||
},
|
||||
inspectorButtonText: {
|
||||
color: textColor,
|
||||
fontSize: 14,
|
||||
opacity: 0.8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
openInEditorButton: {
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
},
|
||||
inspectorCount: {
|
||||
padding: 15,
|
||||
paddingBottom: 0,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
inspectorCountText: {
|
||||
color: textColor,
|
||||
fontSize: 14,
|
||||
},
|
||||
inspectorWarning: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
inspectorWarningText: {
|
||||
color: textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
list: {
|
||||
backgroundColor: 'transparent',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
elevation: elevation,
|
||||
},
|
||||
listRow: {
|
||||
backgroundColor: backgroundColor(0.95),
|
||||
height: rowHeight,
|
||||
marginTop: rowGutter,
|
||||
},
|
||||
listRowContent: {
|
||||
flex: 1,
|
||||
},
|
||||
listRowCount: {
|
||||
color: 'rgba(255, 255, 255, 0.5)',
|
||||
},
|
||||
listRowText: {
|
||||
color: textColor,
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: Platform.OS === 'android' ? 5 : 7,
|
||||
marginLeft: 15,
|
||||
marginRight: 15,
|
||||
},
|
||||
dismissAllContainer: {
|
||||
height: 20,
|
||||
justifyContent: 'center',
|
||||
marginTop: -30,
|
||||
marginRight: 5,
|
||||
backgroundColor: backgroundColor(0.95),
|
||||
alignSelf: 'flex-end',
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: 10,
|
||||
},
|
||||
dismissAll: {
|
||||
color: 'white',
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = YellowBox;
|
||||
Reference in New Issue
Block a user