├── README.md ├── index.js ├── package-lock.json ├── package.json └── src ├── console.js ├── event.js ├── info.js ├── network.js └── tool.js /README.md: -------------------------------------------------------------------------------- 1 | # RNVConsole 2 | vConsole for react native, inspired by vconsole & vconsole-react-native. 3 | Debugger on top of screen. Have a try in expo https://snack.expo.io/SklJHMS3S 4 | 5 | ## Features 6 | 1. console[log, warn, error, info] in Log Panel. 7 | 2. exec Command to show message. 8 | 3. Network request list & detail. 9 | 4. Customized Version Info you want to show. 10 | 11 | ## Install 12 | ``` 13 | npm install rnvconsole 14 | ``` 15 | 16 | ## Usage 17 | ```javascript 18 | // Options 19 | import RN from 'react-native' 20 | import Native from '../native' // your own Module 21 | 22 | // Show app information in INFO panel 23 | const INFO = { 24 | version: '1.0.0', 25 | test_version: '4', 26 | message: 'test xxx features' 27 | } 28 | const options = { 29 | info: INFO, 30 | // global Object can be called in Command Input 31 | global: { 32 | rn: RN 33 | native: Native 34 | } 35 | } 36 | 37 | const RNVConsole = require('rnvconsole').showLogWhenDev(options) 38 | 39 | // in render function 40 | render() { 41 | return ( 42 | 43 | {RNVConsole} // add RNVConsole somewhere in JSX 44 | 45 | 46 | ) 47 | } 48 | ``` 49 | 50 | ## Examples 51 | 52 | ### Log Panel 53 |
54 | 55 | ### Command 56 | Object defined in global options can be call in command. Such as react-native module or your native function. 57 |
58 | 59 | ### Network Panel 60 |
61 | 62 | ### Info Panel 63 |
64 | 65 | ### Dev Button 66 |
67 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * vconsole for react native 开发测试工具 3 | */ 4 | 5 | import React, { PureComponent } from 'react' 6 | import { 7 | View, 8 | Text, 9 | TouchableOpacity, 10 | PanResponder, 11 | Animated, 12 | Dimensions, 13 | StyleSheet, 14 | TextInput, 15 | Keyboard, 16 | NativeModules, 17 | Platform, 18 | KeyboardAvoidingView 19 | } from 'react-native' 20 | import event from './src/event' 21 | import Network from './src/network' 22 | import Log from './src/console' 23 | import Info from './src/info' 24 | 25 | const PANELS = ['Log', 'Network', 'Info'] 26 | const { width, height } = Dimensions.get('window') 27 | let AppInfo = {} 28 | let GlobalCommandObj = {} 29 | 30 | function evalInContext(js, context) { 31 | return function(str) { 32 | let result = '' 33 | try { 34 | // eslint-disable-next-line no-eval 35 | result = eval(str) 36 | } catch (err) { 37 | result = '无效输入' 38 | } 39 | return event.trigger('addLog', result) 40 | }.call(context, `with(this) { ${js} } `) 41 | } 42 | 43 | class RNVConsole extends PureComponent { 44 | constructor(props) { 45 | super(props) 46 | this.state = { 47 | commandValue: '', 48 | showPanel: false, 49 | currentPanelTab: 'Log', 50 | pan: new Animated.ValueXY(), 51 | scale: new Animated.Value(1) 52 | } 53 | this.panResponder = PanResponder.create({ 54 | onStartShouldSetPanResponder: () => true, 55 | onPanResponderGrant: () => { 56 | this.state.pan.setOffset({ 57 | x: this.state.pan.x._value, 58 | y: this.state.pan.y._value 59 | }) 60 | this.state.pan.setValue({ x: 0, y: 0 }) 61 | Animated.spring(this.state.scale, { toValue: 1.3, friction: 3 }).start() 62 | }, 63 | onPanResponderMove: Animated.event([null, { dx: this.state.pan.x, dy: this.state.pan.y }]), 64 | onPanResponderRelease: (evt, gestureState) => { 65 | // 点击 66 | if (Math.abs(gestureState.dx) < 5 && Math.abs(gestureState.dy) < 5) { 67 | this.togglePanel() 68 | } 69 | this.state.pan.flattenOffset() 70 | Animated.spring(this.state.scale, { toValue: 1, friction: 3 }).start() 71 | } 72 | }) 73 | } 74 | 75 | togglePanel = () => { 76 | this.setState(state => ({ 77 | showPanel: !state.showPanel 78 | })) 79 | } 80 | 81 | clearLogs = () => { 82 | const tabName = this.state.currentPanelTab 83 | event.trigger('clear', tabName) 84 | } 85 | 86 | showDevPanel = () => { 87 | NativeModules.DevMenu.show() 88 | } 89 | 90 | reload = () => { 91 | NativeModules.DevMenu.reload() 92 | } 93 | 94 | execCommand = () => { 95 | const context = GlobalCommandObj 96 | evalInContext(this.state.commandValue, context) 97 | Keyboard.dismiss() 98 | } 99 | 100 | renderPanelHeader() { 101 | return ( 102 | 103 | {PANELS.map(type => ( 104 | { 107 | this.setState({ 108 | currentPanelTab: type 109 | }) 110 | }} 111 | style={[ 112 | styles.panelHeaderItem, 113 | type === this.state.currentPanelTab && styles.activeTab 114 | ]} 115 | > 116 | {type} 117 | 118 | ))} 119 | 120 | ) 121 | } 122 | 123 | renderCommandBar() { 124 | return ( 125 | 126 | this.setState({ commandValue: text })} 131 | value={this.state.commandValue} 132 | /> 133 | this.execCommand()}> 134 | OK 135 | 136 | 137 | ) 138 | } 139 | 140 | renderPanelFooter() { 141 | return ( 142 | 143 | 144 | Clear 145 | 146 | 147 | Dev 148 | 149 | 150 | Hide 151 | 152 | 153 | ) 154 | } 155 | 156 | renderEmptyPanel() { 157 | return ( 158 | 159 | 敬请期待! 160 | 161 | ) 162 | } 163 | 164 | renderPanel() { 165 | const panels = { 166 | Log, 167 | Network, 168 | Info: , 169 | Empty: this.renderEmptyPanel() 170 | } 171 | return ( 172 | 176 | {this.renderPanelHeader()} 177 | 178 | {panels[this.state.currentPanelTab || 'Log'] || panels.Empty} 179 | 180 | {this.renderCommandBar()} 181 | {this.renderPanelFooter()} 182 | 183 | ) 184 | } 185 | 186 | renderHomeBtn() { 187 | const { pan, scale } = this.state 188 | const [translateX, translateY] = [pan.x, pan.y] 189 | const btnStyle = { transform: [{ translateX }, { translateY }, { scale }] } 190 | 191 | return ( 192 | 193 | RNVConsole 194 | 195 | ) 196 | } 197 | 198 | render() { 199 | return this.state.showPanel ? this.renderPanel() : this.renderHomeBtn() 200 | } 201 | } 202 | 203 | const styles = StyleSheet.create({ 204 | activeTab: { 205 | backgroundColor: '#fff' 206 | }, 207 | panel: { 208 | position: 'absolute', 209 | zIndex: 999999999, 210 | elevation: 999999999, 211 | backgroundColor: '#fff', 212 | width, 213 | height: (height / 3) * 2, 214 | bottom: 80, 215 | right: 0, 216 | flexDirection: 'column' 217 | }, 218 | panelHeader: { 219 | width, 220 | backgroundColor: '#eee', 221 | flexDirection: 'row', 222 | borderWidth: StyleSheet.hairlineWidth, 223 | borderColor: '#d9d9d9' 224 | }, 225 | panelHeaderItem: { 226 | flex: 1, 227 | height: 40, 228 | color: '#000', 229 | borderRightWidth: StyleSheet.hairlineWidth, 230 | borderColor: '#d9d9d9', 231 | justifyContent: 'center' 232 | }, 233 | panelHeaderItemText: { 234 | textAlign: 'center' 235 | }, 236 | panelContent: { 237 | width, 238 | flex: 0.9 239 | }, 240 | panelBottom: { 241 | width, 242 | flex: 0.1, 243 | borderWidth: StyleSheet.hairlineWidth, 244 | borderColor: '#d9d9d9', 245 | flexDirection: 'row', 246 | alignItems: 'center', 247 | backgroundColor: '#eee' 248 | }, 249 | panelBottomBtn: { 250 | flex: 1, 251 | height: 40, 252 | borderRightWidth: StyleSheet.hairlineWidth, 253 | borderColor: '#d9d9d9', 254 | justifyContent: 'center' 255 | }, 256 | panelBottomBtnText: { 257 | color: '#000', 258 | fontSize: 14, 259 | textAlign: 'center' 260 | }, 261 | panelEmpty: { 262 | flex: 1, 263 | alignItems: 'center', 264 | justifyContent: 'center' 265 | }, 266 | homeBtn: { 267 | width: 100, 268 | height: 40, 269 | backgroundColor: '#04be02', 270 | borderRadius: 4, 271 | alignItems: 'center', 272 | justifyContent: 'center', 273 | position: 'absolute', 274 | zIndex: 999999999, 275 | bottom: 140, 276 | right: 10, 277 | shadowColor: 'rgb(18,34,74)', 278 | shadowOffset: { width: 0, height: 1 }, 279 | shadowOpacity: 0.08, 280 | elevation: 0.4 281 | }, 282 | homeBtnText: { 283 | color: '#fff' 284 | }, 285 | commandBar: { 286 | height: 40, 287 | flexDirection: 'row', 288 | borderWidth: StyleSheet.hairlineWidth, 289 | borderColor: '#d9d9d9' 290 | }, 291 | commandBarInput: { 292 | flex: 1, 293 | paddingLeft: 10 294 | }, 295 | commandBarBtn: { 296 | width: 60, 297 | alignItems: 'center', 298 | justifyContent: 'center', 299 | backgroundColor: '#eee' 300 | } 301 | }) 302 | 303 | module.exports = { 304 | Panel: RNVConsole, 305 | showLogWhenDev(options) { 306 | options = options || {} 307 | AppInfo = options.info || '' 308 | GlobalCommandObj = options.global || {} 309 | return global.__DEV__ ? : null 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rnvconsole", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rnvconsole", 3 | "version": "0.3.0", 4 | "description": "vconsole for 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": "git+https://github.com/fwon/RNVConsole.git" 12 | }, 13 | "keywords": [ 14 | "vconsole;", 15 | "debugger;", 16 | "console;", 17 | "react-native;" 18 | ], 19 | "peerDependencies": { 20 | "react": "*", 21 | "react-native": ">=0.50.0" 22 | }, 23 | "author": "fwon", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/fwon/RNVConsole/issues" 27 | }, 28 | "homepage": "https://github.com/fwon/RNVConsole#readme" 29 | } 30 | -------------------------------------------------------------------------------- /src/console.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { FlatList, Text, StyleSheet, View } from 'react-native' 3 | import event from './event' 4 | import { debounce } from './tool' 5 | 6 | let logStack = null 7 | 8 | // log 消息类 9 | class LogStack { 10 | constructor() { 11 | this.logs = [] 12 | this.maxLength = 100 13 | this.listeners = [] 14 | this.notify = debounce(500, false, this.notify) 15 | } 16 | 17 | getLogs() { 18 | return this.logs 19 | } 20 | 21 | addLog(method, data) { 22 | if (this.logs.length > this.maxLength) { 23 | this.logs = this.logs.slice(1) 24 | } 25 | const date = new Date() 26 | this.logs.push({ 27 | method, 28 | data: strLog(data), 29 | time: `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}:${date.getMilliseconds()}`, 30 | id: unixId() 31 | }) 32 | this.notify() 33 | } 34 | 35 | clearLogs() { 36 | this.logs = [] 37 | this.notify() 38 | } 39 | 40 | notify() { 41 | this.listeners.forEach(callback => { 42 | callback() 43 | }) 44 | } 45 | 46 | attach(callback) { 47 | this.listeners.push(callback) 48 | } 49 | } 50 | 51 | class Console extends Component { 52 | constructor(props) { 53 | super(props) 54 | this.name = 'Log' 55 | this.mountState = false 56 | this.state = { 57 | logs: [] 58 | } 59 | logStack.attach(() => { 60 | if (this.mountState) { 61 | const logs = logStack.getLogs() 62 | this.setState({ 63 | logs 64 | }) 65 | } 66 | }) 67 | } 68 | 69 | componentDidMount() { 70 | this.mountState = true 71 | this.setState({ 72 | logs: logStack.getLogs() 73 | }) 74 | // 类方法用bind会指向不同地址,导致off失败 75 | event.on('clear', this.clearLogs) 76 | event.on('addLog', this.addLog) 77 | } 78 | 79 | componentWillUnmount() { 80 | this.mountState = false 81 | event.off('clear', this.clearLogs) 82 | event.off('addLog', this.addLog) 83 | } 84 | 85 | shouldComponentUpdate(nextProps, nextState) { 86 | return nextState.logs.length !== this.state.logs.length 87 | } 88 | 89 | addLog = msg => { 90 | logStack.addLog('log', [msg]) 91 | } 92 | 93 | clearLogs = name => { 94 | if (name === this.name) { 95 | logStack.clearLogs() 96 | } 97 | } 98 | 99 | scrollToEnd = () => { 100 | this.flatList.scrollToEnd({ animated: true }) 101 | } 102 | 103 | renderLogItem({ item }) { 104 | return ( 105 | 106 | {item.time} 107 | {item.data} 108 | 109 | ) 110 | } 111 | 112 | render() { 113 | return ( 114 | { 116 | this.flatList = ref 117 | }} 118 | legacyImplementation 119 | // onLayout={() => this.flatList.scrollToEnd({ animated: true })} 120 | // initialNumToRender={20} 121 | showsVerticalScrollIndicator 122 | extraData={this.state} 123 | data={this.state.logs} 124 | renderItem={this.renderLogItem} 125 | ListEmptyComponent={() => Loading...} 126 | keyExtractor={item => item.id} 127 | /> 128 | ) 129 | } 130 | } 131 | 132 | const styles = StyleSheet.create({ 133 | log: { 134 | color: '#000' 135 | }, 136 | warn: { 137 | color: 'orange', 138 | backgroundColor: '#fffacd', 139 | borderColor: '#ffb930' 140 | }, 141 | error: { 142 | color: '#dc143c', 143 | backgroundColor: '#ffe4e1', 144 | borderColor: '#f4a0ab' 145 | }, 146 | logItem: { 147 | borderBottomWidth: StyleSheet.hairlineWidth, 148 | borderColor: '#eee' 149 | }, 150 | logItemText: { 151 | fontSize: 12, 152 | paddingHorizontal: 10, 153 | paddingVertical: 8 154 | }, 155 | logItemTime: { 156 | fontSize: 11, 157 | fontWeight: '700', 158 | textAlign: 'center' 159 | } 160 | }) 161 | 162 | function unixId() { 163 | return Math.round(Math.random() * 1000000).toString(16) 164 | } 165 | 166 | function strLog(logs) { 167 | const arr = logs.map(data => formatLog(data)) 168 | return arr.join(' ') 169 | } 170 | 171 | function formatLog(obj) { 172 | if ( 173 | obj === null || 174 | obj === undefined || 175 | typeof obj === 'string' || 176 | typeof obj === 'number' || 177 | typeof obj === 'boolean' || 178 | typeof obj === 'function' 179 | ) { 180 | return `"${String(obj)}"` 181 | } 182 | if (obj instanceof Date) { 183 | return `Date(${obj.toISOString()})` 184 | } 185 | if (Array.isArray(obj)) { 186 | return `Array(${obj.length})[${obj.map(elem => formatLog(elem))}]` 187 | } 188 | if (obj.toString) { 189 | try { 190 | return `object(${JSON.stringify(obj, null, 2)})` 191 | } catch (err) { 192 | return '非法输入' 193 | } 194 | } 195 | return 'unknown data' 196 | } 197 | 198 | function proxyConsole(console, stack) { 199 | const methods = ['log', 'warn', 'error', 'info'] 200 | methods.forEach(method => { 201 | const fn = console[method] 202 | console[method] = function(...args) { 203 | stack.addLog(method, args) 204 | fn.apply(console, args) 205 | } 206 | }) 207 | } 208 | 209 | module.exports = (function() { 210 | if (!logStack) { 211 | logStack = new LogStack() 212 | } 213 | proxyConsole(global.console, logStack) 214 | return 215 | })() 216 | -------------------------------------------------------------------------------- /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/info.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { View, Text } from 'react-native' 3 | 4 | export default class Info extends Component { 5 | constructor(props) { 6 | super(props) 7 | this.state = { 8 | info: '' 9 | } 10 | } 11 | 12 | componentDidMount() { 13 | let info = this.props.info 14 | if (typeof info === 'object') { 15 | try { 16 | info = JSON.stringify(info, null, 2) 17 | } catch (err) { 18 | console.log(err) 19 | } 20 | } 21 | this.setState({ 22 | info 23 | }) 24 | } 25 | 26 | render() { 27 | return ( 28 | 29 | {this.state.info} 30 | 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/network.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 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 | // ajx 请求类 9 | class AjaxStack { 10 | constructor() { 11 | this.requestIds = [] 12 | this.requests = {} 13 | this.maxLength = 100 14 | this.listeners = [] 15 | this.notify = debounce(500, false, this.notify) 16 | } 17 | 18 | getRequestIds() { 19 | return this.requestIds 20 | } 21 | 22 | getRequests() { 23 | return this.requests 24 | } 25 | 26 | getRequest(id) { 27 | return this.requests[id] || {} 28 | } 29 | 30 | updateRequest(id, data) { 31 | // update item 32 | const item = this.requests[id] || {} 33 | 34 | if (this.requestIds.length > this.maxLength) { 35 | const _id = this.requestIds[0] 36 | this.requestIds = this.requestIds.slice(1) 37 | this.requests[id] && delete this.requests[_id] 38 | } 39 | for (const key in data) { 40 | item[key] = data[key] 41 | } 42 | // update dom 43 | const domData = { 44 | id, 45 | url: item.url, 46 | status: item.status, 47 | method: item.method || '-', 48 | costTime: item.costTime > 0 ? `${item.costTime}ms` : '-', 49 | requestHeader: item.requestHeader || null, 50 | responseHeader: item.responseHeader || null, 51 | getData: item.getData || null, 52 | postData: item.postData || null, 53 | response: null, 54 | actived: !!item.actived 55 | } 56 | switch (item.responseType) { 57 | case '': 58 | case 'text': 59 | // try to parse JSON 60 | if (typeof item.response === 'string') { 61 | try { 62 | domData.response = JSON.parse(item.response) 63 | domData.response = JSON.stringify(domData.response, null, 1) 64 | } catch (e) { 65 | // not a JSON string 66 | domData.response = item.response 67 | } 68 | } else if (typeof item.response !== 'undefined') { 69 | domData.response = Object.prototype.toString.call(item.response) 70 | } 71 | break 72 | case 'json': 73 | if (typeof item.response !== 'undefined') { 74 | domData.response = JSON.stringify(item.response, null, 1) 75 | } 76 | break 77 | case 'blob': 78 | case 'document': 79 | case 'arraybuffer': 80 | default: 81 | if (typeof item.response !== 'undefined') { 82 | domData.response = Object.prototype.toString.call(item.response) 83 | } 84 | break 85 | } 86 | if (item.readyState === 0 || item.readyState === 1) { 87 | domData.status = 'Pending' 88 | } else if (item.readyState === 2 || item.readyState === 3) { 89 | domData.status = 'Loading' 90 | } else if (item.readyState === 4) { 91 | // do nothing 92 | } else { 93 | domData.status = 'Unknown' 94 | } 95 | if (this.requestIds.indexOf(id) === -1) { 96 | this.requestIds.push(id) 97 | } 98 | this.requests[id] = domData 99 | this.notify() 100 | } 101 | 102 | clearRequests() { 103 | this.requestIds = [] 104 | this.requests = {} 105 | this.notify() 106 | } 107 | 108 | notify() { 109 | this.listeners.forEach(callback => { 110 | callback() 111 | }) 112 | } 113 | 114 | attach(callback) { 115 | this.listeners.push(callback) 116 | } 117 | } 118 | 119 | class Network extends Component { 120 | constructor(props) { 121 | super(props) 122 | this.name = 'Network' 123 | this.mountState = false 124 | this.state = { 125 | showingId: null, 126 | requestIds: [], 127 | requests: {} 128 | } 129 | ajaxStack.attach(() => { 130 | if (this.mountState) { 131 | this.setState({ 132 | requestIds: ajaxStack.getRequestIds(), 133 | requests: ajaxStack.getRequests() 134 | }) 135 | } 136 | }) 137 | } 138 | 139 | componentDidMount() { 140 | this.mountState = true 141 | this.setState({ 142 | requestIds: ajaxStack.getRequestIds(), 143 | requests: ajaxStack.getRequests() 144 | }) 145 | event.on('clear', this.clearRequests.bind(this)) 146 | } 147 | 148 | componentWillUnmount() { 149 | this.mountState = false 150 | event.off('clear', this.clearRequests.bind(this)) 151 | } 152 | 153 | shouldComponentUpdate(nextProps, nextState) { 154 | return ( 155 | nextState.requestIds.length !== this.state.requestIds.length || 156 | nextState.showingId || 157 | this.state.showingId 158 | ) 159 | } 160 | 161 | clearRequests(name) { 162 | if (name === this.name) { 163 | ajaxStack.clearRequests() 164 | } 165 | } 166 | 167 | renderHeader() { 168 | const count = Object.keys(this.state.requests).length || 0 169 | return ( 170 | 171 | Name ({count}) 172 | Method 173 | Status 174 | Time 175 | 176 | ) 177 | } 178 | 179 | renderReqItem({ item }) { 180 | const _item = this.state.requests[item] || {} 181 | return ( 182 | 183 | { 185 | this.setState(state => ({ 186 | showingId: state.showingId === _item.id ? null : _item.id 187 | })) 188 | }} 189 | > 190 | = 400 && styles.error 195 | ]} 196 | > 197 | 202 | {_item.url} 203 | 204 | {_item.method} 205 | 206 | {_item.status} 207 | 208 | {_item.costTime} 209 | 210 | 211 | {this.state.showingId === _item.id && ( 212 | 213 | 214 | General 215 | 216 | 217 | {_item.url} 218 | 219 | 220 | 221 | {_item.requestHeader && ( 222 | 223 | Request Header 224 | {Object.keys(_item.requestHeader).map(key => ( 225 | 226 | {key}: 227 | 228 | {_item.requestHeader[key]} 229 | 230 | 231 | ))} 232 | 233 | )} 234 | {_item.getData && ( 235 | 236 | 237 | Query String Parameters 238 | 239 | {Object.keys(_item.getData).map(key => ( 240 | 241 | {key}: 242 | 243 | {_item.getData[key]} 244 | 245 | 246 | ))} 247 | 248 | )} 249 | {_item.postData && ( 250 | 251 | Form Data 252 | {Object.keys(_item.postData).map(key => ( 253 | 254 | {key}: 255 | {_item.postData[key]} 256 | 257 | ))} 258 | 259 | )} 260 | {_item.responseHeader && ( 261 | 262 | Response Header 263 | {Object.keys(_item.responseHeader).map(key => ( 264 | 265 | {key}: 266 | 267 | {_item.responseHeader[key]} 268 | 269 | 270 | ))} 271 | 272 | )} 273 | 274 | Response 275 | 276 | {_item.response || ''} 277 | 278 | 279 | 280 | )} 281 | 282 | ) 283 | } 284 | 285 | render() { 286 | return ( 287 | Loading...} 295 | keyExtractor={item => item} 296 | /> 297 | ) 298 | } 299 | } 300 | 301 | const styles = StyleSheet.create({ 302 | bold: { 303 | fontWeight: '700' 304 | }, 305 | active: { 306 | backgroundColor: '#fffacd' 307 | }, 308 | flex3: { 309 | flex: 3 310 | }, 311 | flex1: { 312 | flex: 1 313 | }, 314 | error: { 315 | backgroundColor: '#ffe4e1', 316 | borderColor: '#ffb930' 317 | }, 318 | nwHeader: { 319 | flexDirection: 'row', 320 | backgroundColor: '#fff' 321 | }, 322 | nwHeaderTitle: { 323 | borderColor: '#eee', 324 | borderWidth: StyleSheet.hairlineWidth, 325 | paddingVertical: 4, 326 | paddingHorizontal: 2 327 | }, 328 | nwItem: {}, 329 | nwItemDetail: { 330 | borderColor: '#eee', 331 | borderLeftWidth: StyleSheet.hairlineWidth, 332 | marginLeft: 15 333 | }, 334 | nwItemDetailHeader: { 335 | paddingLeft: 5, 336 | paddingVertical: 4, 337 | backgroundColor: '#eee' 338 | }, 339 | nwDetailItem: { 340 | paddingLeft: 5 341 | }, 342 | headers: { 343 | flexDirection: 'row', 344 | paddingVertical: 2, 345 | borderBottomWidth: StyleSheet.hairlineWidth, 346 | borderColor: '#ccc' 347 | }, 348 | headerLeft: { 349 | flex: 1, 350 | borderRightWidth: StyleSheet.hairlineWidth, 351 | borderColor: '#ccc' 352 | }, 353 | headerRight: { 354 | flex: 2, 355 | paddingLeft: 10, 356 | color: '#aaa' 357 | } 358 | }) 359 | 360 | function unixId() { 361 | return Math.round(Math.random() * 1000000).toString(16) 362 | } 363 | 364 | function proxyAjax(XHR, stack) { 365 | if (!XHR) { 366 | return 367 | } 368 | const _open = XHR.prototype.open 369 | const _send = XHR.prototype.send 370 | this._open = _open 371 | this._send = _send 372 | 373 | // mock open() 374 | XHR.prototype.open = function(...args) { 375 | const XMLReq = this 376 | const method = args[0] 377 | const url = args[1] 378 | const id = unixId() 379 | let timer = null 380 | 381 | // may be used by other functions 382 | XMLReq._requestID = id 383 | XMLReq._method = method 384 | XMLReq._url = url 385 | 386 | // mock onreadystatechange 387 | const _onreadystatechange = XMLReq.onreadystatechange || function() {} 388 | const onreadystatechange = function() { 389 | const item = stack.getRequest(id) 390 | // update status 391 | item.readyState = XMLReq.readyState 392 | item.status = 0 393 | if (XMLReq.readyState > 1) { 394 | item.status = XMLReq.status 395 | } 396 | item.responseType = XMLReq.responseType 397 | 398 | if (XMLReq.readyState === 0) { 399 | // UNSENT 400 | if (!item.startTime) { 401 | item.startTime = +new Date() 402 | } 403 | } else if (XMLReq.readyState === 1) { 404 | // OPENED 405 | if (!item.startTime) { 406 | item.startTime = +new Date() 407 | } 408 | } else if (XMLReq.readyState === 2) { 409 | // HEADERS_RECEIVED 410 | item.responseHeader = {} 411 | const arr = headers.trim().split(/[\r\n]+/) 412 | 413 | // Create a map of header names to values 414 | arr.forEach(line => { 415 | const parts = line.split(': ') 416 | const header = parts.shift() 417 | const value = parts.join(': ') 418 | item.responseHeader[header] = value 419 | }) 420 | } else if (XMLReq.readyState === 3) { 421 | // LOADING 422 | } else if (XMLReq.readyState === 4) { 423 | // DONE 424 | clearInterval(timer) 425 | item.endTime = +new Date() 426 | item.costTime = item.endTime - (item.startTime || item.endTime) 427 | item.response = XMLReq.response 428 | } else { 429 | clearInterval(timer) 430 | } 431 | 432 | if (!XMLReq._noVConsole) { 433 | stack.updateRequest(id, item) 434 | } 435 | return _onreadystatechange.apply(XMLReq, args) 436 | } 437 | XMLReq.onreadystatechange = onreadystatechange 438 | 439 | // some 3rd libraries will change XHR's default function 440 | // so we use a timer to avoid lost tracking of readyState 441 | let preState = -1 442 | timer = setInterval(() => { 443 | if (preState !== XMLReq.readyState) { 444 | preState = XMLReq.readyState 445 | onreadystatechange.call(XMLReq) 446 | } 447 | }, 10) 448 | 449 | return _open.apply(XMLReq, args) 450 | } 451 | 452 | // mock send() 453 | XHR.prototype.send = function(...args) { 454 | const XMLReq = this 455 | const data = args[0] 456 | 457 | const item = stack.getRequest(XMLReq._requestID) 458 | item.method = XMLReq._method.toUpperCase() 459 | let query = XMLReq._url.split('?') // a.php?b=c&d=?e => ['a.php', 'b=c&d=', '?e'] 460 | item.url = query.shift() // => ['b=c&d=', '?e'] 461 | 462 | if (query.length > 0) { 463 | item.getData = {} 464 | query = query.join('?') // => 'b=c&d=?e' 465 | query = query.split('&') // => ['b=c', 'd=?e'] 466 | for (let q of query) { 467 | q = q.split('=') 468 | item.getData[q[0]] = decodeURIComponent(q[1]) 469 | } 470 | } 471 | 472 | if (XMLReq._headers) { 473 | item.requestHeader = XMLReq._headers 474 | } 475 | 476 | if (item.method === 'POST') { 477 | // save POST data 478 | if (typeof data === 'string') { 479 | const arr = data.split('&') 480 | item.postData = {} 481 | for (let q of arr) { 482 | q = q.split('=') 483 | item.postData[q[0]] = q[1] 484 | } 485 | } else { 486 | item.postData = data 487 | } 488 | } 489 | 490 | if (!XMLReq._noVConsole) { 491 | stack.updateRequest(XMLReq._requestID, item) 492 | } 493 | 494 | return _send.apply(XMLReq, args) 495 | } 496 | } 497 | 498 | module.exports = (function() { 499 | if (!ajaxStack) { 500 | ajaxStack = new AjaxStack() 501 | } 502 | proxyAjax(global.originalXMLHttpRequest || global.XMLHttpRequest, ajaxStack) 503 | return 504 | })() 505 | -------------------------------------------------------------------------------- /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 === undefined && elapsed > delay) { 32 | exec() 33 | } else if (noTrailing !== true) { 34 | timeoutID = setTimeout( 35 | debounceMode ? clear : exec, 36 | debounceMode === undefined ? delay - elapsed : delay 37 | ) 38 | } 39 | } 40 | 41 | return wrapper 42 | } 43 | 44 | function debounce(delay, atBegin, callback) { 45 | return callback === undefined 46 | ? throttle(delay, atBegin, false) 47 | : throttle(delay, callback, atBegin !== false) 48 | } 49 | 50 | module.exports = { 51 | throttle, 52 | debounce 53 | } 54 | --------------------------------------------------------------------------------