├── .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 | [](https://www.npmjs.org/package/react-native-vdebug)
5 | [](https://npmcharts.com/compare/react-native-vdebug?minimal=true)
6 | [](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 |
--------------------------------------------------------------------------------