├── .npmignore ├── LICENSE ├── README.md ├── index.js ├── package.json ├── snapshot ├── 79tx5-qew2r.gif ├── abovz-11w3r.jpg └── aka6s-nqpkq.jpg └── src ├── config └── index.js ├── event.js ├── hoc.js ├── info.js ├── log.js ├── network.js ├── storage.js └── tool.js /.npmignore: -------------------------------------------------------------------------------- 1 | snapshot/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-vdebug 2 | 3 | 4 | [![NPM Version](http://img.shields.io/npm/v/react-native-vdebug.svg?style=flat)](https://www.npmjs.org/package/react-native-vdebug) 5 | [![NPM Downloads](https://img.shields.io/npm/dm/react-native-vdebug.svg?style=flat)](https://npmcharts.com/compare/react-native-vdebug?minimal=true) 6 | [![install size](https://packagephobia.now.sh/badge?p=react-native-vdebug)](https://packagephobia.now.sh/result?p=react-native-vdebug) 7 | 8 | 9 | `React-Native 调试工具` 10 | 11 | ### 支持情况 12 | - [x] Command 自定义上下文 13 | - [x] 复制 cURL 至粘贴板 14 | - [x] 重新请求 URL 15 | - [x] 可视化 Response 16 | - [x] Log 等级分类 17 | - [x] 关键字过滤 Log / Network 18 | - [x] Command 历史记录 19 | - [ ] 导出所有 Log / Network (ing...) 20 | 21 | ## Install 22 | 23 | [Install NodeJS and suggest >= 8.11.0](https://nodejs.org/zh-cn/) 24 | 25 | ## Usage 26 | 27 | ```JavaScript 28 | npm install 'react-native-vdebug' 29 | 30 | import VDebug, { initTrace, setExternalContext } from 'react-native-vdebug'; 31 | 32 | // Before component Render, perform Proxy Console/Network (Optional) 33 | initTrace() 34 | 35 | // Context object when the command is executed (Optional) 36 | setExternalContext('your context') 37 | 38 | return 47 | 48 | ``` 49 | 50 | ## Snapshot 51 | 52 | 53 |
54 | 55 |
56 | 57 |
58 | 59 | ------------------- 60 | 61 | [初始版本](https://github.com/fwon/RNVConsole) / [✶ MIT ✶](./LICENSE) 62 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { PureComponent } from 'react'; 3 | import { ScrollView, View, Text, TouchableOpacity, PanResponder, Animated, Dimensions, StyleSheet, TextInput, Keyboard, NativeModules, Platform, KeyboardAvoidingView } from 'react-native'; 4 | import event from './src/event'; 5 | import Network, { traceNetwork } from './src/network'; 6 | import Log, { traceLog } from './src/log'; 7 | import Info from './src/info'; 8 | import HocComp from './src/hoc'; 9 | import Storage from './src/storage'; 10 | import { replaceReg } from './src/tool'; 11 | 12 | const { width, height } = Dimensions.get('window'); 13 | 14 | let commandContext = global; 15 | 16 | export const setExternalContext = externalContext => { 17 | if (externalContext) commandContext = externalContext; 18 | }; 19 | 20 | // Log/network trace when Element is not initialized. 21 | export const initTrace = () => { 22 | traceLog(); 23 | traceNetwork(); 24 | }; 25 | 26 | class VDebug extends PureComponent { 27 | static propTypes = { 28 | // Info panel (Optional) 29 | info: PropTypes.object, 30 | // Expansion panel (Optional) 31 | panels: PropTypes.array 32 | }; 33 | 34 | static defaultProps = { 35 | info: {}, 36 | panels: null 37 | }; 38 | 39 | constructor(props) { 40 | super(props); 41 | initTrace(); 42 | this.containerHeight = (height / 3) * 2; 43 | this.refsObj = {}; 44 | this.state = { 45 | commandValue: '', 46 | showPanel: false, 47 | currentPageIndex: 0, 48 | pan: new Animated.ValueXY(), 49 | scale: new Animated.Value(1), 50 | panelHeight: new Animated.Value(0), 51 | panels: this.addPanels(), 52 | history: [], 53 | historyFilter: [], 54 | showHistory: false 55 | }; 56 | this.panResponder = PanResponder.create({ 57 | onStartShouldSetPanResponder: () => true, 58 | onPanResponderGrant: () => { 59 | this.state.pan.setOffset({ 60 | x: this.state.pan.x._value, 61 | y: this.state.pan.y._value 62 | }); 63 | this.state.pan.setValue({ x: 0, y: 0 }); 64 | Animated.spring(this.state.scale, { 65 | useNativeDriver: true, 66 | toValue: 1.3, 67 | friction: 7 68 | }).start(); 69 | }, 70 | onPanResponderMove: Animated.event([null, { dx: this.state.pan.x, dy: this.state.pan.y }]), 71 | onPanResponderRelease: ({ nativeEvent }, gestureState) => { 72 | if (Math.abs(gestureState.dx) < 5 && Math.abs(gestureState.dy) < 5) this.togglePanel(); 73 | setTimeout(() => { 74 | Animated.spring(this.state.scale, { 75 | useNativeDriver: true, 76 | toValue: 1, 77 | friction: 7 78 | }).start(() => { 79 | this.setState({ 80 | top: nativeEvent.pageY 81 | }); 82 | }); 83 | this.state.pan.flattenOffset(); 84 | }, 0); 85 | } 86 | }); 87 | } 88 | 89 | componentDidMount() { 90 | this.state.pan.setValue({ x: 0, y: 0 }); 91 | Storage.support() && 92 | Storage.get('react-native-vdebug@history').then(res => { 93 | if (res) { 94 | this.setState({ 95 | history: res 96 | }); 97 | } 98 | }); 99 | } 100 | 101 | getRef(index) { 102 | return ref => { 103 | if (!this.refsObj[index]) this.refsObj[index] = ref; 104 | }; 105 | } 106 | 107 | addPanels() { 108 | let defaultPanels = [ 109 | { 110 | title: 'Log', 111 | component: HocComp(Log, this.getRef(0)) 112 | }, 113 | { 114 | title: 'Network', 115 | component: HocComp(Network, this.getRef(1)) 116 | }, 117 | { 118 | title: 'Info', 119 | component: HocComp(Info, this.getRef(2)), 120 | props: { info: this.props.info } 121 | } 122 | ]; 123 | if (this.props.panels && this.props.panels.length) { 124 | this.props.panels.forEach((item, index) => { 125 | // support up to five extended panels 126 | if (index >= 3) return; 127 | if (item.title && item.component) { 128 | item.component = HocComp(item.component, this.getRef(defaultPanels.length)); 129 | defaultPanels.push(item); 130 | } 131 | }); 132 | } 133 | return defaultPanels; 134 | } 135 | 136 | togglePanel() { 137 | this.state.panelHeight.setValue(this.state.panelHeight._value ? 0 : this.containerHeight); 138 | } 139 | 140 | clearLogs() { 141 | const tabName = this.state.panels[this.state.currentPageIndex].title; 142 | event.trigger('clear', tabName); 143 | } 144 | 145 | showDev() { 146 | NativeModules?.DevMenu?.show(); 147 | } 148 | 149 | reloadDev() { 150 | NativeModules?.DevMenu?.reload(); 151 | } 152 | 153 | evalInContext(js, context) { 154 | return function (str) { 155 | let result = ''; 156 | try { 157 | // eslint-disable-next-line no-eval 158 | result = eval(str); 159 | } catch (err) { 160 | result = 'Invalid input'; 161 | } 162 | return event.trigger('addLog', result); 163 | }.call(context, `with(this) { ${js} } `); 164 | } 165 | 166 | execCommand() { 167 | if (!this.state.commandValue) return; 168 | this.evalInContext(this.state.commandValue, commandContext); 169 | this.syncHistory(); 170 | Keyboard.dismiss(); 171 | } 172 | 173 | clearCommand() { 174 | this.textInput.clear(); 175 | this.setState({ 176 | historyFilter: [] 177 | }); 178 | } 179 | 180 | scrollToPage(index, animated = true) { 181 | this.scrollToCard(index, animated); 182 | } 183 | 184 | scrollToCard(cardIndex, animated = true) { 185 | if (cardIndex < 0) cardIndex = 0; 186 | else if (cardIndex >= this.cardCount) cardIndex = this.cardCount - 1; 187 | if (this.scrollView) { 188 | this.scrollView.scrollTo({ x: width * cardIndex, y: 0, animated: animated }); 189 | } 190 | } 191 | 192 | scrollToTop() { 193 | const item = this.refsObj[this.state.currentPageIndex]; 194 | const instance = item?.getScrollRef && item?.getScrollRef(); 195 | if (instance) { 196 | // FlatList 197 | instance.scrollToOffset && instance.scrollToOffset({ animated: true, viewPosition: 0, index: 0 }); 198 | // ScrollView 199 | instance.scrollTo && instance.scrollTo({ x: 0, y: 0, animated: true }); 200 | } 201 | } 202 | 203 | renderPanelHeader() { 204 | return ( 205 | 206 | {this.state.panels.map((item, index) => ( 207 | { 210 | if (index != this.state.currentPageIndex) { 211 | this.scrollToPage(index); 212 | this.setState({ currentPageIndex: index }); 213 | } else { 214 | this.scrollToTop(); 215 | } 216 | }} 217 | style={[styles.panelHeaderItem, index === this.state.currentPageIndex && styles.activeTab]} 218 | > 219 | {item.title} 220 | 221 | ))} 222 | 223 | ); 224 | } 225 | 226 | syncHistory() { 227 | if (!Storage.support()) return; 228 | const res = this.state.history.filter(f => { 229 | return f == this.state.commandValue; 230 | }); 231 | if (res && res.length) return; 232 | this.state.history.splice(0, 0, this.state.commandValue); 233 | this.state.historyFilter.splice(0, 0, this.state.commandValue); 234 | this.setState( 235 | { 236 | history: this.state.history, 237 | historyFilter: this.state.historyFilter 238 | }, 239 | () => { 240 | Storage.save('react-native-vdebug@history', this.state.history); 241 | this.forceUpdate(); 242 | } 243 | ); 244 | } 245 | 246 | onChange(text) { 247 | const state = { commandValue: text }; 248 | if (text) { 249 | const res = this.state.history.filter(f => f.toLowerCase().match(replaceReg(text))); 250 | if (res && res.length) state.historyFilter = res; 251 | } else { 252 | state.historyFilter = []; 253 | } 254 | this.setState(state); 255 | } 256 | 257 | renderCommandBar() { 258 | return ( 259 | 269 | 270 | 271 | {this.state.historyFilter.map(text => { 272 | return ( 273 | { 276 | if (text && text.toString) { 277 | this.setState({ 278 | commandValue: text.toString() 279 | }); 280 | } 281 | }} 282 | > 283 | {text} 284 | 285 | ); 286 | })} 287 | 288 | 289 | 290 | { 292 | this.textInput = ref; 293 | }} 294 | style={styles.commandBarInput} 295 | placeholderTextColor={'#000000a1'} 296 | placeholder="Command..." 297 | onChangeText={this.onChange.bind(this)} 298 | value={this.state.commandValue} 299 | onFocus={() => { 300 | this.setState({ showHistory: true }); 301 | }} 302 | onSubmitEditing={this.execCommand.bind(this)} 303 | /> 304 | 305 | X 306 | 307 | 308 | OK 309 | 310 | 311 | 312 | ); 313 | } 314 | 315 | renderPanelFooter() { 316 | return ( 317 | 318 | 319 | Clear 320 | 321 | {__DEV__ && Platform.OS == 'ios' && ( 322 | 323 | Dev 324 | 325 | )} 326 | 327 | Hide 328 | 329 | 330 | ); 331 | } 332 | 333 | onScrollAnimationEnd({ nativeEvent }) { 334 | const currentPageIndex = Math.floor(nativeEvent.contentOffset.x / Math.floor(width)); 335 | currentPageIndex != this.state.currentPageIndex && 336 | this.setState({ 337 | currentPageIndex: currentPageIndex 338 | }); 339 | } 340 | 341 | renderPanel() { 342 | return ( 343 | 344 | {this.renderPanelHeader()} 345 | { 348 | this.scrollView = ref; 349 | }} 350 | pagingEnabled={true} 351 | showsHorizontalScrollIndicator={false} 352 | horizontal={true} 353 | style={styles.panelContent} 354 | > 355 | {this.state.panels.map((item, index) => { 356 | return ( 357 | 358 | 359 | 360 | ); 361 | })} 362 | 363 | {this.renderCommandBar()} 364 | {this.renderPanelFooter()} 365 | 366 | ); 367 | } 368 | 369 | renderDebugBtn() { 370 | const { pan, scale } = this.state; 371 | const [translateX, translateY] = [pan.x, pan.y]; 372 | const btnStyle = { transform: [{ translateX }, { translateY }, { scale }] }; 373 | 374 | return ( 375 | 376 | Debug 377 | 378 | ); 379 | } 380 | 381 | render() { 382 | return ( 383 | 384 | {this.renderPanel()} 385 | {this.renderDebugBtn()} 386 | 387 | ); 388 | } 389 | } 390 | 391 | const styles = StyleSheet.create({ 392 | activeTab: { 393 | backgroundColor: '#fff' 394 | }, 395 | panel: { 396 | position: 'absolute', 397 | zIndex: 99998, 398 | elevation: 99998, 399 | backgroundColor: '#fff', 400 | width, 401 | bottom: 0, 402 | right: 0 403 | }, 404 | panelHeader: { 405 | width, 406 | backgroundColor: '#eee', 407 | flexDirection: 'row', 408 | borderWidth: StyleSheet.hairlineWidth, 409 | borderColor: '#d9d9d9' 410 | }, 411 | panelHeaderItem: { 412 | flex: 1, 413 | height: 40, 414 | color: '#000', 415 | borderRightWidth: StyleSheet.hairlineWidth, 416 | borderColor: '#d9d9d9', 417 | justifyContent: 'center' 418 | }, 419 | panelHeaderItemText: { 420 | textAlign: 'center' 421 | }, 422 | panelContent: { 423 | width, 424 | flex: 0.9 425 | }, 426 | panelBottom: { 427 | width, 428 | borderWidth: StyleSheet.hairlineWidth, 429 | borderColor: '#d9d9d9', 430 | flexDirection: 'row', 431 | alignItems: 'center', 432 | backgroundColor: '#eee', 433 | height: 40 434 | }, 435 | panelBottomBtn: { 436 | flex: 1, 437 | height: 40, 438 | borderRightWidth: StyleSheet.hairlineWidth, 439 | borderColor: '#d9d9d9', 440 | justifyContent: 'center' 441 | }, 442 | panelBottomBtnText: { 443 | color: '#000', 444 | fontSize: 14, 445 | textAlign: 'center' 446 | }, 447 | panelEmpty: { 448 | flex: 1, 449 | alignItems: 'center', 450 | justifyContent: 'center' 451 | }, 452 | homeBtn: { 453 | width: 60, 454 | paddingVertical: 5, 455 | backgroundColor: '#04be02', 456 | borderRadius: 4, 457 | alignItems: 'center', 458 | justifyContent: 'center', 459 | position: 'absolute', 460 | zIndex: 99999, 461 | bottom: height / 2, 462 | right: 0, 463 | shadowColor: 'rgb(18,34,74)', 464 | shadowOffset: { width: 0, height: 1 }, 465 | shadowOpacity: 0.08, 466 | elevation: 99999 467 | }, 468 | homeBtnText: { 469 | color: '#fff' 470 | }, 471 | commandBar: { 472 | flexDirection: 'row', 473 | borderWidth: StyleSheet.hairlineWidth, 474 | borderColor: '#d9d9d9', 475 | flexDirection: 'row', 476 | height: 40 477 | }, 478 | commandBarInput: { 479 | flex: 1, 480 | paddingLeft: 10, 481 | backgroundColor: '#ffffff', 482 | color: '#000000' 483 | }, 484 | commandBarBtn: { 485 | width: 40, 486 | alignItems: 'center', 487 | justifyContent: 'center', 488 | backgroundColor: '#eee' 489 | }, 490 | historyContainer: { 491 | borderTopWidth: 1, 492 | borderTopColor: '#d9d9d9', 493 | backgroundColor: '#ffffff', 494 | paddingHorizontal: 10 495 | } 496 | }); 497 | 498 | export default VDebug; 499 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-vdebug", 3 | "version": "1.2.2", 4 | "description": "React-Native 调试工具", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/itenl/react-native-vdebug.git" 12 | }, 13 | "keywords": [ 14 | "react-native", 15 | "vdebug", 16 | "vconsole" 17 | ], 18 | "author": "itenl", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/itenl/react-native-vdebug/issues" 22 | }, 23 | "engines": { 24 | "node": ">=8.11.0" 25 | }, 26 | "homepage": "https://github.com/itenl/react-native-vdebug#readme" 27 | } 28 | -------------------------------------------------------------------------------- /snapshot/79tx5-qew2r.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itenl/react-native-vdebug/6568894b439aead2f7274514bed51f395d86bb49/snapshot/79tx5-qew2r.gif -------------------------------------------------------------------------------- /snapshot/abovz-11w3r.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itenl/react-native-vdebug/6568894b439aead2f7274514bed51f395d86bb49/snapshot/abovz-11w3r.jpg -------------------------------------------------------------------------------- /snapshot/aka6s-nqpkq.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itenl/react-native-vdebug/6568894b439aead2f7274514bed51f395d86bb49/snapshot/aka6s-nqpkq.jpg -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | import packagejson from '../../package.json'; 2 | 3 | export default { 4 | APPINFO: { 5 | name: packagejson.name, 6 | author: packagejson.author, 7 | homepage: 'https://itenl.com', 8 | repository: packagejson.repository.url, 9 | description: packagejson.description, 10 | version: packagejson.version 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/event.js: -------------------------------------------------------------------------------- 1 | export default class Event { 2 | constructor() { 3 | this.eventList = {} 4 | } 5 | 6 | on(eventName, callback) { 7 | if (!this.eventList[eventName]) { 8 | this.eventList[eventName] = [] 9 | } 10 | this.eventList[eventName].push(callback) 11 | return this 12 | } 13 | 14 | trigger(...args) { 15 | const key = Array.prototype.shift.call(args) 16 | const fns = this.eventList[key] 17 | if (!fns || fns.length === 0) { 18 | return this 19 | } 20 | for (let i = 0, fn; (fn = fns[i++]); ) { 21 | fn.apply(this, args) 22 | } 23 | return this 24 | } 25 | 26 | off(key, fn) { 27 | const fns = this.eventList[key] 28 | if (!fns) { 29 | return this 30 | } 31 | if (!fn) { 32 | if (fns) { 33 | fns.length = 0 34 | } 35 | } else { 36 | for (let i = fns.length - 1; i >= 0; i--) { 37 | const _fn = fns[i] 38 | if (_fn === fn) { 39 | fns.splice(i, 1) 40 | } 41 | } 42 | } 43 | return this 44 | } 45 | } 46 | let event 47 | module.exports = (function() { 48 | if (!event) { 49 | event = new Event() 50 | } 51 | return event 52 | })() 53 | -------------------------------------------------------------------------------- /src/hoc.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | 3 | export default (WrappedComponent, getRef = () => {}) => { 4 | return class Hoc extends PureComponent { 5 | constructor(props) { 6 | super(props); 7 | } 8 | render() { 9 | return ( 10 | { 12 | this.comp = comp; 13 | getRef && getRef(comp); 14 | }} 15 | {...this.props} 16 | /> 17 | ); 18 | } 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/info.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Clipboard, ScrollView, View, Text } from 'react-native'; 3 | import config from '../src/config'; 4 | 5 | export default class Info extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | info: '', 10 | enabled: false 11 | }; 12 | } 13 | 14 | verifyPassword() { 15 | Clipboard.getString().then(password => { 16 | const date = new Date(); 17 | if (password == `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}|itenl`) { 18 | this.setState({ 19 | enabled: true 20 | }); 21 | } 22 | }); 23 | } 24 | 25 | getScrollRef() { 26 | return this.scrollView; 27 | } 28 | 29 | componentDidMount() { 30 | let info = Object.assign( 31 | { 32 | APP_INFO: config.APPINFO 33 | }, 34 | { EXTERNAL_INFO: this.props.info } 35 | ); 36 | if (typeof info === 'object') { 37 | try { 38 | info = JSON.stringify(info, null, 2); 39 | } catch (err) { 40 | console.log(err); 41 | } 42 | } 43 | this.setState({ 44 | info 45 | }); 46 | this.verifyPassword(); 47 | } 48 | 49 | render() { 50 | return ( 51 | { 53 | this.scrollView = ref; 54 | }} 55 | style={{ flex: 1, padding: 5 }} 56 | > 57 | 58 | {this.state.info} 59 | 60 | 61 | {` 62 | .::::. 63 | .::::::::::. 64 | :::::::::::: 65 | ..:::::::::::::' 66 | ':::::::::::::' 67 | .::::::::::: 68 | '::::::::::::::.. 69 | ..:::::::::::::::::. 70 | :::::::::::::::::::: 71 | :::: :::::::::::' .:::. 72 | ::::' '::::::' .::::::::. 73 | .::::' ::::: .:::::::':::::. 74 | :.:::' :::::: .:::::::::' ':::::. 75 | .::' :::::.:::::::::' ':::::. 76 | .::' ::::::::::::::' ::::. 77 | ...::: ::::::::::::' ::. 78 | ':. ':::::::::' :::::::::. 79 | '.:::::' ':' 80 | `} 81 | Goddess bless you, there will never be BUG. 82 | 83 | 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/log.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { TextInput, FlatList, Text, StyleSheet, View, TouchableOpacity, Clipboard, TouchableWithoutFeedback, Alert } from 'react-native'; 3 | import event from './event'; 4 | import { debounce } from './tool'; 5 | 6 | const LEVEL_ENUM = { 7 | All: '', 8 | Log: 'log', 9 | Info: 'info', 10 | Warn: 'warn', 11 | Error: 'error' 12 | }; 13 | 14 | let logStack = null; 15 | 16 | class LogStack { 17 | constructor() { 18 | this.logs = []; 19 | this.maxLength = 200; 20 | this.listeners = []; 21 | this.notify = debounce(10, false, this.notify); 22 | } 23 | 24 | getLogs() { 25 | return this.logs; 26 | } 27 | 28 | addLog(method, data) { 29 | if (this.logs.length > this.maxLength) { 30 | this.logs.splice(this.logs.length - 1, 1); 31 | } 32 | const date = new Date(); 33 | this.logs.splice(0, 0, { 34 | index: this.logs.length + 1, 35 | method, 36 | data: strLog(data), 37 | time: `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}:${date.getMilliseconds()}`, 38 | id: unixId() 39 | }); 40 | this.notify(); 41 | } 42 | 43 | clearLogs() { 44 | this.logs = []; 45 | this.notify(); 46 | } 47 | 48 | notify() { 49 | this.listeners.forEach(callback => { 50 | callback(); 51 | }); 52 | } 53 | 54 | attach(callback) { 55 | this.listeners.push(callback); 56 | } 57 | } 58 | 59 | class Log extends Component { 60 | constructor(props) { 61 | super(props); 62 | 63 | this.name = 'Log'; 64 | this.mountState = false; 65 | this.state = { 66 | logs: [], 67 | filterLevel: '' 68 | // filterValue: '' 69 | }; 70 | logStack.attach(() => { 71 | if (this.mountState) { 72 | const logs = logStack.getLogs(); 73 | this.setState({ 74 | logs 75 | }); 76 | } 77 | }); 78 | } 79 | 80 | getScrollRef() { 81 | return this.flatList; 82 | } 83 | 84 | componentDidMount() { 85 | this.mountState = true; 86 | this.setState({ 87 | logs: logStack.getLogs() 88 | }); 89 | // 类方法用bind会指向不同地址,导致off失败 90 | event.on('clear', this.clearLogs); 91 | event.on('addLog', this.addLog); 92 | } 93 | 94 | componentWillUnmount() { 95 | this.mountState = false; 96 | event.off('clear', this.clearLogs); 97 | event.off('addLog', this.addLog); 98 | } 99 | 100 | addLog = msg => { 101 | logStack.addLog('log', [msg]); 102 | }; 103 | 104 | clearLogs = name => { 105 | if (name === this.name) { 106 | logStack.clearLogs(); 107 | } 108 | }; 109 | 110 | ListHeaderComponent() { 111 | return ( 112 | 113 | 114 | Index 115 | Method 116 | 117 | {Object.keys(LEVEL_ENUM).map((key, index) => { 118 | return ( 119 | { 122 | this.setState({ 123 | filterLevel: LEVEL_ENUM[key] 124 | }); 125 | }} 126 | style={[styles.headerBtnLevel, this.state.filterLevel == LEVEL_ENUM[key] && { backgroundColor: '#eeeeee', borderColor: '#959595a1', borderWidth: 1 }]} 127 | > 128 | {key} 129 | 130 | ); 131 | })} 132 | 133 | 134 | 135 | { 137 | this.textInput = ref; 138 | }} 139 | style={styles.filterValueBarInput} 140 | placeholderTextColor={'#000000a1'} 141 | placeholder="After entering the content, please submit to filter..." 142 | onSubmitEditing={({ nativeEvent }) => { 143 | if (nativeEvent) { 144 | this.regInstance = new RegExp(nativeEvent.text, 'ig'); 145 | this.setState({ filterValue: nativeEvent.text }); 146 | } 147 | }} 148 | /> 149 | 150 | X 151 | 152 | 153 | 154 | ); 155 | } 156 | 157 | clearFilterValue() { 158 | this.setState( 159 | { 160 | filterValue: '' 161 | }, 162 | () => { 163 | this.textInput.clear(); 164 | } 165 | ); 166 | } 167 | 168 | renderItem({ item }) { 169 | if (this.state.filterLevel && this.state.filterLevel != item.method) return null; 170 | if (this.state.filterValue && this.regInstance && !this.regInstance.test(item.data)) return null; 171 | return ( 172 | { 174 | try { 175 | Clipboard.setString(`${item.data}\r\n\r\nLight up the little star and support me.\r\nhttps://github.com/itenl/react-native-vdebug`); 176 | Alert.alert('Info', 'Copy successfully', [{ text: 'OK' }]); 177 | } catch (error) {} 178 | }} 179 | > 180 | 181 | 182 | 183 | {item.index} 184 | 185 | 186 | {item.method} 187 | 188 | 189 | {item.time} 190 | 191 | 192 | {item.data} 193 | 194 | 195 | ); 196 | } 197 | 198 | render() { 199 | return ( 200 | { 202 | this.flatList = ref; 203 | }} 204 | legacyImplementation 205 | // initialNumToRender={20} 206 | showsVerticalScrollIndicator 207 | extraData={this.state} 208 | data={this.state.logs} 209 | stickyHeaderIndices={[0]} 210 | ListHeaderComponent={this.ListHeaderComponent.bind(this)} 211 | renderItem={this.renderItem.bind(this)} 212 | ListEmptyComponent={() => Loading...} 213 | keyExtractor={item => item.id} 214 | /> 215 | ); 216 | } 217 | } 218 | 219 | const styles = StyleSheet.create({ 220 | log: { 221 | color: '#000' 222 | }, 223 | info: { 224 | color: '#000' 225 | }, 226 | warn: { 227 | color: 'orange', 228 | backgroundColor: '#fffacd', 229 | borderColor: '#ffb930' 230 | }, 231 | error: { 232 | color: '#dc143c', 233 | backgroundColor: '#ffe4e1', 234 | borderColor: '#f4a0ab' 235 | }, 236 | logItem: { 237 | borderBottomWidth: StyleSheet.hairlineWidth, 238 | borderColor: '#eee' 239 | }, 240 | logItemText: { 241 | fontSize: 12, 242 | paddingHorizontal: 10, 243 | paddingVertical: 8 244 | }, 245 | logItemTime: { 246 | marginLeft: 5, 247 | fontSize: 11, 248 | fontWeight: '700' 249 | }, 250 | filterValueBarBtn: { 251 | width: 40, 252 | alignItems: 'center', 253 | justifyContent: 'center', 254 | backgroundColor: '#eee' 255 | }, 256 | filterValueBarInput: { 257 | flex: 1, 258 | paddingLeft: 10, 259 | backgroundColor: '#ffffff', 260 | color: '#000000' 261 | }, 262 | filterValueBar: { 263 | flexDirection: 'row', 264 | height: 40, 265 | borderWidth: 1, 266 | borderColor: '#eee' 267 | }, 268 | headerText: { 269 | flex: 0.8, 270 | borderColor: '#eee', 271 | borderWidth: StyleSheet.hairlineWidth, 272 | paddingVertical: 4, 273 | paddingHorizontal: 2, 274 | fontWeight: '700' 275 | }, 276 | headerBtnLevel: { 277 | flex: 1, 278 | borderColor: '#eee', 279 | borderWidth: StyleSheet.hairlineWidth, 280 | paddingHorizontal: 2 281 | }, 282 | headerTextLevel: { 283 | fontWeight: '700', 284 | textAlign: 'center' 285 | } 286 | }); 287 | 288 | function unixId() { 289 | return Math.round(Math.random() * 1000000).toString(16); 290 | } 291 | 292 | function strLog(logs) { 293 | const arr = logs.map(data => formatLog(data)); 294 | return arr.join(' '); 295 | } 296 | 297 | function formatLog(obj) { 298 | if (obj === null || obj === undefined || typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean' || typeof obj === 'function') { 299 | return `"${String(obj)}"`; 300 | } 301 | if (obj instanceof Date) { 302 | return `Date(${obj.toISOString()})`; 303 | } 304 | if (Array.isArray(obj)) { 305 | return `Array(${obj.length})[${obj.map(elem => formatLog(elem))}]`; 306 | } 307 | if (obj.toString) { 308 | try { 309 | return `object(${JSON.stringify(obj, null, 2)})`; 310 | } catch (err) { 311 | return 'Invalid symbol'; 312 | } 313 | } 314 | return 'unknown data'; 315 | } 316 | 317 | function proxyConsole(console, stack) { 318 | const methods = [LEVEL_ENUM.Log, LEVEL_ENUM.Info, LEVEL_ENUM.Warn, LEVEL_ENUM.Error]; 319 | methods.forEach(method => { 320 | const fn = console[method]; 321 | console[method] = function (...args) { 322 | stack.addLog(method, args); 323 | fn.apply(console, args); 324 | }; 325 | }); 326 | } 327 | 328 | export default Log; 329 | 330 | export const traceLog = () => { 331 | if (!logStack) { 332 | logStack = new LogStack(); 333 | proxyConsole(global.console, logStack); 334 | } 335 | }; 336 | -------------------------------------------------------------------------------- /src/network.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { TextInput, Clipboard, View, Text, StyleSheet, FlatList, TouchableOpacity } from 'react-native'; 3 | import event from './event'; 4 | import { debounce } from './tool'; 5 | 6 | let ajaxStack = null; 7 | 8 | class AjaxStack { 9 | constructor() { 10 | this.requestIds = []; 11 | this.requests = {}; 12 | this.maxLength = 200; 13 | this.listeners = []; 14 | this.notify = debounce(10, false, this.notify); 15 | } 16 | 17 | getRequestIds() { 18 | return this.requestIds; 19 | } 20 | 21 | getRequests() { 22 | return this.requests; 23 | } 24 | 25 | getRequest(id) { 26 | return this.requests[id] || {}; 27 | } 28 | 29 | readBlobAsText(blob, encoding = 'utf-8') { 30 | return new Promise((resolve, reject) => { 31 | const fr = new FileReader(); 32 | fr.onload = event => { 33 | resolve(fr.result); 34 | }; 35 | fr.onerror = err => { 36 | reject(err); 37 | }; 38 | fr.readAsText(blob, encoding); 39 | }); 40 | } 41 | 42 | JSONTryParse(jsonStr) { 43 | try { 44 | return JSON.parse(jsonStr); 45 | } catch (error) { 46 | return {}; 47 | } 48 | } 49 | 50 | formatResponse(response) { 51 | if (response) { 52 | if (typeof response == 'string') response = this.JSONTryParse(response); 53 | return JSON.stringify(response, null, 2); 54 | } else { 55 | return '{}'; 56 | } 57 | } 58 | 59 | updateRequest(id, data) { 60 | // update item 61 | const item = this.requests[id] || {}; 62 | 63 | if (this.requestIds.length > this.maxLength) { 64 | const _id = this.requestIds[this.requestIds.length - 1]; 65 | this.requestIds.splice(this.requestIds.length - 1, 1); 66 | this.requests[id] && delete this.requests[_id]; 67 | } 68 | for (const key in data) { 69 | item[key] = data[key]; 70 | } 71 | // update dom 72 | const domData = { 73 | id, 74 | index: item.index ?? this.requestIds.length + 1, 75 | host: item.host, 76 | url: item.url, 77 | status: item.status, 78 | method: item.method || '-', 79 | costTime: item.costTime > 0 ? `${item.costTime} ms` : '-', 80 | resHeaders: item.resHeaders || null, 81 | reqHeaders: item.reqHeaders || null, 82 | getData: item.getData || null, 83 | postData: item.postData || null, 84 | response: null, 85 | actived: !!item.actived, 86 | startTime: item.startTime, 87 | endTime: item.endTime 88 | }; 89 | switch (item.responseType) { 90 | case '': 91 | case 'text': 92 | // try to parse JSON 93 | if (typeof item.response === 'string') { 94 | try { 95 | domData.response = this.formatResponse(item.response); 96 | } catch (e) { 97 | // not a JSON string 98 | domData.response = item.response; 99 | } 100 | } else if (typeof item.response !== 'undefined') { 101 | domData.response = Object.prototype.toString.call(item.response); 102 | } 103 | break; 104 | case 'json': 105 | if (typeof item.response !== 'undefined') { 106 | domData.response = this.formatResponse(item.response); 107 | } 108 | break; 109 | case 'blob': 110 | case 'document': 111 | case 'arraybuffer': 112 | default: 113 | if (item.response && typeof item.response !== 'undefined') { 114 | this.readBlobAsText(item.response).then(res => { 115 | domData.response = this.formatResponse(res); 116 | }); 117 | } 118 | break; 119 | } 120 | if (item.readyState === 0 || item.readyState === 1) { 121 | domData.status = 'Pending'; 122 | } else if (item.readyState === 2 || item.readyState === 3) { 123 | domData.status = 'Loading'; 124 | } else if (item.readyState === 4) { 125 | // do nothing 126 | } else { 127 | domData.status = 'Unknown'; 128 | } 129 | if (this.requestIds.indexOf(id) === -1) { 130 | this.requestIds.splice(0, 0, id); 131 | } 132 | this.requests[id] = domData; 133 | this.notify(this.requests[id]); 134 | } 135 | 136 | clearRequests() { 137 | this.requestIds = []; 138 | this.requests = {}; 139 | this.notify(); 140 | } 141 | 142 | notify(args) { 143 | this.listeners.forEach(callback => { 144 | callback(args); 145 | }); 146 | } 147 | 148 | attach(callback) { 149 | this.listeners.push(callback); 150 | } 151 | } 152 | 153 | class Network extends Component { 154 | constructor(props) { 155 | super(props); 156 | this.name = 'Network'; 157 | this.mountState = false; 158 | this.state = { 159 | showingId: null, 160 | requestIds: [], 161 | requests: {}, 162 | filterValue: '' 163 | }; 164 | ajaxStack.attach(currentRequest => { 165 | if (this.mountState) { 166 | this.setState({ 167 | requestIds: ajaxStack.getRequestIds(), 168 | requests: ajaxStack.getRequests() 169 | }); 170 | } 171 | }); 172 | } 173 | 174 | getScrollRef() { 175 | return this.flatList; 176 | } 177 | 178 | componentDidMount() { 179 | this.mountState = true; 180 | this.setState({ 181 | requestIds: ajaxStack.getRequestIds(), 182 | requests: ajaxStack.getRequests() 183 | }); 184 | event.on('clear', this.clearRequests.bind(this)); 185 | } 186 | 187 | componentWillUnmount() { 188 | this.mountState = false; 189 | event.off('clear', this.clearRequests.bind(this)); 190 | } 191 | 192 | clearRequests(name) { 193 | if (name === this.name) { 194 | ajaxStack.clearRequests(); 195 | } 196 | } 197 | 198 | ListHeaderComponent() { 199 | const count = Object.keys(this.state.requests).length || 0; 200 | return ( 201 | 202 | 203 | ({count})Host 204 | Method 205 | Status 206 | Time/Retry 207 | 208 | 209 | { 211 | this.textInput = ref; 212 | }} 213 | style={styles.filterValueBarInput} 214 | placeholderTextColor={'#000000a1'} 215 | placeholder="After entering the content, please submit to filter..." 216 | onSubmitEditing={({ nativeEvent }) => { 217 | if (nativeEvent) { 218 | this.regInstance = new RegExp(nativeEvent.text, 'ig'); 219 | this.setState({ filterValue: nativeEvent.text }); 220 | } 221 | }} 222 | /> 223 | 224 | X 225 | 226 | 227 | 228 | ); 229 | } 230 | 231 | clearFilterValue() { 232 | this.setState( 233 | { 234 | filterValue: '' 235 | }, 236 | () => { 237 | this.textInput.clear(); 238 | } 239 | ); 240 | } 241 | 242 | copy2cURL(item) { 243 | let headerStr = ''; 244 | if (item.reqHeaders) { 245 | Object.keys(item.reqHeaders).forEach(key => { 246 | let reqHeaders = item.reqHeaders[key]; 247 | if (reqHeaders) { 248 | headerStr += ` -H '${key}: ${reqHeaders}'`; 249 | } 250 | }); 251 | } 252 | let cURL = `curl -X ${item.method} '${item.url}' ${headerStr}`; 253 | if (item.method === 'POST' && item.postData) cURL += ` --data-binary '${item.postData}'`; 254 | Clipboard.setString(cURL); 255 | } 256 | 257 | retryFetch(item) { 258 | let options = { 259 | method: item.method 260 | }; 261 | if (item.reqHeaders) options.headers = item.reqHeaders; 262 | if (item.method == 'POST' && item.postData) options.body = item.postData; 263 | fetch(item.url, options); 264 | } 265 | 266 | renderItem({ item }) { 267 | const _item = this.state.requests[item] || {}; 268 | if (this.state.filterValue && this.regInstance && !this.regInstance.test(_item.url)) return null; 269 | return ( 270 | 271 | { 273 | this.setState(state => ({ 274 | showingId: state.showingId === _item.id ? null : _item.id 275 | })); 276 | }} 277 | > 278 | = 400 && styles.error]}> 279 | 280 | {`(${_item.index})${_item.host}`} 281 | 282 | {_item.method} 283 | 284 | {_item.status} 285 | 286 | { 288 | this.retryFetch(_item); 289 | }} 290 | style={[styles.nwHeaderTitle, { width: 90, borderRadius: 20, borderColor: '#eeeeee', borderWidth: 1 }]} 291 | > 292 | {_item.costTime} 293 | 294 | 295 | 296 | {this.state.showingId === _item.id && ( 297 | 298 | 299 | Operate 300 | { 302 | this.copy2cURL(_item); 303 | }} 304 | > 305 | {'[ Copy cURL to clipboard ]'} 306 | 307 | { 309 | Clipboard.setString(_item.response); 310 | }} 311 | > 312 | {'[ Copy response to clipboard ]'} 313 | 314 | 315 | 316 | General 317 | 318 | URL: 319 | {_item.url} 320 | 321 | 322 | startTime: 323 | {_item.startTime} 324 | 325 | 326 | endTime: 327 | {_item.endTime} 328 | 329 | 330 | {_item.reqHeaders && ( 331 | 332 | Request Header 333 | {Object.keys(_item.reqHeaders).map(key => ( 334 | 335 | {key}: 336 | {_item.reqHeaders[key]} 337 | 338 | ))} 339 | 340 | )} 341 | {_item.resHeaders && ( 342 | 343 | Response Header 344 | {Object.keys(_item.resHeaders).map(key => ( 345 | 346 | {key}: 347 | {_item.resHeaders[key]} 348 | 349 | ))} 350 | 351 | )} 352 | {_item.getData && ( 353 | 354 | Query String Parameters 355 | {Object.keys(_item.getData).map(key => ( 356 | 357 | {key}: 358 | {_item.getData[key]} 359 | 360 | ))} 361 | 362 | )} 363 | {_item.postData && ( 364 | 365 | Form Data 366 | {_item.postData} 367 | 368 | )} 369 | 370 | Response 371 | 372 | {_item.response || ''} 373 | 374 | 375 | 376 | )} 377 | 378 | ); 379 | } 380 | 381 | render() { 382 | return ( 383 | { 385 | this.flatList = ref; 386 | }} 387 | showsVerticalScrollIndicator={true} 388 | ListHeaderComponent={this.ListHeaderComponent.bind(this)} 389 | extraData={this.state} 390 | data={this.state.requestIds} 391 | stickyHeaderIndices={[0]} 392 | renderItem={this.renderItem.bind(this)} 393 | ListEmptyComponent={() => Loading...} 394 | keyExtractor={item => item} 395 | /> 396 | ); 397 | } 398 | } 399 | 400 | const styles = StyleSheet.create({ 401 | bold: { 402 | fontWeight: '700' 403 | }, 404 | active: { 405 | backgroundColor: '#fffacd' 406 | }, 407 | flex3: { 408 | flex: 3 409 | }, 410 | flex1: { 411 | flex: 1 412 | }, 413 | error: { 414 | backgroundColor: '#ffe4e1', 415 | borderColor: '#ffb930' 416 | }, 417 | nwHeader: { 418 | flexDirection: 'row', 419 | backgroundColor: '#fff' 420 | }, 421 | nwHeaderTitle: { 422 | borderColor: '#eee', 423 | borderWidth: StyleSheet.hairlineWidth, 424 | paddingVertical: 4, 425 | paddingHorizontal: 2 426 | }, 427 | nwItem: {}, 428 | nwItemDetail: { 429 | borderColor: '#eee', 430 | borderLeftWidth: StyleSheet.hairlineWidth 431 | }, 432 | nwItemDetailHeader: { 433 | paddingLeft: 5, 434 | paddingVertical: 4, 435 | backgroundColor: '#eee' 436 | }, 437 | nwDetailItem: { 438 | paddingLeft: 5, 439 | flexDirection: 'row' 440 | }, 441 | filterValueBarBtn: { 442 | width: 40, 443 | alignItems: 'center', 444 | justifyContent: 'center', 445 | backgroundColor: '#eee' 446 | }, 447 | filterValueBarInput: { 448 | flex: 1, 449 | paddingLeft: 10, 450 | backgroundColor: '#ffffff', 451 | color: '#000000' 452 | }, 453 | filterValueBar: { 454 | flexDirection: 'row', 455 | height: 40, 456 | borderWidth: 1, 457 | borderColor: '#eee' 458 | } 459 | }); 460 | 461 | function unixId() { 462 | return Math.round(Math.random() * 1000000).toString(16); 463 | } 464 | 465 | function proxyAjax(XHR, stack) { 466 | if (!XHR) { 467 | return; 468 | } 469 | const _open = XHR.prototype.open; 470 | const _send = XHR.prototype.send; 471 | this._open = _open; 472 | this._send = _send; 473 | 474 | // mock open() 475 | XHR.prototype.open = function (...args) { 476 | const XMLReq = this; 477 | const method = args[0]; 478 | const url = args[1]; 479 | const id = unixId(); 480 | let timer = null; 481 | 482 | // may be used by other functions 483 | XMLReq._requestID = id; 484 | XMLReq._method = method; 485 | XMLReq._url = url; 486 | 487 | // mock onreadystatechange 488 | const _onreadystatechange = XMLReq.onreadystatechange || function () {}; 489 | const onreadystatechange = function () { 490 | const item = stack.getRequest(id); 491 | 492 | // update status 493 | item.readyState = XMLReq.readyState; 494 | item.status = 0; 495 | if (XMLReq.readyState > 1) { 496 | item.status = XMLReq.status; 497 | } 498 | item.responseType = XMLReq.responseType; 499 | 500 | if (XMLReq.readyState === 0) { 501 | // UNSENT 502 | if (!item.startTime) { 503 | item.startTime = +new Date(); 504 | } 505 | } else if (XMLReq.readyState === 1) { 506 | // OPENED 507 | if (!item.startTime) { 508 | item.startTime = +new Date(); 509 | } 510 | } else if (XMLReq.readyState === 2) { 511 | // HEADERS_RECEIVED 512 | item.resHeaders = {}; 513 | const resHeaders = XMLReq.getAllResponseHeaders() || ''; 514 | const resHeadersArr = resHeaders.split('\n'); 515 | // extract plain text to key-value format 516 | for (let i = 0; i < resHeadersArr.length; i++) { 517 | const line = resHeadersArr[i]; 518 | if (!line) { 519 | // eslint-disable-next-line no-continue 520 | continue; 521 | } 522 | const arr = line.split(': '); 523 | const key = arr[0]; 524 | const value = arr.slice(1).join(': '); 525 | item.resHeaders[key] = value; 526 | } 527 | } else if (XMLReq.readyState === 3) { 528 | // LOADING 529 | } else if (XMLReq.readyState === 4) { 530 | // DONE 531 | clearInterval(timer); 532 | item.endTime = +new Date(); 533 | item.costTime = item.endTime - (item.startTime || item.endTime); 534 | item.response = XMLReq.response; 535 | } else { 536 | clearInterval(timer); 537 | } 538 | 539 | if (!XMLReq._noVConsole) { 540 | stack.updateRequest(id, item); 541 | } 542 | return _onreadystatechange.apply(XMLReq, args); 543 | }; 544 | XMLReq.onreadystatechange = onreadystatechange; 545 | 546 | // some 3rd libraries will change XHR's default function 547 | // so we use a timer to avoid lost tracking of readyState 548 | let preState = -1; 549 | timer = setInterval(() => { 550 | if (preState !== XMLReq.readyState) { 551 | preState = XMLReq.readyState; 552 | onreadystatechange.call(XMLReq); 553 | } 554 | }, 10); 555 | 556 | return _open.apply(XMLReq, args); 557 | }; 558 | 559 | // mock send() 560 | XHR.prototype.send = function (...args) { 561 | const XMLReq = this; 562 | const data = args[0]; 563 | 564 | const item = stack.getRequest(XMLReq._requestID); 565 | item.method = XMLReq._method.toUpperCase(); 566 | 567 | let query = XMLReq._url.split('?'); // a.php?b=c&d=?e => ['a.php', 'b=c&d=', '?e'] 568 | item.url = XMLReq._url; 569 | item.host = query[0]; 570 | 571 | if (query.length == 2) { 572 | item.getData = {}; 573 | query = query[1].split('&'); // => ['b=c', 'd=?e'] 574 | for (let q of query) { 575 | q = q.split('='); 576 | item.getData[q[0]] = decodeURIComponent(q[1]); 577 | } 578 | } 579 | 580 | item.reqHeaders = XMLReq._headers; 581 | 582 | if (item.method === 'POST' && data) { 583 | // save POST data 584 | if (typeof data === 'string') { 585 | item.postData = data; 586 | } else { 587 | try { 588 | item.postData = JSON.stringify(data); 589 | } catch (error) {} 590 | } 591 | } 592 | 593 | if (!XMLReq._noVConsole) { 594 | stack.updateRequest(XMLReq._requestID, item); 595 | } 596 | 597 | return _send.apply(XMLReq, args); 598 | }; 599 | } 600 | 601 | export default Network; 602 | 603 | export const traceNetwork = () => { 604 | if (!ajaxStack) { 605 | ajaxStack = new AjaxStack(); 606 | proxyAjax(global.originalXMLHttpRequest || global.XMLHttpRequest, ajaxStack); 607 | } 608 | }; 609 | -------------------------------------------------------------------------------- /src/storage.js: -------------------------------------------------------------------------------- 1 | import React, { AsyncStorage } from 'react-native'; 2 | 3 | const storage = { 4 | support: function () { 5 | try { 6 | if (AsyncStorage && AsyncStorage.getItem && AsyncStorage.setItem) return true; 7 | return false; 8 | } catch (error) { 9 | return false; 10 | } 11 | }, 12 | get: async function (key) { 13 | try { 14 | const value = await AsyncStorage.getItem(key); 15 | if (value !== null) { 16 | const jsonValue = JSON.parse(value); 17 | return jsonValue; 18 | } 19 | return null; 20 | } catch (error) { 21 | return null; 22 | } 23 | }, 24 | save: async function (key, value) { 25 | try { 26 | await AsyncStorage.setItem(key, JSON.stringify(value)); 27 | } catch (error) { 28 | console.log(error); 29 | } 30 | }, 31 | update: function (key, value) { 32 | return StorageUtil.get(key).then(item => { 33 | value = typeof value === 'string' ? value : Object.assign({}, item, value); 34 | return AsyncStorage.setItem(key, JSON.stringify(value)); 35 | }); 36 | }, 37 | delete: async key => { 38 | await AsyncStorage.removeItem(key); 39 | } 40 | }; 41 | 42 | export default storage; 43 | -------------------------------------------------------------------------------- /src/tool.js: -------------------------------------------------------------------------------- 1 | function throttle(delay, noTrailing, callback, debounceMode) { 2 | let timeoutID; 3 | let lastExec = 0; 4 | if (typeof noTrailing !== 'boolean') { 5 | debounceMode = callback; 6 | callback = noTrailing; 7 | noTrailing = undefined; 8 | } 9 | 10 | function wrapper(...args) { 11 | const self = this; 12 | const elapsed = Number(new Date()) - lastExec; 13 | 14 | function exec() { 15 | lastExec = Number(new Date()); 16 | callback.apply(self, args); 17 | } 18 | 19 | function clear() { 20 | timeoutID = undefined; 21 | } 22 | 23 | if (debounceMode && !timeoutID) { 24 | exec(); 25 | } 26 | 27 | if (timeoutID) { 28 | clearTimeout(timeoutID); 29 | } 30 | 31 | if (!debounceMode && elapsed > delay) { 32 | exec(); 33 | } else if (noTrailing !== true) { 34 | timeoutID = setTimeout(debounceMode ? clear : exec, !debounceMode ? delay - elapsed : delay); 35 | } 36 | } 37 | 38 | return wrapper; 39 | } 40 | 41 | function debounce(delay, atBegin, callback) { 42 | return callback === undefined ? throttle(delay, atBegin, false) : throttle(delay, callback, atBegin !== false); 43 | } 44 | 45 | function replaceReg(str) { 46 | const regStr = /\\|\$|\(|\)|\*|\+|\.|\[|\]|\?|\^|\{|\}|\|/gi; 47 | return str.replace(regStr, function (input) { 48 | return `\\${input}`; 49 | }); 50 | } 51 | 52 | module.exports = { 53 | throttle, 54 | debounce, 55 | replaceReg 56 | }; 57 | --------------------------------------------------------------------------------