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