├── .gitignore ├── tsconfig.json ├── webpack.js ├── LICENSE ├── package.json ├── README.md └── src └── index.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "includes": ["./*"] 3 | } 4 | -------------------------------------------------------------------------------- /webpack.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | mode: "production", 5 | entry: { 6 | index: "./src/index.ts", 7 | }, 8 | output: { 9 | filename: "[name].bundle.js", 10 | path: path.resolve(__dirname, "dist"), 11 | libraryTarget: 'umd' 12 | }, 13 | resolve: { 14 | extensions: [".ts", ".js"], 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.ts$/, 20 | use: ["babel-loader", "ts-loader"], 21 | }, 22 | ], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SugarTurboS 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. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repeat-request-minder", 3 | "title": "repeat-request-minder", 4 | "version": "1.0.0", 5 | "description": "一个可以帮助你检查项目中是否存在重复请求的工具。使用后会自动监听请求,当发现1秒内有发出多次相同的请求时,会toast提示并在控制台上打印请求信息。", 6 | "homepage": "https://github.com/SugarTurboS/repeat-request-minder", 7 | "main": "dist/index.bundle.js", 8 | "scripts": { 9 | "build": "webpack --config ./webpack.js --report" 10 | }, 11 | "author": { 12 | "name": "sugarteam" 13 | }, 14 | "keywords": [ 15 | "repeat", 16 | "request", 17 | "minder" 18 | ], 19 | "licenses": [ 20 | { 21 | "type": "MIT", 22 | "url": "https://github.com/jquery/jquery/blob/master/MIT-LICENSE.txt" 23 | } 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git@github.com:SugarTurboS/repeat-request-minder.git" 28 | }, 29 | "files": [ 30 | "dist" 31 | ], 32 | "dependencies": { 33 | "@babel/core": "^7.11.4", 34 | "babel-loader": "^8.1.0", 35 | "ts-loader": "^8.0.3", 36 | "typescript": "^4.0.2", 37 | "webpack": "^4.44.1" 38 | }, 39 | "devDependencies": { 40 | "webpack-cli": "^3.3.12" 41 | }, 42 | "publishConfig": { 43 | "registry": "https://registry.npmjs.org/" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to repeat-request-minder 👋

2 |

3 | Version 4 | 5 | License: ISC 6 | 7 |

8 | 9 | > A tool that can help you check whether there are duplicate requests in your project. After use, the request will be automatically monitored. When it is found that the same request has been sent multiple times within 1 second, it will prompt toast and print the request information on the console. 10 | 11 | ## Usage 12 | 13 | ```js 14 | import repeatRequestMinder from 'repeat-request-minder'; 15 | repeatRequestMinder(); 16 | ``` 17 | 18 | You can also configure whether toast is displayed and the duration of toast display (default display toast, duration is 3 seconds) 19 | 20 | ```js 21 | repeatRequestMinder({ 22 | isShowToast: true, 23 | toastTime: 10000 24 | }); 25 | ``` 26 | 27 | As well, if you don't want to use it by call the function yourself, you can use the webpack plugin to help you. 28 | 29 | Here is the plugin [ 30 | repeat-request-minder-webpack-plugin](https://github.com/SugarTurboS/repeat-request-minder-webpack-plugin) 31 | 32 | ## Author 33 | 34 | 👤 **Brady** 35 | 36 | * Github: [@Brady](https://github.com/WadeZhu) 37 | 38 | ## Show your support 39 | 40 | Give a ⭐️ if this project helped you! 41 | 42 | *** 43 | _This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | const LRUCache = function (capacity) { 2 | this.map = {}; 3 | this.size = 0; 4 | this.maxSize = capacity; 5 | 6 | // 链表初始化,初始化只有一个头和尾 7 | this.head = { 8 | prev: null, 9 | next: null, 10 | }; 11 | this.tail = { 12 | prev: this.head, 13 | next: null, 14 | }; 15 | 16 | this.head.next = this.tail; 17 | }; 18 | 19 | LRUCache.prototype.get = function (key) { 20 | if (this.map[key]) { 21 | const node = this.extractNode(this.map[key]); 22 | 23 | // 最新访问,将该节点放到链表的头部 24 | this.insertNodeToHead(node); 25 | 26 | return this.map[key].val; 27 | } else { 28 | return -1; 29 | } 30 | }; 31 | 32 | LRUCache.prototype.put = function (key, value) { 33 | let node; 34 | 35 | if (this.map[key]) { 36 | // 该项已经存在,更新值 37 | node = this.extractNode(this.map[key]); 38 | node.val = value; 39 | } else { 40 | // 如该项不存在,新创造节点 41 | node = { 42 | prev: null, 43 | next: null, 44 | val: value, 45 | key, 46 | }; 47 | 48 | this.map[key] = node; 49 | this.size++; 50 | } 51 | 52 | // 最新写入,将该节点放到链表的头部 53 | this.insertNodeToHead(node); 54 | 55 | // 判断长度是否已经到达上限 56 | if (this.size > this.maxSize) { 57 | const nodeToDelete = this.tail.prev; 58 | const keyToDelete = nodeToDelete.key; 59 | this.extractNode(nodeToDelete); 60 | this.size--; 61 | delete this.map[keyToDelete]; 62 | } 63 | }; 64 | 65 | // 插入节点到链表首项 66 | LRUCache.prototype.insertNodeToHead = function (node) { 67 | const head = this.head; 68 | const lastFirstNode = this.head.next; 69 | 70 | node.prev = head; 71 | head.next = node; 72 | node.next = lastFirstNode; 73 | lastFirstNode.prev = node; 74 | 75 | return node; 76 | }; 77 | 78 | // 从链表中抽取节点 79 | LRUCache.prototype.extractNode = function (node) { 80 | const beforeNode = node.prev; 81 | const afterNode = node.next; 82 | 83 | beforeNode.next = afterNode; 84 | afterNode.prev = beforeNode; 85 | 86 | node.prev = null; 87 | node.next = null; 88 | 89 | return node; 90 | }; 91 | 92 | interface IRequestCache { 93 | url: string; 94 | body: string; 95 | times: number; 96 | timestamp: number; 97 | } 98 | 99 | interface IRequestObj { 100 | url: string; 101 | body: string; 102 | method: string; 103 | async: boolean; 104 | } 105 | 106 | interface IOptions { 107 | isShowToast: boolean; 108 | toastTime: number; 109 | } 110 | 111 | const monitorRepeatRequest = (options: IOptions = { isShowToast: true, toastTime: 3000 }) => { 112 | const { isShowToast, toastTime } = options; 113 | 114 | let requestCache = new LRUCache(30); 115 | let requestInfo = {} as IRequestObj; 116 | 117 | const onXMLOpen = (params) => { 118 | requestInfo.method = params[0]; 119 | requestInfo.url = params[1]; 120 | requestInfo.async = params[2]; 121 | }; 122 | 123 | const onXMLSend = (params) => { 124 | requestInfo.body = params[0]; 125 | checkIsRepeat(requestInfo); 126 | }; 127 | 128 | function checkIsRepeat(requestObj: IRequestObj) { 129 | const { url, body } = requestObj; 130 | const curRequest = requestCache.get(url); 131 | let times = 1; 132 | if (curRequest && Date.now() - curRequest.timestamp < 1000) { 133 | times = curRequest.times + 1; 134 | // 1s内连续发送 135 | if (JSON.stringify(body) === curRequest.body) { 136 | // 相同参数 137 | const errMsg = `${url}在1s内连续请求${times}次,且参数相同,请检查`; 138 | isShowToast && toast(errMsg); 139 | console.log("【重复请求】", errMsg); 140 | } 141 | } 142 | const cacheValue: IRequestCache = { 143 | url, 144 | timestamp: Date.now(), 145 | body: JSON.stringify(body), 146 | times, 147 | }; 148 | requestCache.put(url, cacheValue); 149 | }; 150 | 151 | function toast(msg) { 152 | const styles = { 153 | background: "rgba(51, 51, 51, 0.8)", 154 | border: "1px solid rgba(255, 255, 255, 0.2)", 155 | boxShadow: "0 0 4px 0 rgba(0, 0, 0, 0.2)", 156 | borderRadius: "4px", 157 | padding: "12px 16px", 158 | color: "white", 159 | fontSize: "16px", 160 | position: "fixed", 161 | top: "10%", 162 | left: "50%", 163 | transform: "translateX(-50%)", 164 | zIndex: "9999", 165 | }; 166 | const tipsDom = document.createElement("div"); 167 | tipsDom.innerText = msg; 168 | Object.keys(styles).forEach((key) => { 169 | tipsDom.style[key] = styles[key]; 170 | }); 171 | document.body.appendChild(tipsDom); 172 | setTimeout(() => { 173 | document.body.removeChild(tipsDom); 174 | }, toastTime); 175 | }; 176 | 177 | const originSend = XMLHttpRequest.prototype.send; 178 | const originOpen = XMLHttpRequest.prototype.open; 179 | 180 | XMLHttpRequest.prototype.open = function (...args) { 181 | // 在这里插入open拦截代码 182 | onXMLOpen(args); 183 | return originOpen.apply(this, args); 184 | }; 185 | XMLHttpRequest.prototype.send = function (...args) { 186 | // 在这里插入open拦截代码 187 | onXMLSend(args); 188 | return originSend.apply(this, args); 189 | }; 190 | }; 191 | 192 | export default monitorRepeatRequest; 193 | --------------------------------------------------------------------------------