xref: /MusicFree/src/lib/react-native-vdebug/src/log.js (revision 5589cdf32b2bb0f641e5ac7bf1f6152cd6b9b70e)
1import React, {Component} from 'react';
2import {
3    Alert,
4    FlatList,
5    StyleSheet,
6    Text,
7    TextInput,
8    TouchableOpacity,
9    TouchableWithoutFeedback,
10    View,
11} from 'react-native';
12import event from './event';
13import {debounce} from './tool';
14
15const LEVEL_ENUM = {
16    All: '',
17    Log: 'log',
18    Info: 'info',
19    Warn: 'warn',
20    Error: 'error',
21};
22
23let logStack = null;
24
25class LogStack {
26    constructor() {
27        this.logs = [];
28        this.maxLength = 200;
29        this.listeners = [];
30        this.notify = debounce(10, false, this.notify);
31    }
32
33    getLogs() {
34        return this.logs;
35    }
36
37    addLog(method, data) {
38        if (this.logs.length > this.maxLength) {
39            this.logs.splice(this.logs.length - 1, 1);
40        }
41        const date = new Date();
42        this.logs.splice(0, 0, {
43            index: this.logs.length + 1,
44            method,
45            data: strLog(data),
46            time: `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}:${date.getMilliseconds()}`,
47            id: unixId(),
48        });
49        this.notify();
50    }
51
52    clearLogs() {
53        this.logs = [];
54        this.notify();
55    }
56
57    notify() {
58        this.listeners.forEach(callback => {
59            callback();
60        });
61    }
62
63    attach(callback) {
64        this.listeners.push(callback);
65    }
66}
67
68class Log extends Component {
69    constructor(props) {
70        super(props);
71
72        this.name = 'Log';
73        this.mountState = false;
74        this.state = {
75            logs: [],
76            filterLevel: '',
77            // filterValue: ''
78        };
79        logStack.attach(() => {
80            if (this.mountState) {
81                const logs = logStack.getLogs();
82                this.setState({
83                    logs,
84                });
85            }
86        });
87    }
88
89    getScrollRef() {
90        return this.flatList;
91    }
92
93    componentDidMount() {
94        this.mountState = true;
95        this.setState({
96            logs: logStack.getLogs(),
97        });
98        // 类方法用bind会指向不同地址,导致off失败
99        event.on('clear', this.clearLogs);
100        event.on('addLog', this.addLog);
101    }
102
103    componentWillUnmount() {
104        this.mountState = false;
105        event.off('clear', this.clearLogs);
106        event.off('addLog', this.addLog);
107    }
108
109    addLog = msg => {
110        logStack.addLog('log', [msg]);
111    };
112
113    clearLogs = name => {
114        if (name === this.name) {
115            logStack.clearLogs();
116        }
117    };
118
119    ListHeaderComponent() {
120        return (
121            <View>
122                <View style={{flexDirection: 'row', backgroundColor: '#fff'}}>
123                    <Text style={styles.headerText}>Index</Text>
124                    <Text style={styles.headerText}>Method</Text>
125                    <View
126                        style={[
127                            styles.headerText,
128                            {flexDirection: 'row', flex: 2},
129                        ]}>
130                        {Object.keys(LEVEL_ENUM).map((key, index) => {
131                            return (
132                                <TouchableOpacity
133                                    key={index.toString()}
134                                    onPress={() => {
135                                        this.setState({
136                                            filterLevel: LEVEL_ENUM[key],
137                                        });
138                                    }}
139                                    style={[
140                                        styles.headerBtnLevel,
141                                        this.state.filterLevel ==
142                                            LEVEL_ENUM[key] && {
143                                            backgroundColor: '#eeeeee',
144                                            borderColor: '#959595a1',
145                                            borderWidth: 1,
146                                        },
147                                    ]}>
148                                    <Text style={styles.headerTextLevel}>
149                                        {key}
150                                    </Text>
151                                </TouchableOpacity>
152                            );
153                        })}
154                    </View>
155                </View>
156                <View style={styles.filterValueBar}>
157                    <TextInput
158                        ref={ref => {
159                            this.textInput = ref;
160                        }}
161                        style={styles.filterValueBarInput}
162                        placeholderTextColor={'#000000a1'}
163                        placeholder="输入过滤条件..."
164                        onSubmitEditing={({nativeEvent}) => {
165                            if (nativeEvent) {
166                                this.regInstance = new RegExp(
167                                    nativeEvent.text,
168                                    'ig',
169                                );
170                                this.setState({filterValue: nativeEvent.text});
171                            }
172                        }}
173                    />
174                    <TouchableOpacity
175                        style={styles.filterValueBarBtn}
176                        onPress={this.clearFilterValue.bind(this)}>
177                        <Text>X</Text>
178                    </TouchableOpacity>
179                </View>
180            </View>
181        );
182    }
183
184    clearFilterValue() {
185        this.setState(
186            {
187                filterValue: '',
188            },
189            () => {
190                this.textInput.clear();
191            },
192        );
193    }
194
195    renderItem({item}) {
196        if (this.state.filterLevel && this.state.filterLevel != item.method)
197            return null;
198        if (
199            this.state.filterValue &&
200            this.regInstance &&
201            !this.regInstance.test(item.data)
202        )
203            return null;
204        return (
205            <TouchableWithoutFeedback
206                onLongPress={() => {
207                    try {
208                        Alert.alert('提示', '复制成功', [{text: '确认'}]);
209                    } catch (error) {}
210                }}>
211                <View style={styles.logItem}>
212                    <View style={{flexDirection: 'row'}}>
213                        <View style={{flex: 0.8}}>
214                            <Text style={styles.logItemTime}>{item.index}</Text>
215                        </View>
216                        <View style={{flex: 0.8}}>
217                            <Text style={styles.logItemTime}>
218                                {item.method}
219                            </Text>
220                        </View>
221                        <View style={{flex: 2}}>
222                            <Text style={styles.logItemTime}>{item.time}</Text>
223                        </View>
224                    </View>
225                    <Text style={[styles.logItemText, styles[item.method]]}>
226                        {item.data}
227                    </Text>
228                </View>
229            </TouchableWithoutFeedback>
230        );
231    }
232
233    render() {
234        return (
235            <FlatList
236                ref={ref => {
237                    this.flatList = ref;
238                }}
239                legacyImplementation
240                // initialNumToRender={20}
241                showsVerticalScrollIndicator
242                extraData={this.state}
243                data={this.state.logs}
244                stickyHeaderIndices={[0]}
245                ListHeaderComponent={this.ListHeaderComponent.bind(this)}
246                renderItem={this.renderItem.bind(this)}
247                ListEmptyComponent={() => <Text> Loading...</Text>}
248                keyExtractor={item => item.id}
249            />
250        );
251    }
252}
253
254const styles = StyleSheet.create({
255    log: {
256        color: '#000',
257    },
258    info: {
259        color: '#000',
260    },
261    warn: {
262        color: 'orange',
263        backgroundColor: '#fffacd',
264        borderColor: '#ffb930',
265    },
266    error: {
267        color: '#dc143c',
268        backgroundColor: '#ffe4e1',
269        borderColor: '#f4a0ab',
270    },
271    logItem: {
272        borderBottomWidth: StyleSheet.hairlineWidth,
273        borderColor: '#eee',
274    },
275    logItemText: {
276        fontSize: 12,
277        paddingHorizontal: 10,
278        paddingVertical: 8,
279    },
280    logItemTime: {
281        marginLeft: 5,
282        fontSize: 11,
283        fontWeight: '700',
284    },
285    filterValueBarBtn: {
286        width: 40,
287        alignItems: 'center',
288        justifyContent: 'center',
289        backgroundColor: '#eee',
290    },
291    filterValueBarInput: {
292        flex: 1,
293        paddingLeft: 10,
294        backgroundColor: '#ffffff',
295        color: '#000000',
296    },
297    filterValueBar: {
298        flexDirection: 'row',
299        height: 40,
300        borderWidth: 1,
301        borderColor: '#eee',
302    },
303    headerText: {
304        flex: 0.8,
305        borderColor: '#eee',
306        borderWidth: StyleSheet.hairlineWidth,
307        paddingVertical: 4,
308        paddingHorizontal: 2,
309        fontWeight: '700',
310    },
311    headerBtnLevel: {
312        flex: 1,
313        borderColor: '#eee',
314        borderWidth: StyleSheet.hairlineWidth,
315        paddingHorizontal: 2,
316    },
317    headerTextLevel: {
318        fontWeight: '700',
319        textAlign: 'center',
320    },
321});
322
323function unixId() {
324    return Math.round(Math.random() * 1000000).toString(16);
325}
326
327function strLog(logs) {
328    const arr = logs.map(data => formatLog(data));
329    return arr.join(' ');
330}
331
332function formatLog(obj) {
333    if (
334        obj === null ||
335        obj === undefined ||
336        typeof obj === 'string' ||
337        typeof obj === 'number' ||
338        typeof obj === 'boolean' ||
339        typeof obj === 'function'
340    ) {
341        return `"${String(obj)}"`;
342    }
343    if (obj instanceof Date) {
344        return `Date(${obj.toISOString()})`;
345    }
346    if (Array.isArray(obj)) {
347        return `Array(${obj.length})[${obj.map(elem => formatLog(elem))}]`;
348    }
349    if (obj.toString) {
350        try {
351            return `object(${JSON.stringify(obj, null, 2)})`;
352        } catch (err) {
353            return 'Invalid symbol';
354        }
355    }
356    return 'unknown data';
357}
358
359export default Log;
360
361export const traceLog = () => {
362    if (!logStack) {
363        logStack = new LogStack();
364    }
365};
366
367export const addLog = (level, ...args) => {
368    logStack.addLog(level, args);
369};
370