From a9b5fa11fd8679acdbb7168b446fef6e0d00e4a5 Mon Sep 17 00:00:00 2001 From: Maiz Date: Wed, 16 Mar 2022 10:41:03 +0800 Subject: [PATCH 01/11] fix(Core): Remove debug logs. --- package.json | 2 +- src/core/core.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 42c4b402..fab5bb61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vconsole", - "version": "3.13.0", + "version": "3.13.1-rc", "description": "A lightweight, extendable front-end developer tool for mobile web page.", "homepage": "https://github.com/Tencent/vConsole", "files": [ diff --git a/src/core/core.ts b/src/core/core.ts index 86dad372..7e597bfc 100644 --- a/src/core/core.ts +++ b/src/core/core.ts @@ -282,7 +282,6 @@ export class VConsole { eventName = 'on' + eventName.charAt(0).toUpperCase() + eventName.slice(1); if (tool.isFunction(this.option[eventName])) { setTimeout(() => { - console.log('triggerEvent', eventName); this.option[eventName].apply(this, param); }, 0); } From 271b39e1faf99e8dad2adbb5de4b6ff05dcb7a19 Mon Sep 17 00:00:00 2001 From: Maiz Date: Thu, 17 Mar 2022 16:18:27 +0800 Subject: [PATCH 02/11] Refactor(Network): Now network records will be more accurate by using Proxy to prevent XMLHttpRequest overwriting by other request libraries (like Axios). --- CHANGELOG.md | 5 + dev/network.html | 73 +++++++++++++- src/network/network.model.ts | 190 ++++++----------------------------- src/network/requestItem.ts | 104 ++++++++++++++++++- src/network/xhr.proxy.ts | 154 ++++++++++++++++++++++++++++ webpack.serve.config.js | 50 ++++++--- 6 files changed, 400 insertions(+), 176 deletions(-) create mode 100644 src/network/xhr.proxy.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f88bb55a..5de4be29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ English | [简体中文](./CHANGELOG_CN.md) +## 3.14.0-rc (2022-03-??) + +- `Refactor(Network)` Now network records will be more accurate by using Proxy to prevent XMLHttpRequest overwriting by other request libraries (like Axios). + + ## 3.13.0 (2022-03-15) - `Feat(Log)` Add new option `log.showTimestames`, see [Public Properties & Methods](./doc/public_properties_methods.md). diff --git a/dev/network.html b/dev/network.html index 09ff2056..45295d30 100644 --- a/dev/network.html +++ b/dev/network.html @@ -7,8 +7,8 @@ - +
@@ -256,6 +258,75 @@ }); } +function fetchStream() { + window.fetch('./data/stream.flv?id=' + Math.random()).then((response) => { + console.log(response instanceof window.Response); + return; + console.log('then response', 'bodyUsed:', response.bodyUsed, 'locked:', response.body.locked); + const reader = response.body.getReader(); + console.log('then response', 'bodyUsed:', response.bodyUsed, 'locked:', response.body.locked); + let bytesReceived = 0; + + return reader.read().then(function process(result) { + console.log('reader.read', 'bodyUsed:', response.bodyUsed, 'locked:', response.body.locked); + if (result.done) { + console.log('Failed to find match'); + return; + } + + bytesReceived += result.value.length; + console.log(`Received ${bytesReceived} bytes.`); + + if (bytesReceived > 3000000) { + reader.cancel(); + console.log('Cancel.', response.status); + return; + } + + return reader.read().then(process); + }); + }); +} + +function xhrStream() { + vConsole.show(); + const url = './data/stream.flv?id=' + Math.random(); + const xhr = new XMLHttpRequest(); + xhr.timeout = 1000; + console.log('xhr:', xhr); + console.log('xhr type:', typeof xhr, xhr instanceof XMLHttpRequest); + xhr.open('GET', url); + xhr.send(); + xhr.onreadystatechange = () => { + console.log('XHR onreadystatechange:', xhr.readyState); + }; + xhr.onprogress = (e) => { + // console.log('XHR onprogress:', 'readyState:', xhr.readyState, 'status:', xhr.status, 'loaded:', e.loaded, 'timeStamp:', e.timeStamp); + // console.log('XHR onprogress state:', xhr.readyState, xhr.status); + if (e.loaded > 3000000) { + xhr.abort(); + } + }; + xhr.onloadstart = (e) => { + // console.log('XHR onloadstart:', e); + }; + xhr.onloadend = (e) => { + // console.log('XHR onloadend:', 'readyState:', xhr.readyState, xhr.status, e); + }; + xhr.onload = (e) => { + // console.log('XHR onload:', 'readyState:', xhr.readyState, xhr.status, e); + }; + xhr.onerror = (e) => { + console.log('XHR onerror:', e); + }; + xhr.onabort = (e) => { + console.log('XHR onabort:', xhr.readyState, xhr.status, e); + }; + xhr.ontimeout = (e) => { + console.log('XHR ontimeout:', e); + } +} + function postImage() { console.info('postImage() Start, response should be logged after End'); const xhr = new XMLHttpRequest(); diff --git a/src/network/network.model.ts b/src/network/network.model.ts index d40aed62..d8e9ce08 100644 --- a/src/network/network.model.ts +++ b/src/network/network.model.ts @@ -3,6 +3,7 @@ import * as tool from '../lib/tool'; import { VConsoleModel } from '../lib/model'; import { contentStore } from '../core/core.model'; import { VConsoleNetworkRequestItem, RequestItemHelper } from './requestItem'; +import { XHRProxy } from './xhr.proxy'; import type { VConsoleRequestMethod } from './requestItem'; @@ -19,9 +20,6 @@ export class VConsoleNetworkModel extends VConsoleModel { public maxNetworkNumber: number = 1000; protected itemCounter: number = 0; - private _xhrOpen: XMLHttpRequest['open'] = undefined; // the origin function - private _xhrSend: XMLHttpRequest['send'] = undefined; - private _xhrSetRequestHeader: XMLHttpRequest['setRequestHeader'] = undefined; private _fetch: WindowOrWorkerGlobalScope['fetch'] = undefined; private _sendBeacon: Navigator['sendBeacon'] = undefined; @@ -36,12 +34,7 @@ export class VConsoleNetworkModel extends VConsoleModel { public unMock() { // recover original functions if (window.XMLHttpRequest) { - window.XMLHttpRequest.prototype.open = this._xhrOpen; - window.XMLHttpRequest.prototype.send = this._xhrSend; - window.XMLHttpRequest.prototype.setRequestHeader = this._xhrSetRequestHeader; - this._xhrOpen = undefined; - this._xhrSend = undefined; - this._xhrSetRequestHeader = undefined; + window.XMLHttpRequest = XHRProxy.origXMLHttpRequest; } if (window.fetch) { window.fetch = this._fetch; @@ -90,153 +83,10 @@ export class VConsoleNetworkModel extends VConsoleModel { const _XMLHttpRequest = window.XMLHttpRequest; if (!_XMLHttpRequest) { return; } - const that = this; - const _open = window.XMLHttpRequest.prototype.open, - _send = window.XMLHttpRequest.prototype.send, - _setRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader; - that._xhrOpen = _open; - that._xhrSend = _send; - that._xhrSetRequestHeader = _setRequestHeader; - - // mock open() - window.XMLHttpRequest.prototype.open = function() { - const XMLReq: XMLHttpRequest = this; - const args = [].slice.call(arguments), - method = args[0], - url = args[1]; - const item = new VConsoleNetworkRequestItem(); - let timer = null; - - // may be used by other functions - (XMLReq)._requestID = item.id; - (XMLReq)._method = method; - (XMLReq)._url = url; - - // mock onReadyStateChange - const _onreadystatechange = (XMLReq)._origOnreadystatechange || XMLReq.onreadystatechange || function() {}; - const onreadystatechange = function() { - - // update status - item.readyState = XMLReq.readyState; - item.responseType = XMLReq.responseType; - item.requestType = 'xhr'; - - // update data by readyState - switch (XMLReq.readyState) { - case 0: // UNSENT - item.status = 0; - item.statusText = 'Pending'; - if (!item.startTime) { - item.startTime = (+new Date()); - } - break; - - case 1: // OPENED - item.status = 0; - item.statusText = 'Pending'; - if (!item.startTime) { - item.startTime = (+new Date()); - } - break; - - case 2: // HEADERS_RECEIVED - item.status = XMLReq.status; - item.statusText = 'Loading'; - item.header = {}; - const header = XMLReq.getAllResponseHeaders() || '', - headerArr = header.split('\n'); - // extract plain text to key-value format - for (let i = 0; i < headerArr.length; i++) { - const line = headerArr[i]; - if (!line) { continue; } - const arr = line.split(': '); - const key = arr[0], - value = arr.slice(1).join(': '); - item.header[key] = value; - } - break; - - case 3: // LOADING - item.status = XMLReq.status; - item.statusText = 'Loading'; - break; - - case 4: // DONE - clearInterval(timer); - item.status = XMLReq.status; - item.statusText = String(XMLReq.status); // show status code when request completed - item.endTime = +new Date(), - item.costTime = item.endTime - (item.startTime || item.endTime); - item.response = XMLReq.response; - break; - - default: - clearInterval(timer); - item.status = XMLReq.status; - item.statusText = 'Unknown'; - break; - } - - // update response by responseType - item.response = RequestItemHelper.genResonseByResponseType(item.responseType, item.response); - - if (!(XMLReq)._noVConsole) { - that.updateRequest(item.id, item); - } - return _onreadystatechange.apply(XMLReq, arguments); - }; - XMLReq.onreadystatechange = onreadystatechange; - // when the XHR object is reused, we can still call the original function while it is overwrote. (issue #214) - (XMLReq)._origOnreadystatechange = _onreadystatechange; - - // some 3rd-libraries will change XHR's default function - // so we use a timer to avoid lost tracking of readyState - let preState = -1; - timer = setInterval(function() { - if (preState != XMLReq.readyState) { - preState = XMLReq.readyState; - onreadystatechange.call(XMLReq); - } - }, 10); - - return _open.apply(XMLReq, args); - }; - - // mock setRequestHeader() - window.XMLHttpRequest.prototype.setRequestHeader = function() { - const XMLReq = this; - const args = [].slice.call(arguments); - - const reqList = get(requestList); - const item = reqList[XMLReq._requestID]; - if (item) { - if (!item.requestHeader) { item.requestHeader = {}; } - item.requestHeader[args[0]] = args[1]; - } - return _setRequestHeader.apply(XMLReq, args); - }; - - // mock send() - window.XMLHttpRequest.prototype.send = function() { - const XMLReq: XMLHttpRequest = this; - const args = [].slice.call(arguments), - data: XMLHttpRequestBodyInit = args[0]; - const { _requestID, _url, _method } = XMLReq; - - const reqList = get(requestList); - const item = reqList[_requestID] || new VConsoleNetworkRequestItem(); - item.method = _method ? _method.toUpperCase() : 'GET'; - item.url = _url || ''; - item.name = item.url.replace(new RegExp('[/]*$'), '').split('/').pop() || ''; - item.getData = RequestItemHelper.genGetDataByUrl(item.url, {}); - item.postData = that.getFormattedBody(data); - - if (!(XMLReq)._noVConsole) { - that.updateRequest(item.id, item); - } - - return _send.apply(XMLReq, args); - }; + window.XMLHttpRequest = XHRProxy.create((item: VConsoleNetworkRequestItem) => { + this.updateRequest(item.id, item); + }); + }; @@ -314,8 +164,28 @@ export class VConsoleNetworkModel extends VConsoleModel { return _fetch(request, init).then((res) => { // fix ios<11 https://github.com/github/fetch/issues/504 - const response = res.clone(); - _fetchReponse = response.clone(); + const response = res; + _fetchReponse = res; + // const response = new Proxy(res, { + // // set(target, name, value) { + // // console.log('set', name); + // // return Reflect.set(target, name, value); + // // }, + // get(target, name) { + // console.log('get', name) + // if (typeof target[name] === 'function') { + // if (name === 'cancel') { + + // } + // } + // return Reflect.get(target, name); + // }, + // apply(target, thisArg, argumentsList) { + // console.log('apply:', target.name); + // return target(...argumentsList); + // } + // }); + // _fetchReponse = res; // (window as any)._vcOrigConsole.log('_fetch', _fetchReponse); item.endTime = +new Date(); @@ -333,10 +203,10 @@ export class VConsoleNetworkModel extends VConsoleModel { const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { item.responseType = 'json'; - return response.clone().text(); + return response.text(); } else if (contentType && (contentType.includes('text/html') || contentType.includes('text/plain'))) { item.responseType = 'text'; - return response.clone().text(); + return response.text(); } else { item.responseType = ''; return '[object Object]'; diff --git a/src/network/requestItem.ts b/src/network/requestItem.ts index 7a6da625..94a233d3 100644 --- a/src/network/requestItem.ts +++ b/src/network/requestItem.ts @@ -7,8 +7,9 @@ export class VConsoleNetworkRequestItem { name?: string = ''; method: VConsoleRequestMethod = ''; url: string = ''; - status: number | string = 0; - statusText?: string = ''; + status: number | string = 0; // HTTP status codes + statusText?: string = ''; // for display + cancelState?: 0 | 1 | 2 | 3 = 0; // 0=no cancel; 1=abort (for XHR); 2=cancel (for Fetch); 3=timeout; readyState?: XMLHttpRequest['readyState'] = 0; header: { [key: string]: string } = null; // response header responseType: XMLHttpRequest['responseType'] = ''; @@ -21,6 +22,7 @@ export class VConsoleNetworkRequestItem { getData: { [key: string]: string } = null; postData: { [key: string]: string } | string = null; actived: boolean = false; + noVConsole?: boolean = false; constructor() { this.id = tool.getUniqueID(); @@ -148,4 +150,102 @@ export const RequestItemHelper = { } return ret; }, + + /** + * Update item's properties according to readyState. + */ + updateItemByReadyState(item: VConsoleNetworkRequestItem, XMLReq: XMLHttpRequest) { + switch (XMLReq.readyState) { + case 0: // UNSENT + item.status = 0; + item.statusText = 'Pending'; + if (!item.startTime) { + item.startTime = (+new Date()); + } + break; + + case 1: // OPENED + item.status = 0; + item.statusText = 'Pending'; + if (!item.startTime) { + item.startTime = (+new Date()); + } + break; + + case 2: // HEADERS_RECEIVED + item.status = XMLReq.status; + item.statusText = 'Loading'; + item.header = {}; + const header = XMLReq.getAllResponseHeaders() || '', + headerArr = header.split('\n'); + // extract plain text to key-value format + for (let i = 0; i < headerArr.length; i++) { + const line = headerArr[i]; + if (!line) { continue; } + const arr = line.split(': '); + const key = arr[0], + value = arr.slice(1).join(': '); + item.header[key] = value; + } + break; + + case 3: // LOADING + item.status = XMLReq.status; + item.statusText = 'Loading'; + break; + + case 4: // DONE + // clearInterval(timer); + // `XMLReq.abort()` will change `status` from 200 to 0, so use previous value in this case + item.status = XMLReq.status || item.status || 0; + item.statusText = String(item.status); // show status code when request completed + item.endTime = Date.now(), + item.costTime = item.endTime - (item.startTime || item.endTime); + item.response = XMLReq.response; + break; + + default: + // clearInterval(timer); + item.status = XMLReq.status; + item.statusText = 'Unknown'; + break; + } + }, + + /** + * Generate formatted response body by XMLHttpRequestBodyInit. + */ + genFormattedBody(body?: BodyInit) { + if (!body) { return null; } + let ret: string | { [key: string]: string } = null; + + if (typeof body === 'string') { + try { // '{a:1}' => try to parse as json + ret = JSON.parse(body); + } catch (e) { // 'a=1&b=2' => try to parse as query + const arr = body.split('&'); + if (arr.length === 1) { // not a query, parse as original string + ret = body; + } else { // 'a=1&b=2&c' => parse as query + ret = {}; + for (let q of arr) { + const kv = q.split('='); + ret[ kv[0] ] = kv[1] === undefined ? 'undefined' : kv[1]; + } + } + } + } else if (tool.isIterable(body)) { + // FormData or URLSearchParams or Array + ret = {}; + for (const [key, value] of body) { + ret[key] = typeof value === 'string' ? value : '[object Object]'; + } + } else if (tool.isPlainObject(body)) { + ret = body; + } else { + const type = tool.getPrototypeName(body); + ret = `[object ${type}]`; + } + return ret; + }, }; diff --git a/src/network/xhr.proxy.ts b/src/network/xhr.proxy.ts new file mode 100644 index 00000000..b3b8183d --- /dev/null +++ b/src/network/xhr.proxy.ts @@ -0,0 +1,154 @@ +import { VConsoleNetworkRequestItem, RequestItemHelper } from './requestItem'; + +type IOnUpdateCallback = (item: VConsoleNetworkRequestItem) => void; + +export class XHRProxyHandler implements ProxyHandler { + public XMLReq: XMLHttpRequest; + public item: VConsoleNetworkRequestItem; + protected onUpdateCallback: IOnUpdateCallback; + + constructor(XMLReq: XMLHttpRequest, onUpdateCallback: IOnUpdateCallback) { + this.XMLReq = XMLReq; + this.XMLReq.onreadystatechange = () => { this.onReadyStateChange() }; + this.XMLReq.onabort = () => { this.onAbort() }; + this.XMLReq.ontimeout = () => { this.onTimeout() }; + this.item = new VConsoleNetworkRequestItem(); + this.item.requestType = 'xhr'; + this.onUpdateCallback = onUpdateCallback; + } + + public get(target: T, key: string) { + // if (typeof key === 'string') { console.log('Proxy get:', typeof key, key); } + switch (key) { + case 'open': + return this.getOpen(target); + + case 'send': + return this.getSend(target); + + default: + if (typeof target[key] === 'function') { + return (...args) => { + return target[key].apply(target, args); + }; + } else { + return Reflect.get(target, key); + } + } + } + + public set(target: T, key: string, value: any) { + // if (typeof key === 'string') { console.log('Proxy set:', typeof key, key); } + switch (key) { + case '_noVConsole': + this.item.noVConsole = !!value; + return; + + case 'onreadystatechange': + return this.setOnReadyStateChange(target, key, value); + + case 'onabort': + return this.setOnAbort(target, key, value); + + case 'ontimeout': + return this.setOnTimeout(target, key, value); + + default: + // do nothing + } + return Reflect.set(target, key, value); + } + + public onReadyStateChange() { + // console.log('Proxy onReadyStateChange()') + this.item.readyState = this.XMLReq.readyState; + this.item.responseType = this.XMLReq.responseType; + this.item.endTime = Date.now(); + this.item.costTime = this.item.endTime - this.item.startTime; + + // update data by readyState + RequestItemHelper.updateItemByReadyState(this.item, this.XMLReq); + + // update response by responseType + this.item.response = RequestItemHelper.genResonseByResponseType(this.item.responseType, this.item.response); + + this.triggerUpdate(); + } + + public onAbort() { + // console.log('Proxy onAbort()') + this.item.cancelState = 1; + this.item.statusText = 'Abort'; + this.triggerUpdate(); + } + + public onTimeout() { + this.item.cancelState = 3; + this.item.statusText = 'Timeout'; + this.triggerUpdate(); + } + + protected triggerUpdate() { + if (!this.item.noVConsole) { + this.onUpdateCallback(this.item); + } + } + + protected getOpen(target) { + return (...args) => { + // console.log('Proxy open()'); + const method = args[0]; + const url = args[1]; + this.item.method = method ? method.toUpperCase() : 'GET'; + this.item.url = url || ''; + this.item.name = this.item.url.replace(new RegExp('[/]*$'), '').split('/').pop() || ''; + this.item.getData = RequestItemHelper.genGetDataByUrl(this.item.url, {}); + this.triggerUpdate(); + return target.open.apply(target, args); + }; + } + + protected getSend(target) { + return (...args) => { + // console.log('Proxy send()'); + const data: XMLHttpRequestBodyInit = args[0]; + this.item.postData = RequestItemHelper.genFormattedBody(data); + this.triggerUpdate(); + return target.send.apply(target, args); + }; + } + + protected setOnReadyStateChange(target: T, key: string, value) { + return Reflect.set(target, key, (...args) => { + this.onReadyStateChange(); + value.apply(target, args); + }); + } + + protected setOnAbort(target: T, key: string, value) { + return Reflect.set(target, key, (...args) => { + this.onAbort(); + value.apply(target, args); + }); + } + + protected setOnTimeout(target: T, key: string, value) { + return Reflect.set(target, key, (...args) => { + this.onTimeout(); + value.apply(target, args); + }); + } +} + +export class XHRProxy extends XMLHttpRequest { + public static origXMLHttpRequest = XMLHttpRequest; + + public static create(onUpdateCallback: IOnUpdateCallback) { + return new Proxy(XMLHttpRequest, { + construct(ctor) { + const XMLReq = new ctor(); + return new Proxy(XMLReq, new XHRProxyHandler(XMLReq, onUpdateCallback)); + }, + }); + } +} \ No newline at end of file diff --git a/webpack.serve.config.js b/webpack.serve.config.js index 4213d4dc..3bb8ed04 100644 --- a/webpack.serve.config.js +++ b/webpack.serve.config.js @@ -25,19 +25,43 @@ module.exports = (env, argv) => { ], onBeforeSetupMiddleware(devServer) { devServer.app.all('*', (req, res) => { - const delay = req.query.t || Math.ceil(Math.random() * 100); - setTimeout(() => { - res.status(req.query.s || 200); - const filePath = Path.join(contentBase, req.path); - try { - fs.accessSync(filePath, fs.constants.F_OK); - // res.send(fs.readFileSync(filePath)); - res.sendFile(filePath); - } catch (e) { - res.end(); - } - // console.log(req.query); - }, delay); + if (req.path.includes('.flv')) { + res.set({ + 'Content-Type': 'video/x-flv', + // 'Content-Type', 'application/octet-stream', + 'Transfer-Encoding': 'chunked', + 'Connection': 'keep-alive', + }); + let n = 0; + const write = () => { + setTimeout(() => { + n++; + const buf = Buffer.alloc(100000, 1); + res.write(buf); + if (n < 100) { + write(); + } else { + res.end(); + } + }, 100); + }; + write(); + + } else { + const delay = req.query.t || Math.ceil(Math.random() * 100); + setTimeout(() => { + res.status(req.query.s || 200); + const filePath = Path.join(contentBase, req.path); + try { + fs.accessSync(filePath, fs.constants.F_OK); + // res.send(fs.readFileSync(filePath)); + res.sendFile(filePath); + } catch (e) { + res.end(); + } + // console.log(req.query); + }, delay); + } }); } }, From 70c61dafd3017ddab7e7b9ab6f9ab3b692e09ff2 Mon Sep 17 00:00:00 2001 From: Maiz Date: Thu, 17 Mar 2022 17:02:57 +0800 Subject: [PATCH 03/11] Feat(Network): Improve rendering performance of large Response data by cropping the displayed response content. --- CHANGELOG.md | 1 + src/core/style/theme.less | 3 +++ src/lib/tool.ts | 11 +++++++++++ src/network/requestItem.ts | 6 ++++-- src/network/xhr.proxy.ts | 2 +- 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5de4be29..4966ad79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ English | [简体中文](./CHANGELOG_CN.md) ## 3.14.0-rc (2022-03-??) +- `Feat(Network)` Improve rendering performance of large Response data by cropping the displayed response content. - `Refactor(Network)` Now network records will be more accurate by using Proxy to prevent XMLHttpRequest overwriting by other request libraries (like Axios). diff --git a/src/core/style/theme.less b/src/core/style/theme.less index 91858873..f5d81a44 100644 --- a/src/core/style/theme.less +++ b/src/core/style/theme.less @@ -66,6 +66,9 @@ dd, dl, pre { margin: 0; } + pre { + white-space: pre-wrap; + } i { font-style: normal; diff --git a/src/lib/tool.ts b/src/lib/tool.ts index 7b007a29..2961cc27 100644 --- a/src/lib/tool.ts +++ b/src/lib/tool.ts @@ -376,6 +376,17 @@ export function subString(str: string, len: number) { return str; } +/** + * Get a string within a limited max length. + */ +export function getStringWithinLength(str: string, maxLen: number) { + const bytes = getStringBytes(str); + if (bytes > maxLen) { + str = subString(str, maxLen) + `...(${getBytesText(bytes)})`; + } + return str; +} + const _sortArrayCompareFn = (a: T, b: T) => { return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' }); }; diff --git a/src/network/requestItem.ts b/src/network/requestItem.ts index 94a233d3..35f70d0f 100644 --- a/src/network/requestItem.ts +++ b/src/network/requestItem.ts @@ -118,7 +118,7 @@ export const RequestItemHelper = { * Generate formatted response data by responseType. */ genResonseByResponseType(responseType: string, response) { - let ret; + let ret = ''; switch (responseType) { case '': case 'text': @@ -130,12 +130,13 @@ export const RequestItemHelper = { ret = tool.safeJSONStringify(ret, { maxDepth: 10, keyMaxLen: 500000, pretty: true }); } catch (e) { // not a JSON string - ret = response; + ret = tool.getStringWithinLength(String(response), 500000); } } else if (tool.isObject(response) || tool.isArray(response)) { ret = tool.safeJSONStringify(response, { maxDepth: 10, keyMaxLen: 500000, pretty: true }); } else if (typeof response !== 'undefined') { ret = Object.prototype.toString.call(response); + ret = tool.getStringWithinLength(ret, 500000); } break; @@ -145,6 +146,7 @@ export const RequestItemHelper = { default: if (typeof response !== 'undefined') { ret = Object.prototype.toString.call(response); + ret = tool.getStringWithinLength(ret, 500000); } break; } diff --git a/src/network/xhr.proxy.ts b/src/network/xhr.proxy.ts index b3b8183d..ceb42697 100644 --- a/src/network/xhr.proxy.ts +++ b/src/network/xhr.proxy.ts @@ -140,7 +140,7 @@ export class XHRProxyHandler implements ProxyHandler Date: Fri, 18 Mar 2022 12:22:23 +0800 Subject: [PATCH 04/11] Refactor(Network): Use proxy on fetch(). --- CHANGELOG.md | 2 +- dev/network.html | 21 ++-- src/network/fetch.proxy.ts | 207 ++++++++++++++++++++++++++++++++++ src/network/network.model.ts | 208 +++-------------------------------- src/network/requestItem.ts | 13 +++ 5 files changed, 247 insertions(+), 204 deletions(-) create mode 100644 src/network/fetch.proxy.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4966ad79..8bf4b8d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ English | [简体中文](./CHANGELOG_CN.md) ## 3.14.0-rc (2022-03-??) - `Feat(Network)` Improve rendering performance of large Response data by cropping the displayed response content. -- `Refactor(Network)` Now network records will be more accurate by using Proxy to prevent XMLHttpRequest overwriting by other request libraries (like Axios). +- `Refactor(Network)` Now network records will be more accurate by using Proxy to prevent `XMLHttpRequest | fetch` overwriting by other request libraries (like Axios). ## 3.13.0 (2022-03-15) diff --git a/dev/network.html b/dev/network.html index 45295d30..280689ae 100644 --- a/dev/network.html +++ b/dev/network.html @@ -133,6 +133,7 @@ } function getFetch() { + vConsole.show(); window.fetch('./data/success.json?method=fetchGet&id=' + Math.random(), { method: 'GET', headers: { @@ -140,9 +141,11 @@ 'content-type': 'application/json' }, }).then((data) => { + console.log('getFetch() response:', data); + // return data; return data.json(); }).then((data) => { - console.log('get Fetch Response:', data); + console.log('getFetch() json:', data); }); } @@ -163,7 +166,6 @@ }); } - function getFetchSimple() { window.fetch('./data/large.json?type=fetchGet&id=' + Math.random()).then((data) => { return data.json(); @@ -259,10 +261,11 @@ } function fetchStream() { + vConsole.show(); window.fetch('./data/stream.flv?id=' + Math.random()).then((response) => { - console.log(response instanceof window.Response); - return; console.log('then response', 'bodyUsed:', response.bodyUsed, 'locked:', response.body.locked); + // console.log(response.text()) + // return; const reader = response.body.getReader(); console.log('then response', 'bodyUsed:', response.bodyUsed, 'locked:', response.body.locked); let bytesReceived = 0; @@ -292,19 +295,17 @@ vConsole.show(); const url = './data/stream.flv?id=' + Math.random(); const xhr = new XMLHttpRequest(); - xhr.timeout = 1000; - console.log('xhr:', xhr); + xhr.timeout = 11000; console.log('xhr type:', typeof xhr, xhr instanceof XMLHttpRequest); xhr.open('GET', url); xhr.send(); xhr.onreadystatechange = () => { - console.log('XHR onreadystatechange:', xhr.readyState); + console.log('XHR onreadystatechange:', 'readyState:', xhr.readyState, 'responseType:', xhr.responseType); }; xhr.onprogress = (e) => { // console.log('XHR onprogress:', 'readyState:', xhr.readyState, 'status:', xhr.status, 'loaded:', e.loaded, 'timeStamp:', e.timeStamp); - // console.log('XHR onprogress state:', xhr.readyState, xhr.status); if (e.loaded > 3000000) { - xhr.abort(); + // xhr.abort(); } }; xhr.onloadstart = (e) => { @@ -314,7 +315,7 @@ // console.log('XHR onloadend:', 'readyState:', xhr.readyState, xhr.status, e); }; xhr.onload = (e) => { - // console.log('XHR onload:', 'readyState:', xhr.readyState, xhr.status, e); + console.log('XHR onload:', 'readyState:', xhr.readyState, xhr.status, xhr.responseType); }; xhr.onerror = (e) => { console.log('XHR onerror:', e); diff --git a/src/network/fetch.proxy.ts b/src/network/fetch.proxy.ts new file mode 100644 index 00000000..e393a0d5 --- /dev/null +++ b/src/network/fetch.proxy.ts @@ -0,0 +1,207 @@ +import * as tool from '../lib/tool'; +import { VConsoleNetworkRequestItem, RequestItemHelper } from './requestItem'; +import type { VConsoleRequestMethod } from './requestItem'; + +type IOnUpdateCallback = (item: VConsoleNetworkRequestItem) => void; + +export class ResponseProxyHandler implements ProxyHandler { + public resp: Response; + public item: VConsoleNetworkRequestItem; + protected onUpdateCallback: IOnUpdateCallback; + + constructor(resp: T, item: VConsoleNetworkRequestItem, onUpdateCallback: IOnUpdateCallback) { + // console.log('Proxy: new constructor') + this.resp = resp; + this.item = item; + this.onUpdateCallback = onUpdateCallback; + this.mockReader(); + } + + public set(target, key, value) { + // if (typeof key === 'string') { console.log('Proxy set:', key) } + return Reflect.set(target, key, value); + } + + public get(target, key) { + // if (typeof key === 'string') { console.log('Proxy get:', key) } + switch (key) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + return () => { + this.item.responseType = key.toLowerCase(); + return target[key]().then((val) => { + this.item.response = RequestItemHelper.genResonseByResponseType(this.item.responseType, val); + this.onUpdateCallback(this.item); + return val; + }); + }; + } + if (typeof target[key] === 'function') { + return (...args) => { + return target[key].apply(target, args); + }; + } else { + return Reflect.get(target, key); + } + } + + protected mockReader() { + let readerReceivedValue = new Uint8Array(); + const _getReader = this.resp.body.getReader; + this.resp.body.getReader = () => { + const reader = >_getReader.apply(this.resp.body); + const _read = reader.read; + const _cancel = reader.cancel; + this.item.responseType = 'arraybuffer'; + + reader.read = () => { + return (>_read.apply(reader)).then((result) => { + this.item.endTime = Date.now(); + this.item.costTime = this.item.endTime - (this.item.startTime || this.item.endTime); + this.item.readyState = result.done ? 4 : 3; + this.item.statusText = result.done ? String(this.item.status) : 'Loading'; + readerReceivedValue = new Uint8Array([...readerReceivedValue, ...result.value]); + if (result.done) { + this.item.response = RequestItemHelper.genResonseByResponseType(this.item.responseType, readerReceivedValue); + } + this.onUpdateCallback(this.item); + return result; + }); + }; + + reader.cancel = (...args) => { + this.item.cancelState = 2; + this.item.statusText = 'Cancel'; + this.item.endTime = Date.now(); + this.item.costTime = this.item.endTime - (this.item.startTime || this.item.endTime); + this.item.response = RequestItemHelper.genResonseByResponseType(this.item.responseType, readerReceivedValue); + this.onUpdateCallback(this.item); + return _cancel.apply(reader, args); + }; + return reader; + }; + } +} + +export class FetchProxyHandler implements ProxyHandler { + protected onUpdateCallback: IOnUpdateCallback; + + constructor(onUpdateCallback: IOnUpdateCallback) { + this.onUpdateCallback = onUpdateCallback; + } + + public apply(target: T, thisArg: T, argsList) { + const input: RequestInfo = argsList[0]; + const init: RequestInit = argsList[1]; + const item = new VConsoleNetworkRequestItem(); + this.beforeFetch(item, input, init); + + return (>target.apply(thisArg, argsList)).then(this.afterFetch(item)).catch((e) => { + // mock finally + item.endTime = Date.now(); + item.costTime = item.endTime - (item.startTime || item.endTime); + this.onUpdateCallback(item); + throw e; + }); + } + + protected beforeFetch(item: VConsoleNetworkRequestItem, input: RequestInfo, init?: RequestInit) { + let url: URL, + method = 'GET', + requestHeader: HeadersInit = null; + + // handle `input` content + if (tool.isString(input)) { // when `input` is a string + method = init?.method || 'GET'; + url = RequestItemHelper.getURL(input); + requestHeader = init?.headers || null; + } else { // when `input` is a `Request` object + method = (input).method || 'GET'; + url = RequestItemHelper.getURL((input).url); + requestHeader = (input).headers; + } + + item.method = method; + item.requestType = 'fetch'; + item.requestHeader = requestHeader; + item.url = url.toString(); + item.name = (url.pathname.split('/').pop() || '') + url.search; + item.status = 0; + item.statusText = 'Pending'; + if (!item.startTime) { // UNSENT + item.startTime = (+new Date()); + } + + if (Object.prototype.toString.call(requestHeader) === '[object Headers]') { + item.requestHeader = {}; + for (const [key, value] of requestHeader) { + item.requestHeader[key] = value; + } + } else { + item.requestHeader = requestHeader; + } + + // save GET data + if (url.search && url.searchParams) { + item.getData = {}; + for (const [key, value] of url.searchParams) { + item.getData[key] = value; + } + } + + // save POST data + if (init?.body) { + item.postData = RequestItemHelper.genFormattedBody(init.body); + } + + // const request = tool.isString(input) ? url.toString() : input; + + this.onUpdateCallback(item); + } + + protected afterFetch(item) { + const then = (resp: Response) => { + item.endTime = Date.now(); + item.costTime = item.endTime - (item.startTime || item.endTime); + item.status = resp.status; + item.statusText = String(resp.status); + + item.header = {}; + for (const [key, value] of resp.headers) { + item.header[key] = value; + } + item.readyState = 4; + + this.onUpdateCallback(item); + + return new Proxy(resp, new ResponseProxyHandler(resp, item, this.onUpdateCallback)); + + // parse response body by Content-Type + // const contentType = response.headers.get('content-type'); + // if (contentType && contentType.includes('application/json')) { + // item.responseType = 'json'; + // return response.text(); + // } else if (contentType && (contentType.includes('text/html') || contentType.includes('text/plain'))) { + // item.responseType = 'text'; + // return response.text(); + // } else { + // item.responseType = 'blob'; + // return response.text(); + // // item.responseType = ''; + // // return '[object Object]'; + // } + }; + return then; + } +} + +export class FetchProxy { + public static origFetch = fetch; + + public static create(onUpdateCallback: IOnUpdateCallback) { + return new Proxy(fetch, new FetchProxyHandler(onUpdateCallback)); + } +} diff --git a/src/network/network.model.ts b/src/network/network.model.ts index d8e9ce08..29c6b022 100644 --- a/src/network/network.model.ts +++ b/src/network/network.model.ts @@ -2,9 +2,9 @@ import { writable, get } from 'svelte/store'; import * as tool from '../lib/tool'; import { VConsoleModel } from '../lib/model'; import { contentStore } from '../core/core.model'; -import { VConsoleNetworkRequestItem, RequestItemHelper } from './requestItem'; +import { RequestItemHelper, VConsoleNetworkRequestItem } from './requestItem'; import { XHRProxy } from './xhr.proxy'; -import type { VConsoleRequestMethod } from './requestItem'; +import { FetchProxy } from './fetch.proxy'; /** @@ -20,7 +20,6 @@ export class VConsoleNetworkModel extends VConsoleModel { public maxNetworkNumber: number = 1000; protected itemCounter: number = 0; - private _fetch: WindowOrWorkerGlobalScope['fetch'] = undefined; private _sendBeacon: Navigator['sendBeacon'] = undefined; @@ -33,12 +32,11 @@ export class VConsoleNetworkModel extends VConsoleModel { public unMock() { // recover original functions - if (window.XMLHttpRequest) { + if (window.hasOwnProperty('XMLHttpRequest')) { window.XMLHttpRequest = XHRProxy.origXMLHttpRequest; } - if (window.fetch) { - window.fetch = this._fetch; - this._fetch = undefined; + if (window.hasOwnProperty('fetch')) { + window.fetch = FetchProxy.origFetch; } if (window.navigator.sendBeacon) { window.navigator.sendBeacon = this._sendBeacon; @@ -80,14 +78,13 @@ export class VConsoleNetworkModel extends VConsoleModel { * @private */ private mockXHR() { - const _XMLHttpRequest = window.XMLHttpRequest; - if (!_XMLHttpRequest) { return; } + if (!window.hasOwnProperty('XMLHttpRequest')) { + return; + } window.XMLHttpRequest = XHRProxy.create((item: VConsoleNetworkRequestItem) => { this.updateRequest(item.id, item); }); - - }; /** @@ -95,142 +92,13 @@ export class VConsoleNetworkModel extends VConsoleModel { * @private */ private mockFetch() { - const _fetch = window.fetch; - if (!_fetch) { return; } - const that = this; - this._fetch = _fetch; + if (!window.hasOwnProperty('fetch')) { + return; + } - (window).fetch = (input: RequestInfo, init?: RequestInit) => { - const item = new VConsoleNetworkRequestItem(); + window.fetch = FetchProxy.create((item: VConsoleNetworkRequestItem) => { this.updateRequest(item.id, item); - let url: URL, - method = 'GET', - requestHeader: HeadersInit = null; - let _fetchReponse: Response; - - // handle `input` content - if (tool.isString(input)) { // when `input` is a string - method = init?.method || 'GET'; - url = that.getURL(input); - requestHeader = init?.headers || null; - } else { // when `input` is a `Request` object - method = (input).method || 'GET'; - url = that.getURL((input).url); - requestHeader = (input).headers; - } - - item.method = method; - item.requestType = 'fetch'; - item.requestHeader = requestHeader; - item.url = url.toString(); - item.name = (url.pathname.split('/').pop() || '') + url.search; - item.status = 0; - item.statusText = 'Pending'; - if (!item.startTime) { // UNSENT - item.startTime = (+new Date()); - } - - if (Object.prototype.toString.call(requestHeader) === '[object Headers]') { - item.requestHeader = {}; - for (const [key, value] of requestHeader) { - item.requestHeader[key] = value; - } - } else { - item.requestHeader = requestHeader; - } - - // save GET data - if (url.search && url.searchParams) { - item.getData = {}; - for (const [key, value] of url.searchParams) { - item.getData[key] = value; - } - } - - // save POST data - // if (item.method === 'POST') { - // if (tool.isString(input)) { // when `input` is a string - // item.postData = that.getFormattedBody(init.body); - // } else { // when `input` is a `Request` object - // // cannot get real type of request's body, so just display "[object Object]" - // item.postData = '[object Object]'; - // } - // } - if (init?.body) { - item.postData = that.getFormattedBody(init.body); - } - - const request = tool.isString(input) ? url.toString() : input; - - return _fetch(request, init).then((res) => { - // fix ios<11 https://github.com/github/fetch/issues/504 - const response = res; - _fetchReponse = res; - // const response = new Proxy(res, { - // // set(target, name, value) { - // // console.log('set', name); - // // return Reflect.set(target, name, value); - // // }, - // get(target, name) { - // console.log('get', name) - // if (typeof target[name] === 'function') { - // if (name === 'cancel') { - - // } - // } - // return Reflect.get(target, name); - // }, - // apply(target, thisArg, argumentsList) { - // console.log('apply:', target.name); - // return target(...argumentsList); - // } - // }); - // _fetchReponse = res; - // (window as any)._vcOrigConsole.log('_fetch', _fetchReponse); - - item.endTime = +new Date(); - item.costTime = item.endTime - (item.startTime || item.endTime); - item.status = response.status; - item.statusText = String(response.status); - - item.header = {}; - for (const [key, value] of response.headers) { - item.header[key] = value; - } - item.readyState = 4; - - // parse response body by Content-Type - const contentType = response.headers.get('content-type'); - if (contentType && contentType.includes('application/json')) { - item.responseType = 'json'; - return response.text(); - } else if (contentType && (contentType.includes('text/html') || contentType.includes('text/plain'))) { - item.responseType = 'text'; - return response.text(); - } else { - item.responseType = ''; - return '[object Object]'; - } - - }).then((responseBody) => { - // save response body - item.response = RequestItemHelper.genResonseByResponseType(item.responseType, responseBody); - - // mock finally - that.updateRequest(item.id, item); - - return _fetchReponse; - }).catch((e) => { - // mock finally - that.updateRequest(item.id, item); - throw e; - }); - // ios<11 finally undefined - // .finally(() => { - // _fetchReponse = undefined; - // that.updateRequest(id, item); - // }); - }; + }); } /** @@ -255,7 +123,7 @@ export class VConsoleNetworkModel extends VConsoleModel { const item = new VConsoleNetworkRequestItem(); this.updateRequest(item.id, item); - const url = that.getURL(urlString); + const url = RequestItemHelper.getURL(urlString); item.method = 'POST'; item.url = urlString; item.name = (url.pathname.split('/').pop() || '') + url.search; @@ -270,7 +138,7 @@ export class VConsoleNetworkModel extends VConsoleModel { item.getData[key] = value; } } - item.postData = that.getFormattedBody(data); + item.postData = RequestItemHelper.genFormattedBody(data); if (!item.startTime) { item.startTime = Date.now(); @@ -292,52 +160,6 @@ export class VConsoleNetworkModel extends VConsoleModel { }; } - private getFormattedBody(body?: BodyInit) { - if (!body) { return null; } - let ret: string | { [key: string]: string } = null; - - if (typeof body === 'string') { - try { // '{a:1}' => try to parse as json - ret = JSON.parse(body); - } catch (e) { // 'a=1&b=2' => try to parse as query - const arr = body.split('&'); - if (arr.length === 1) { // not a query, parse as original string - ret = body; - } else { // 'a=1&b=2&c' => parse as query - ret = {}; - for (let q of arr) { - const kv = q.split('='); - ret[ kv[0] ] = kv[1] === undefined ? 'undefined' : kv[1]; - } - } - } - } else if (tool.isIterable(body)) { - // FormData or URLSearchParams or Array - ret = {}; - for (const [key, value] of body) { - ret[key] = typeof value === 'string' ? value : '[object Object]'; - } - } else if (tool.isPlainObject(body)) { - ret = body; - } else { - const type = tool.getPrototypeName(body); - ret = `[object ${type}]`; - } - return ret; - } - - private getURL(urlString: string = '') { - if (urlString.startsWith('//')) { - const baseUrl = new URL(window.location.href); - urlString = `${baseUrl.protocol}${urlString}`; - } - if (urlString.startsWith('http')) { - return new URL(urlString); - } else { - return new URL(urlString, window.location.href); - } - } - protected limitListLength() { // update list length every N rounds const N = 10; diff --git a/src/network/requestItem.ts b/src/network/requestItem.ts index 35f70d0f..c1f89944 100644 --- a/src/network/requestItem.ts +++ b/src/network/requestItem.ts @@ -143,6 +143,7 @@ export const RequestItemHelper = { case 'blob': case 'document': case 'arraybuffer': + case 'formdata': default: if (typeof response !== 'undefined') { ret = Object.prototype.toString.call(response); @@ -250,4 +251,16 @@ export const RequestItemHelper = { } return ret; }, + + getURL(urlString: string = '') { + if (urlString.startsWith('//')) { + const baseUrl = new URL(window.location.href); + urlString = `${baseUrl.protocol}${urlString}`; + } + if (urlString.startsWith('http')) { + return new URL(urlString); + } else { + return new URL(urlString, window.location.href); + } + }, }; From 492b097f2ee6aab8b743842cba3806c34fb534ec Mon Sep 17 00:00:00 2001 From: Maiz Date: Fri, 18 Mar 2022 12:46:28 +0800 Subject: [PATCH 05/11] Refactor(Network): Use proxy on sendBeacon(). --- dev/network.html | 11 +----- src/network/beacon.proxy.ts | 71 ++++++++++++++++++++++++++++++++++++ src/network/network.model.ts | 67 ++++------------------------------ 3 files changed, 80 insertions(+), 69 deletions(-) create mode 100644 src/network/beacon.proxy.ts diff --git a/dev/network.html b/dev/network.html index 280689ae..752298cb 100644 --- a/dev/network.html +++ b/dev/network.html @@ -341,6 +341,7 @@ } function sendBeacon() { + vConsole.show(); console.info('sendBeacon() Start, response should be logged after End'); window.navigator.sendBeacon('./data/success.json?method=beacon', JSON.stringify({ foo: 'bar', @@ -369,16 +370,6 @@ console.info('axiosRequest() End'); } -function sendBeacon() { - console.info('sendBeacon() Start, response should be logged after End'); - window.navigator.sendBeacon('./data/success.json?method=beacon', JSON.stringify({ - foo: 'bar', - id: Math.random(), - type: 'sendBeacon' - })); - console.info('sendBeacon() End'); -} - function axiosRequest(method) { console.info('axiosRequest() Start'); axios({ diff --git a/src/network/beacon.proxy.ts b/src/network/beacon.proxy.ts new file mode 100644 index 00000000..a45c530b --- /dev/null +++ b/src/network/beacon.proxy.ts @@ -0,0 +1,71 @@ +import { VConsoleNetworkRequestItem, RequestItemHelper } from './requestItem'; + +type IOnUpdateCallback = (item: VConsoleNetworkRequestItem) => void; + +// https://fetch.spec.whatwg.org/#concept-bodyinit-extract +const getContentType = (data?: BodyInit) => { + if (data instanceof Blob) { return data.type; } + if (data instanceof FormData) { return 'multipart/form-data'; } + if (data instanceof URLSearchParams) { return 'application/x-www-form-urlencoded;charset=UTF-8'; } + return 'text/plain;charset=UTF-8'; +}; + +export class BeaconProxyHandler implements ProxyHandler { + protected onUpdateCallback: IOnUpdateCallback; + + constructor(onUpdateCallback: IOnUpdateCallback) { + this.onUpdateCallback = onUpdateCallback; + } + + public apply(target: T, thisArg: T, argsList: any[]) { + console.log('on call!!') + const urlString: string = argsList[0]; + const data: BodyInit = argsList[1]; + const item = new VConsoleNetworkRequestItem(); + + const url = RequestItemHelper.getURL(urlString); + item.method = 'POST'; + item.url = urlString; + item.name = (url.pathname.split('/').pop() || '') + url.search; + item.requestType = 'ping'; + item.requestHeader = { 'Content-Type': getContentType(data) }; + item.status = 0; + item.statusText = 'Pending'; + + if (url.search && url.searchParams) { + item.getData = {}; + for (const [key, value] of url.searchParams) { + item.getData[key] = value; + } + } + item.postData = RequestItemHelper.genFormattedBody(data); + + if (!item.startTime) { + item.startTime = Date.now(); + } + + this.onUpdateCallback(item); + + const isSuccess = target.apply(thisArg, argsList); + if (isSuccess) { + item.endTime = Date.now(); + item.costTime = item.endTime - (item.startTime || item.endTime); + item.status = 0; + item.statusText = 'Sent'; + item.readyState = 4; + } else { + item.status = 500; + item.statusText = 'Unknown'; + } + this.onUpdateCallback(item); + return isSuccess; + } +} + +export class BeaconProxy { + public static origSendBeacon = navigator.sendBeacon; + + public static create(onUpdateCallback: IOnUpdateCallback) { + return new Proxy(navigator.sendBeacon, new BeaconProxyHandler(onUpdateCallback)); + } +} diff --git a/src/network/network.model.ts b/src/network/network.model.ts index 29c6b022..bf8ef645 100644 --- a/src/network/network.model.ts +++ b/src/network/network.model.ts @@ -5,6 +5,7 @@ import { contentStore } from '../core/core.model'; import { RequestItemHelper, VConsoleNetworkRequestItem } from './requestItem'; import { XHRProxy } from './xhr.proxy'; import { FetchProxy } from './fetch.proxy'; +import { BeaconProxy } from './beacon.proxy'; /** @@ -19,9 +20,6 @@ export const requestList = writable<{ [id: string]: VConsoleNetworkRequestItem } export class VConsoleNetworkModel extends VConsoleModel { public maxNetworkNumber: number = 1000; protected itemCounter: number = 0; - - private _sendBeacon: Navigator['sendBeacon'] = undefined; - constructor() { super(); @@ -38,9 +36,8 @@ export class VConsoleNetworkModel extends VConsoleModel { if (window.hasOwnProperty('fetch')) { window.fetch = FetchProxy.origFetch; } - if (window.navigator.sendBeacon) { - window.navigator.sendBeacon = this._sendBeacon; - this._sendBeacon = undefined; + if (!!window.navigator.sendBeacon) { + window.navigator.sendBeacon = BeaconProxy.origSendBeacon; } } @@ -81,7 +78,6 @@ export class VConsoleNetworkModel extends VConsoleModel { if (!window.hasOwnProperty('XMLHttpRequest')) { return; } - window.XMLHttpRequest = XHRProxy.create((item: VConsoleNetworkRequestItem) => { this.updateRequest(item.id, item); }); @@ -95,7 +91,6 @@ export class VConsoleNetworkModel extends VConsoleModel { if (!window.hasOwnProperty('fetch')) { return; } - window.fetch = FetchProxy.create((item: VConsoleNetworkRequestItem) => { this.updateRequest(item.id, item); }); @@ -106,58 +101,12 @@ export class VConsoleNetworkModel extends VConsoleModel { * @private */ private mockSendBeacon() { - const _sendBeacon = window.navigator.sendBeacon; - if (!_sendBeacon) { return; } - const that = this; - this._sendBeacon = _sendBeacon; - - // https://fetch.spec.whatwg.org/#concept-bodyinit-extract - const getContentType = (data?: BodyInit) => { - if (data instanceof Blob) { return data.type; } - if (data instanceof FormData) { return 'multipart/form-data'; } - if (data instanceof URLSearchParams) { return 'application/x-www-form-urlencoded;charset=UTF-8'; } - return 'text/plain;charset=UTF-8'; - }; - - window.navigator.sendBeacon = (urlString: string, data?: BodyInit) => { - const item = new VConsoleNetworkRequestItem(); + if (!window.navigator.sendBeacon) { + return; + } + window.navigator.sendBeacon = BeaconProxy.create((item: VConsoleNetworkRequestItem) => { this.updateRequest(item.id, item); - - const url = RequestItemHelper.getURL(urlString); - item.method = 'POST'; - item.url = urlString; - item.name = (url.pathname.split('/').pop() || '') + url.search; - item.requestType = 'ping'; - item.requestHeader = { 'Content-Type': getContentType(data) }; - item.status = 0; - item.statusText = 'Pending'; - - if (url.search && url.searchParams) { - item.getData = {}; - for (const [key, value] of url.searchParams) { - item.getData[key] = value; - } - } - item.postData = RequestItemHelper.genFormattedBody(data); - - if (!item.startTime) { - item.startTime = Date.now(); - } - - const isSuccess = _sendBeacon.call(window.navigator, urlString, data); - if (isSuccess) { - item.endTime = Date.now(); - item.costTime = item.endTime - (item.startTime || item.endTime); - item.status = 0; - item.statusText = 'Sent'; - item.readyState = 4; - } else { - item.status = 500; - item.statusText = 'Unknown'; - } - that.updateRequest(item.id, item); - return isSuccess; - }; + }); } protected limitListLength() { From 1f185b7b8c12629c2c1c8896e08042ee1a1fc380 Mon Sep 17 00:00:00 2001 From: Maiz Date: Fri, 18 Mar 2022 14:21:28 +0800 Subject: [PATCH 06/11] Refactor(Network): use helper functions. --- src/network/beacon.proxy.ts | 8 +- src/network/fetch.proxy.ts | 15 +-- src/network/helper.ts | 181 +++++++++++++++++++++++++++++++++ src/network/network.model.ts | 3 +- src/network/requestItem.ts | 188 ++--------------------------------- src/network/xhr.proxy.ts | 11 +- 6 files changed, 207 insertions(+), 199 deletions(-) create mode 100644 src/network/helper.ts diff --git a/src/network/beacon.proxy.ts b/src/network/beacon.proxy.ts index a45c530b..feca0bf6 100644 --- a/src/network/beacon.proxy.ts +++ b/src/network/beacon.proxy.ts @@ -1,4 +1,6 @@ -import { VConsoleNetworkRequestItem, RequestItemHelper } from './requestItem'; + +import * as Helper from './helper'; +import { VConsoleNetworkRequestItem } from './requestItem'; type IOnUpdateCallback = (item: VConsoleNetworkRequestItem) => void; @@ -23,7 +25,7 @@ export class BeaconProxyHandler implement const data: BodyInit = argsList[1]; const item = new VConsoleNetworkRequestItem(); - const url = RequestItemHelper.getURL(urlString); + const url = Helper.getURL(urlString); item.method = 'POST'; item.url = urlString; item.name = (url.pathname.split('/').pop() || '') + url.search; @@ -38,7 +40,7 @@ export class BeaconProxyHandler implement item.getData[key] = value; } } - item.postData = RequestItemHelper.genFormattedBody(data); + item.postData = Helper.genFormattedBody(data); if (!item.startTime) { item.startTime = Date.now(); diff --git a/src/network/fetch.proxy.ts b/src/network/fetch.proxy.ts index e393a0d5..6f00be8e 100644 --- a/src/network/fetch.proxy.ts +++ b/src/network/fetch.proxy.ts @@ -1,5 +1,6 @@ import * as tool from '../lib/tool'; -import { VConsoleNetworkRequestItem, RequestItemHelper } from './requestItem'; +import * as Helper from './helper'; +import { VConsoleNetworkRequestItem } from './requestItem'; import type { VConsoleRequestMethod } from './requestItem'; type IOnUpdateCallback = (item: VConsoleNetworkRequestItem) => void; @@ -33,7 +34,7 @@ export class ResponseProxyHandler implements ProxyHandler return () => { this.item.responseType = key.toLowerCase(); return target[key]().then((val) => { - this.item.response = RequestItemHelper.genResonseByResponseType(this.item.responseType, val); + this.item.response = Helper.genResonseByResponseType(this.item.responseType, val); this.onUpdateCallback(this.item); return val; }); @@ -65,7 +66,7 @@ export class ResponseProxyHandler implements ProxyHandler this.item.statusText = result.done ? String(this.item.status) : 'Loading'; readerReceivedValue = new Uint8Array([...readerReceivedValue, ...result.value]); if (result.done) { - this.item.response = RequestItemHelper.genResonseByResponseType(this.item.responseType, readerReceivedValue); + this.item.response = Helper.genResonseByResponseType(this.item.responseType, readerReceivedValue); } this.onUpdateCallback(this.item); return result; @@ -77,7 +78,7 @@ export class ResponseProxyHandler implements ProxyHandler this.item.statusText = 'Cancel'; this.item.endTime = Date.now(); this.item.costTime = this.item.endTime - (this.item.startTime || this.item.endTime); - this.item.response = RequestItemHelper.genResonseByResponseType(this.item.responseType, readerReceivedValue); + this.item.response = Helper.genResonseByResponseType(this.item.responseType, readerReceivedValue); this.onUpdateCallback(this.item); return _cancel.apply(reader, args); }; @@ -116,11 +117,11 @@ export class FetchProxyHandler implements ProxyHandlerinput); + url = Helper.getURL(input); requestHeader = init?.headers || null; } else { // when `input` is a `Request` object method = (input).method || 'GET'; - url = RequestItemHelper.getURL((input).url); + url = Helper.getURL((input).url); requestHeader = (input).headers; } @@ -154,7 +155,7 @@ export class FetchProxyHandler implements ProxyHandler { + if (!tool.isObject(getData)) { + getData = {}; + } + let query: string[] = url ? url.split('?') : []; // a.php?b=c&d=?e => ['a.php', 'b=c&d=', 'e'] + query.shift(); // => ['b=c&d=', 'e'] + if (query.length > 0) { + query = query.join('?').split('&'); // => 'b=c&d=?e' => ['b=c', 'd=?e'] + for (const q of query) { + const kv = q.split('='); + try { + getData[ kv[0] ] = decodeURIComponent(kv[1]); + } catch (e) { + // "URIError: URI malformed" will be thrown when `kv[1]` contains "%", so just use raw data + // @issue #470 + // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Malformed_URI + getData[ kv[0] ] = kv[1]; + } + } + } + return getData; +}; + +/** + * Generate formatted response data by responseType. + */ +export const genResonseByResponseType = (responseType: string, response) => { + let ret = ''; + switch (responseType) { + case '': + case 'text': + case 'json': + // try to parse JSON + if (tool.isString(response)) { + try { + ret = JSON.parse(response); + ret = tool.safeJSONStringify(ret, { maxDepth: 10, keyMaxLen: 500000, pretty: true }); + } catch (e) { + // not a JSON string + ret = tool.getStringWithinLength(String(response), 500000); + } + } else if (tool.isObject(response) || tool.isArray(response)) { + ret = tool.safeJSONStringify(response, { maxDepth: 10, keyMaxLen: 500000, pretty: true }); + } else if (typeof response !== 'undefined') { + ret = Object.prototype.toString.call(response); + ret = tool.getStringWithinLength(ret, 500000); + } + break; + + case 'blob': + case 'document': + case 'arraybuffer': + case 'formdata': + default: + if (typeof response !== 'undefined') { + ret = Object.prototype.toString.call(response); + ret = tool.getStringWithinLength(ret, 500000); + } + break; + } + return ret; +}; + +/** + * Update item's properties according to readyState. + */ +export const updateItemByReadyState = (item: VConsoleNetworkRequestItem, XMLReq: XMLHttpRequest) => { + switch (XMLReq.readyState) { + case 0: // UNSENT + item.status = 0; + item.statusText = 'Pending'; + if (!item.startTime) { + item.startTime = (+new Date()); + } + break; + + case 1: // OPENED + item.status = 0; + item.statusText = 'Pending'; + if (!item.startTime) { + item.startTime = (+new Date()); + } + break; + + case 2: // HEADERS_RECEIVED + item.status = XMLReq.status; + item.statusText = 'Loading'; + item.header = {}; + const header = XMLReq.getAllResponseHeaders() || '', + headerArr = header.split('\n'); + // extract plain text to key-value format + for (let i = 0; i < headerArr.length; i++) { + const line = headerArr[i]; + if (!line) { continue; } + const arr = line.split(': '); + const key = arr[0], + value = arr.slice(1).join(': '); + item.header[key] = value; + } + break; + + case 3: // LOADING + item.status = XMLReq.status; + item.statusText = 'Loading'; + break; + + case 4: // DONE + // clearInterval(timer); + // `XMLReq.abort()` will change `status` from 200 to 0, so use previous value in this case + item.status = XMLReq.status || item.status || 0; + item.statusText = String(item.status); // show status code when request completed + item.endTime = Date.now(), + item.costTime = item.endTime - (item.startTime || item.endTime); + item.response = XMLReq.response; + break; + + default: + // clearInterval(timer); + item.status = XMLReq.status; + item.statusText = 'Unknown'; + break; + } +}; + +/** + * Generate formatted response body by XMLHttpRequestBodyInit. + */ +export const genFormattedBody = (body?: BodyInit) => { + if (!body) { return null; } + let ret: string | { [key: string]: string } = null; + + if (typeof body === 'string') { + try { // '{a:1}' => try to parse as json + ret = JSON.parse(body); + } catch (e) { // 'a=1&b=2' => try to parse as query + const arr = body.split('&'); + if (arr.length === 1) { // not a query, parse as original string + ret = body; + } else { // 'a=1&b=2&c' => parse as query + ret = {}; + for (let q of arr) { + const kv = q.split('='); + ret[ kv[0] ] = kv[1] === undefined ? 'undefined' : kv[1]; + } + } + } + } else if (tool.isIterable(body)) { + // FormData or URLSearchParams or Array + ret = {}; + for (const [key, value] of body) { + ret[key] = typeof value === 'string' ? value : '[object Object]'; + } + } else if (tool.isPlainObject(body)) { + ret = body; + } else { + const type = tool.getPrototypeName(body); + ret = `[object ${type}]`; + } + return ret; +}; + +/** + * Get formatted URL object by string. + */ +export const getURL = (urlString: string = '') => { + if (urlString.startsWith('//')) { + const baseUrl = new URL(window.location.href); + urlString = `${baseUrl.protocol}${urlString}`; + } + if (urlString.startsWith('http')) { + return new URL(urlString); + } else { + return new URL(urlString, window.location.href); + } +}; diff --git a/src/network/network.model.ts b/src/network/network.model.ts index bf8ef645..5a039354 100644 --- a/src/network/network.model.ts +++ b/src/network/network.model.ts @@ -1,8 +1,7 @@ import { writable, get } from 'svelte/store'; -import * as tool from '../lib/tool'; import { VConsoleModel } from '../lib/model'; import { contentStore } from '../core/core.model'; -import { RequestItemHelper, VConsoleNetworkRequestItem } from './requestItem'; +import { VConsoleNetworkRequestItem } from './requestItem'; import { XHRProxy } from './xhr.proxy'; import { FetchProxy } from './fetch.proxy'; import { BeaconProxy } from './beacon.proxy'; diff --git a/src/network/requestItem.ts b/src/network/requestItem.ts index c1f89944..ad4f3a08 100644 --- a/src/network/requestItem.ts +++ b/src/network/requestItem.ts @@ -1,4 +1,6 @@ -import * as tool from '../lib/tool'; + +import { getUniqueID } from '../lib/tool'; +import { genResonseByResponseType, genGetDataByUrl } from './helper'; export type VConsoleRequestMethod = '' | 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH'; @@ -25,7 +27,7 @@ export class VConsoleNetworkRequestItem { noVConsole?: boolean = false; constructor() { - this.id = tool.getUniqueID(); + this.id = getUniqueID(); } } @@ -45,7 +47,7 @@ export class VConsoleNetworkRequestItemProxy extends VConsoleNetworkRequestItem // NOTICE: Once the `response` is set, // modifying its internal properties will not take effect, // unless a new `response` is re-assigned. - item._response = RequestItemHelper.genResonseByResponseType(item.responseType, value); + item._response = genResonseByResponseType(item.responseType, value); return true; case 'url': @@ -53,7 +55,7 @@ export class VConsoleNetworkRequestItemProxy extends VConsoleNetworkRequestItem const name = value?.replace(new RegExp('[/]*$'), '').split('/').pop() || 'Unknown'; Reflect.set(item, 'name', name); - const getData = RequestItemHelper.genGetDataByUrl(value, item.getData); + const getData = genGetDataByUrl(value, item.getData); Reflect.set(item, 'getData', getData); break; case 'status': @@ -86,181 +88,3 @@ export class VConsoleNetworkRequestItemProxy extends VConsoleNetworkRequestItem return new Proxy(item, VConsoleNetworkRequestItemProxy.Handler); } } - -export const RequestItemHelper = { - /** - * Generate `getData` by url. - */ - genGetDataByUrl(url: string, getData = {}) { - if (!tool.isObject(getData)) { - getData = {}; - } - let query: string[] = url ? url.split('?') : []; // a.php?b=c&d=?e => ['a.php', 'b=c&d=', 'e'] - query.shift(); // => ['b=c&d=', 'e'] - if (query.length > 0) { - query = query.join('?').split('&'); // => 'b=c&d=?e' => ['b=c', 'd=?e'] - for (const q of query) { - const kv = q.split('='); - try { - getData[ kv[0] ] = decodeURIComponent(kv[1]); - } catch (e) { - // "URIError: URI malformed" will be thrown when `kv[1]` contains "%", so just use raw data - // @issue #470 - // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Malformed_URI - getData[ kv[0] ] = kv[1]; - } - } - } - return getData; - }, - - /** - * Generate formatted response data by responseType. - */ - genResonseByResponseType(responseType: string, response) { - let ret = ''; - switch (responseType) { - case '': - case 'text': - case 'json': - // try to parse JSON - if (tool.isString(response)) { - try { - ret = JSON.parse(response); - ret = tool.safeJSONStringify(ret, { maxDepth: 10, keyMaxLen: 500000, pretty: true }); - } catch (e) { - // not a JSON string - ret = tool.getStringWithinLength(String(response), 500000); - } - } else if (tool.isObject(response) || tool.isArray(response)) { - ret = tool.safeJSONStringify(response, { maxDepth: 10, keyMaxLen: 500000, pretty: true }); - } else if (typeof response !== 'undefined') { - ret = Object.prototype.toString.call(response); - ret = tool.getStringWithinLength(ret, 500000); - } - break; - - case 'blob': - case 'document': - case 'arraybuffer': - case 'formdata': - default: - if (typeof response !== 'undefined') { - ret = Object.prototype.toString.call(response); - ret = tool.getStringWithinLength(ret, 500000); - } - break; - } - return ret; - }, - - /** - * Update item's properties according to readyState. - */ - updateItemByReadyState(item: VConsoleNetworkRequestItem, XMLReq: XMLHttpRequest) { - switch (XMLReq.readyState) { - case 0: // UNSENT - item.status = 0; - item.statusText = 'Pending'; - if (!item.startTime) { - item.startTime = (+new Date()); - } - break; - - case 1: // OPENED - item.status = 0; - item.statusText = 'Pending'; - if (!item.startTime) { - item.startTime = (+new Date()); - } - break; - - case 2: // HEADERS_RECEIVED - item.status = XMLReq.status; - item.statusText = 'Loading'; - item.header = {}; - const header = XMLReq.getAllResponseHeaders() || '', - headerArr = header.split('\n'); - // extract plain text to key-value format - for (let i = 0; i < headerArr.length; i++) { - const line = headerArr[i]; - if (!line) { continue; } - const arr = line.split(': '); - const key = arr[0], - value = arr.slice(1).join(': '); - item.header[key] = value; - } - break; - - case 3: // LOADING - item.status = XMLReq.status; - item.statusText = 'Loading'; - break; - - case 4: // DONE - // clearInterval(timer); - // `XMLReq.abort()` will change `status` from 200 to 0, so use previous value in this case - item.status = XMLReq.status || item.status || 0; - item.statusText = String(item.status); // show status code when request completed - item.endTime = Date.now(), - item.costTime = item.endTime - (item.startTime || item.endTime); - item.response = XMLReq.response; - break; - - default: - // clearInterval(timer); - item.status = XMLReq.status; - item.statusText = 'Unknown'; - break; - } - }, - - /** - * Generate formatted response body by XMLHttpRequestBodyInit. - */ - genFormattedBody(body?: BodyInit) { - if (!body) { return null; } - let ret: string | { [key: string]: string } = null; - - if (typeof body === 'string') { - try { // '{a:1}' => try to parse as json - ret = JSON.parse(body); - } catch (e) { // 'a=1&b=2' => try to parse as query - const arr = body.split('&'); - if (arr.length === 1) { // not a query, parse as original string - ret = body; - } else { // 'a=1&b=2&c' => parse as query - ret = {}; - for (let q of arr) { - const kv = q.split('='); - ret[ kv[0] ] = kv[1] === undefined ? 'undefined' : kv[1]; - } - } - } - } else if (tool.isIterable(body)) { - // FormData or URLSearchParams or Array - ret = {}; - for (const [key, value] of body) { - ret[key] = typeof value === 'string' ? value : '[object Object]'; - } - } else if (tool.isPlainObject(body)) { - ret = body; - } else { - const type = tool.getPrototypeName(body); - ret = `[object ${type}]`; - } - return ret; - }, - - getURL(urlString: string = '') { - if (urlString.startsWith('//')) { - const baseUrl = new URL(window.location.href); - urlString = `${baseUrl.protocol}${urlString}`; - } - if (urlString.startsWith('http')) { - return new URL(urlString); - } else { - return new URL(urlString, window.location.href); - } - }, -}; diff --git a/src/network/xhr.proxy.ts b/src/network/xhr.proxy.ts index ceb42697..1830dab4 100644 --- a/src/network/xhr.proxy.ts +++ b/src/network/xhr.proxy.ts @@ -1,4 +1,5 @@ -import { VConsoleNetworkRequestItem, RequestItemHelper } from './requestItem'; +import * as Helper from './helper'; +import { VConsoleNetworkRequestItem } from './requestItem'; type IOnUpdateCallback = (item: VConsoleNetworkRequestItem) => void; @@ -67,10 +68,10 @@ export class XHRProxyHandler implements ProxyHandler implements ProxyHandler implements ProxyHandler { // console.log('Proxy send()'); const data: XMLHttpRequestBodyInit = args[0]; - this.item.postData = RequestItemHelper.genFormattedBody(data); + this.item.postData = Helper.genFormattedBody(data); this.triggerUpdate(); return target.send.apply(target, args); }; From d80ae3cf99d726cdce3e6ac633fe73f1497a204c Mon Sep 17 00:00:00 2001 From: Maiz Date: Fri, 18 Mar 2022 16:47:42 +0800 Subject: [PATCH 07/11] Feat(Network): Add response size. --- CHANGELOG.md | 2 + CHANGELOG_CN.md | 8 ++++ dev/network.html | 11 ++++-- package.json | 2 +- src/lib/tool.ts | 8 ++-- src/network/fetch.proxy.ts | 79 +++++++++++++++++++++++++------------- src/network/helper.ts | 12 +++--- src/network/network.svelte | 10 +++++ src/network/requestItem.ts | 4 +- 9 files changed, 96 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bf4b8d1..ffb6e989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ English | [简体中文](./CHANGELOG_CN.md) ## 3.14.0-rc (2022-03-??) +- `Feat(Network)` Add response size. +- `Feat(Network)` Add support for `transfer-encoding: chunked`, now streaming response can be recorded. - `Feat(Network)` Improve rendering performance of large Response data by cropping the displayed response content. - `Refactor(Network)` Now network records will be more accurate by using Proxy to prevent `XMLHttpRequest | fetch` overwriting by other request libraries (like Axios). diff --git a/CHANGELOG_CN.md b/CHANGELOG_CN.md index 61108d8a..c4971579 100644 --- a/CHANGELOG_CN.md +++ b/CHANGELOG_CN.md @@ -1,5 +1,13 @@ [English](./CHANGELOG.md) | 简体中文 +## 3.14.0-rc (2022-03-??) + +- `Feat(Network)` 新增显示 Response 的体积。 +- `Feat(Network)` 新增对 `transfer-encoding: chunked` 的支持,现在可记录流式回包(stream response)。 +- `Feat(Network)` 展示时裁剪过大的 Response 回包以提高渲染性能。 +- `Refactor(Network)` 提高网络记录的准确性,以避免被外部库(如 Axios)覆盖;方法是对 `XMLHttpRequest | fetch` 使用 Proxy。 + + ## 3.13.0 (2022-03-15) - `Feat(Log)` 新增配置项 `log.showTimestames`,见 [公共属性及方法](./doc/public_properties_methods_CN.md)。 diff --git a/dev/network.html b/dev/network.html index 752298cb..67382d43 100644 --- a/dev/network.html +++ b/dev/network.html @@ -142,10 +142,15 @@ }, }).then((data) => { console.log('getFetch() response:', data); + setTimeout(() => { + data.json().then((res) => { + console.log(res); + }); + }, 3000); // return data; - return data.json(); + // return data.json(); }).then((data) => { - console.log('getFetch() json:', data); + // console.log('getFetch() json:', data); }); } @@ -305,7 +310,7 @@ xhr.onprogress = (e) => { // console.log('XHR onprogress:', 'readyState:', xhr.readyState, 'status:', xhr.status, 'loaded:', e.loaded, 'timeStamp:', e.timeStamp); if (e.loaded > 3000000) { - // xhr.abort(); + xhr.abort(); } }; xhr.onloadstart = (e) => { diff --git a/package.json b/package.json index fab5bb61..f8610cfd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vconsole", - "version": "3.13.1-rc", + "version": "3.14.0-rc", "description": "A lightweight, extendable front-end developer tool for mobile web page.", "homepage": "https://github.com/Tencent/vConsole", "files": [ diff --git a/src/lib/tool.ts b/src/lib/tool.ts index 2961cc27..1e9c17ab 100644 --- a/src/lib/tool.ts +++ b/src/lib/tool.ts @@ -349,11 +349,11 @@ export function getBytesText(bytes: number) { if (bytes <= 0) { return ''; } - if (bytes >= 1024 * 1024) { - return (bytes / 1024 / 1024).toFixed(1) + ' MB'; + if (bytes >= 1000 * 1000) { + return (bytes / 1000 / 1000).toFixed(1) + ' MB'; } - if (bytes >= 1024 * 1) { - return (bytes / 1024).toFixed(1) + ' KB'; + if (bytes >= 1000 * 1) { + return (bytes / 1000).toFixed(1) + ' KB'; } return bytes + ' B'; } diff --git a/src/network/fetch.proxy.ts b/src/network/fetch.proxy.ts index 6f00be8e..0004ac5a 100644 --- a/src/network/fetch.proxy.ts +++ b/src/network/fetch.proxy.ts @@ -18,12 +18,12 @@ export class ResponseProxyHandler implements ProxyHandler this.mockReader(); } - public set(target, key, value) { + public set(target: T, key: string, value) { // if (typeof key === 'string') { console.log('Proxy set:', key) } return Reflect.set(target, key, value); } - public get(target, key) { + public get(target: T, key: string) { // if (typeof key === 'string') { console.log('Proxy get:', key) } switch (key) { case 'arrayBuffer': @@ -33,10 +33,10 @@ export class ResponseProxyHandler implements ProxyHandler case 'text': return () => { this.item.responseType = key.toLowerCase(); - return target[key]().then((val) => { - this.item.response = Helper.genResonseByResponseType(this.item.responseType, val); + return target[key]().then((resp) => { + this.item.response = Helper.genResonseByResponseType(this.item.responseType, resp); this.onUpdateCallback(this.item); - return val; + return resp; }); }; } @@ -50,7 +50,7 @@ export class ResponseProxyHandler implements ProxyHandler } protected mockReader() { - let readerReceivedValue = new Uint8Array(); + let readerReceivedValue: Uint8Array; const _getReader = this.resp.body.getReader; this.resp.body.getReader = () => { const reader = >_getReader.apply(this.resp.body); @@ -60,11 +60,20 @@ export class ResponseProxyHandler implements ProxyHandler reader.read = () => { return (>_read.apply(reader)).then((result) => { + if (!readerReceivedValue) { + readerReceivedValue = new Uint8Array(result.value); + } else { + const newValue = new Uint8Array(readerReceivedValue.length + result.value.length); + newValue.set(readerReceivedValue); + newValue.set(result.value, readerReceivedValue.length); + readerReceivedValue = newValue; + } this.item.endTime = Date.now(); this.item.costTime = this.item.endTime - (this.item.startTime || this.item.endTime); this.item.readyState = result.done ? 4 : 3; this.item.statusText = result.done ? String(this.item.status) : 'Loading'; - readerReceivedValue = new Uint8Array([...readerReceivedValue, ...result.value]); + this.item.responseSize = readerReceivedValue.length; + this.item.responseSizeText = tool.getBytesText(this.item.responseSize); if (result.done) { this.item.response = Helper.genResonseByResponseType(this.item.responseType, readerReceivedValue); } @@ -132,6 +141,7 @@ export class FetchProxyHandler implements ProxyHandler implements ProxyHandler implements ProxyHandler -1 ? true : isChunked; + } + + if (isChunked) { + // when `transfer-encoding` is chunked, the response is a stream which is under loading, + // so the `readyState` should be 3 (Loading), + // and the response should NOT be `clone()` which will affect stream reading. + item.readyState = 3; + } else { + // Otherwise, not chunked, the response is not a stream, + // so it's completed and can be `clone()` for `text()` calling. + item.readyState = 4; + + this.handleResponseBody(resp.clone(), item).then((responseValue: string | ArrayBuffer) => { + console.log(item.responseType, responseValue) + item.responseSize = typeof responseValue === 'string' ? responseValue.length : responseValue.byteLength; + item.responseSizeText = tool.getBytesText(item.responseSize); + item.response = Helper.genResonseByResponseType(item.responseType, responseValue); + this.onUpdateCallback(item); + }); } - item.readyState = 4; this.onUpdateCallback(item); - return new Proxy(resp, new ResponseProxyHandler(resp, item, this.onUpdateCallback)); - - // parse response body by Content-Type - // const contentType = response.headers.get('content-type'); - // if (contentType && contentType.includes('application/json')) { - // item.responseType = 'json'; - // return response.text(); - // } else if (contentType && (contentType.includes('text/html') || contentType.includes('text/plain'))) { - // item.responseType = 'text'; - // return response.text(); - // } else { - // item.responseType = 'blob'; - // return response.text(); - // // item.responseType = ''; - // // return '[object Object]'; - // } }; return then; } + + protected handleResponseBody(resp: Response, item: VConsoleNetworkRequestItem) { + // parse response body by Content-Type + const contentType = resp.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + item.responseType = 'json'; + return resp.text(); + } else if (contentType && (contentType.includes('text/html') || contentType.includes('text/plain'))) { + item.responseType = 'text'; + return resp.text(); + } else { + item.responseType = 'arraybuffer'; + return resp.arrayBuffer(); + } + } } export class FetchProxy { diff --git a/src/network/helper.ts b/src/network/helper.ts index d3a7e9ec..55bcd515 100644 --- a/src/network/helper.ts +++ b/src/network/helper.ts @@ -30,7 +30,7 @@ export const genGetDataByUrl = (url: string, getData = {}) => { /** * Generate formatted response data by responseType. */ -export const genResonseByResponseType = (responseType: string, response) => { +export const genResonseByResponseType = (responseType: string, response: any) => { let ret = ''; switch (responseType) { case '': @@ -49,7 +49,6 @@ export const genResonseByResponseType = (responseType: string, response) => { ret = tool.safeJSONStringify(response, { maxDepth: 10, keyMaxLen: 500000, pretty: true }); } else if (typeof response !== 'undefined') { ret = Object.prototype.toString.call(response); - ret = tool.getStringWithinLength(ret, 500000); } break; @@ -60,7 +59,6 @@ export const genResonseByResponseType = (responseType: string, response) => { default: if (typeof response !== 'undefined') { ret = Object.prototype.toString.call(response); - ret = tool.getStringWithinLength(ret, 500000); } break; } @@ -108,20 +106,24 @@ export const updateItemByReadyState = (item: VConsoleNetworkRequestItem, XMLReq: case 3: // LOADING item.status = XMLReq.status; item.statusText = 'Loading'; + item.responseSize = XMLReq.response.length; + item.responseSizeText = tool.getBytesText(item.responseSize); break; case 4: // DONE - // clearInterval(timer); // `XMLReq.abort()` will change `status` from 200 to 0, so use previous value in this case item.status = XMLReq.status || item.status || 0; item.statusText = String(item.status); // show status code when request completed item.endTime = Date.now(), item.costTime = item.endTime - (item.startTime || item.endTime); item.response = XMLReq.response; + if (XMLReq.response.length) { + item.responseSize = XMLReq.response.length; + item.responseSizeText = tool.getBytesText(item.responseSize); + } break; default: - // clearInterval(timer); item.status = XMLReq.status; item.statusText = 'Unknown'; break; diff --git a/src/network/network.svelte b/src/network/network.svelte index e5baa303..a8175100 100644 --- a/src/network/network.svelte +++ b/src/network/network.svelte @@ -71,6 +71,10 @@
Request Type
{req.requestType}
+
+
HTTP Status
+
{req.status}
+
{#if (req.requestHeader !== null)}
@@ -149,6 +153,12 @@ + {#if (req.responseSize > 0)} +
+
Size
+
{req.responseSizeText}
+
+ {/if}
{req.response || ''}
diff --git a/src/network/requestItem.ts b/src/network/requestItem.ts index ad4f3a08..2f7a48ed 100644 --- a/src/network/requestItem.ts +++ b/src/network/requestItem.ts @@ -18,13 +18,15 @@ export class VConsoleNetworkRequestItem { requestType: 'xhr' | 'fetch' | 'ping' | 'custom'; requestHeader: HeadersInit = null; response: any; + responseSize: number = 0; // bytes + responseSizeText: string = ''; startTime: number = 0; endTime: number = 0; costTime?: number = 0; getData: { [key: string]: string } = null; postData: { [key: string]: string } | string = null; actived: boolean = false; - noVConsole?: boolean = false; + noVConsole?: boolean = false; constructor() { this.id = getUniqueID(); From ccc4eccb9220cfe14bf5df87621d6804e819e3d0 Mon Sep 17 00:00:00 2001 From: Maiz Date: Tue, 22 Mar 2022 11:13:47 +0800 Subject: [PATCH 08/11] Feat(Core): Panel will auto scroll to previous position when switching plugin panel. --- CHANGELOG.md | 1 + CHANGELOG_CN.md | 1 + src/core/core.svelte | 11 ++++++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffb6e989..608de467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ English | [简体中文](./CHANGELOG_CN.md) ## 3.14.0-rc (2022-03-??) +- `Feat(Core)` Panel will auto scroll to previous position when switching plugin panel. - `Feat(Network)` Add response size. - `Feat(Network)` Add support for `transfer-encoding: chunked`, now streaming response can be recorded. - `Feat(Network)` Improve rendering performance of large Response data by cropping the displayed response content. diff --git a/CHANGELOG_CN.md b/CHANGELOG_CN.md index c4971579..e063ca41 100644 --- a/CHANGELOG_CN.md +++ b/CHANGELOG_CN.md @@ -2,6 +2,7 @@ ## 3.14.0-rc (2022-03-??) +- `Feat(Core)` 切换插件面板时,面板会自动滚动到上次的位置。 - `Feat(Network)` 新增显示 Response 的体积。 - `Feat(Network)` 新增对 `transfer-encoding: chunked` 的支持,现在可记录流式回包(stream response)。 - `Feat(Network)` 展示时裁剪过大的 Response 回包以提高渲染性能。 diff --git a/src/core/core.svelte b/src/core/core.svelte index b1b7adb9..55cf70fd 100644 --- a/src/core/core.svelte +++ b/src/core/core.svelte @@ -43,6 +43,7 @@ let isInBottom = true; let preivousContentUpdateTime = 0; let cssTimer = null; + const contentScrollTop: { [pluginId: string]: number } = {}; $: { if (show === true) { @@ -117,6 +118,10 @@ scrollToBottom(); } }; + const scrollToPreviousPosition = () => { + if (!divContent) { return; } + divContent.scrollTop = contentScrollTop[activedPluginId] || 0; + }; /************************************* @@ -135,6 +140,9 @@ } activedPluginId = tabId; dispatch('changePanel', { pluginId: tabId }); + setTimeout(() => { + scrollToPreviousPosition(); + }, 0); }; const onTapTopbar = (e, pluginId: string, idx: number) => { const topbar = pluginList[pluginId].topbarList[idx]; @@ -200,7 +208,8 @@ } else { isInBottom = false; } - // (window as any)._vcOrigConsole.log('onContentScroll', isInBottom); + contentScrollTop[activedPluginId] = divContent.scrollTop; + // (window as any)._vcOrigConsole.log('onContentScroll', activedPluginId, isInBottom, contentScrollTop[activedPluginId]); }; /** From bbfdc682b90d59df6ede6675690a870b91647e15 Mon Sep 17 00:00:00 2001 From: Maiz Date: Tue, 22 Mar 2022 17:05:08 +0800 Subject: [PATCH 09/11] Feat(Core): Add new option `pluginOrder` to adjust the order of built-in and custom plugins. --- CHANGELOG.md | 1 + CHANGELOG_CN.md | 1 + dev/plugin.html | 10 +++------- doc/public_properties_methods.md | 3 ++- doc/public_properties_methods_CN.md | 4 +++- src/core/core.ts | 25 ++++++++++++++++++++++++- src/core/options.interface.ts | 1 + 7 files changed, 35 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 608de467..e528112c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ English | [简体中文](./CHANGELOG_CN.md) ## 3.14.0-rc (2022-03-??) +- `Feat(Core)` Add new option `pluginOrder` to adjust the order of built-in and custom plugins, see [Public Properties & Methods](./doc/public_properties_methods.md). - `Feat(Core)` Panel will auto scroll to previous position when switching plugin panel. - `Feat(Network)` Add response size. - `Feat(Network)` Add support for `transfer-encoding: chunked`, now streaming response can be recorded. diff --git a/CHANGELOG_CN.md b/CHANGELOG_CN.md index e063ca41..fd74cae6 100644 --- a/CHANGELOG_CN.md +++ b/CHANGELOG_CN.md @@ -2,6 +2,7 @@ ## 3.14.0-rc (2022-03-??) +- `Feat(Core)` 新增配置项 `pluginOrder` 来调整插件面板的排序,见 [公共属性及方法](./doc/public_properties_methods_CN.md)。 - `Feat(Core)` 切换插件面板时,面板会自动滚动到上次的位置。 - `Feat(Network)` 新增显示 Response 的体积。 - `Feat(Network)` 新增对 `transfer-encoding: chunked` 的支持,现在可记录流式回包(stream response)。 diff --git a/dev/plugin.html b/dev/plugin.html index 83873eee..b9b7e760 100644 --- a/dev/plugin.html +++ b/dev/plugin.html @@ -25,18 +25,13 @@